☰
《明清进士题名碑录》结构化:从碑文到历史人物数据库的实现路径
2026/10/19 3:21:56 网站建设 项目流程

《明清进士题名碑录》我翻过不止一次,但这本大部头一直让我既敬且恨。敬的是它把两朝几万名进士的姓名、籍贯、甲第、名次记录得清清楚楚;恨的是,当我想查“某个地区在明清两代一共出过多少进士”时,面对那几百万字的碑文拓片和排印文本,只能一条条人工筛,一场空。后来我实在受不了,就把这本碑录从头到尾做了一次“结构化重构”——把铭刻在石头上的信息拆成了一张可筛选、可统计、可画地图的表格。这篇文章就是那次重构全过程的方法、坑和心得,给同样想做历史人物数据的朋友留个参考。不管你是数字人文方向的学生、地方志研究者,还是纯粹想统计一下自家祖上出了多少位人才的数据爱好者,这篇应该都用得上。

1. 为什么必须把题名碑录“拆”成数据?——一个业余研究者的开工理由

先说动机。我一直想给家乡的进士做个时空分布图,看看哪些年份集中出人,哪些家族反复上榜。但《明清进士题名碑录》的原始形态是碑刻和拓片,后来虽然有了铅印本和点校本,本质上依然是“段落型文本”:每个人一条信息,但这几万人挤在一起,根本没法直接做条件查询。想统计某个年号、某个地区、某个姓氏,只能靠肉眼或批量检索,检索完还得手动抄表,效率低到让人怀疑人生。

真正让我下决心的是一次失败的“同乡检索”。我在书里搜某府籍进士,花了一个多小时,中途数错页码,最终漏了一个人。那时我就意识到:这类人文典籍,如果不转成结构化数据,它的信息密度再高,普通研究者也吃不到红利。所以项目一开始就很明确:把碑录里的每个人变成一行记录,字段尽量齐全,让电脑替我做那些重复劳动。

1.1 从一次“查同乡进士”的痛到一本书

那次经历让我想通了一件事:题名碑录的痛点不在于内容少,而在于内容多但不可计算。它不像现代数据库,随便来个 WHERE 条件就能得到结果。碑录里的“籍贯”是混合着朝代、行政区划、旧称、别称的杂糅文本,比如某人籍贯可能写“某县某里”,也可能写“某府某县人”,甚至只写“某县人”;“排名”也有两种写法:一种按甲第记录,另一种是按总排名记录,两者还要换算。这些细节单靠“阅读”没问题,但一旦要批量比较,就必须先规定一套整齐的结构。

我开工前先定了几条原则:第一,数据必须做到一进士一行;第二,每一行都要有唯一标识,不能靠姓名当主键;第三,所有原始文本保留一份“原文照录”字段,清洗后的字段单独放,避免破坏原始信息;第四,能拆的字段尽量拆,比如“籍贯”拆成“省-府-县-里”四级,而不是存一大段字符串。这套原则后来帮了大忙。

1.2 结构化数据的适用边界:能回答什么,不能回答什么

也得泼盆冷水。结构化数据不是万能的。它擅长回答“谁、哪里、什么时候、第几名”这类有确定边界的问题;但不擅长回答“为什么这个时期这个地区进士猛增”这种需要结合经济、文化、政治背景的开放性问题。也就是说,数据能做的是把事实摆得更清楚,比如统计某区域上榜人数的趋势、某家族连续登科的年份间隔、某甲第在不同时期的比例变化。但具体“为什么”,仍需历史学判断。

所以做这个项目时,我心里始终有数:我的目标是一张“干净的数据表”,而不是一本已经写成答案的书。数据表可以成为后续研究的地基;它本身不负责解释因果,只负责准确描述现象。

2. 字段设计:籍贯、姓名、排名到底该怎么落库?

字段设计是这个项目的灵魂。设计得好,后面统计一马平川;设计得散漫,后期光改字段就能改到吐。我从最开始就按“分析需求反推字段”的方式来做:先列出一堆我想回答的问题,比如“每个省在明朝的平均排名是多少”“某姓氏在不同朝代的登科率怎么变化”“某府的进士集中分布在哪些年份”,然后把回答这些问题需要的最小字段集定下来。

我最终落库的字段大约有十二个,核心字段如下表:

字段名内容类型示例说明
person_id整型100234唯一人物标识,按录入顺序生成
name文本张某某进士姓名
dynasty文本明 / 清朝代
emperor文本某庙号皇帝(庙号)
reign_year文本某年号该科所在年号
year整型1547实际年份(公历)
ranking_total整型128殿试总排名
class_rank文本三甲第61名按甲第排名
class_label文本三甲一甲/二甲/三甲
province_orig文本某省原始户籍省
prefecture_orig文本某府原始籍贯府
county_orig文本某县原始籍贯县
code_region文本某行政区划清洗后标准化地区
geography_note文本原籍/占籍/寄籍籍贯类型备注

2.1 人物标识:别让同名同姓毁掉整张表

第一件要做的事是给每一条人物记录一个person_id。听起来像废话,但没它后面会非常痛。明清几万名进士,同名同姓是常态,特别是“张伟”“王强”这类名字。如果不加编号,后续做关联分析、去重、核对时根本分不清谁是谁。我的做法是:在录入时先分配到科次(年份+科目),再在科次内部按碑录顺序编号,组合成一个唯一ID。这样即使两人同名同姓,只要科次不同,ID也不同;如果同一科出现同名,就额外参考籍贯字段区分并调整ID。

有人可能会说:直接用“姓名+年份+籍贯”当主键不行吗?理论可以,但历史数据中一个姓名可能存在不同写法和异体字,年份也可能因为调整需要修正,组合主键一旦某字段改错,整行记录就容易被误判为另一人。用独立编号做主键,其它字段都只是“属性”,改起来没有后顾之忧。

2.2 籍贯层级:两级到四级的颗粒度怎么选

籍贯是题名碑录里最复杂的信息之一。很多初学者会把“籍贯”存成一个字符串,比如“某府某县人”,然后分析时用模糊匹配。这当然能用,但很不灵活。我选择拆成“省-府-县-里”四级,有哪级填哪级。为什么拆到县级?因为府一级面积太大,同一个府内部各地差异明显,拆到县才能做地图上的点;而“里”这个微观层级则因历史变动频繁,很多碑录只写到县,所以里仅作为备注列存在。

拆的过程中要留意“籍贯”类别的区分。明清时的进士籍贯有“原籍”(祖籍)、“户籍”(官方户口所在地)、“占籍”(落籍在某地)等说法。有时候碑录写的是“某人某县人”,但实际他在另一个县生活。为了不混淆,我会把这类说明单独放在geography_note字段里,不直接覆盖到县级字段上。否则统计数据时会凭空多出一批“假本县人”。

2.3 排名体系:一甲、二甲、三甲与总排名的换算逻辑

《明清进士题名碑录》里,排名有两种表达方式:一种是一甲、二甲、三甲的“甲第排名”,另一种是“总排名”。一甲只有三人,分别是状元、榜眼、探花;二甲若干人,按名次编号;三甲又若干人。总排名其实不是单纯把一甲第二名算作第2吗?差不多,但不同来源有细微差别。有些刻本会在总排名中把一甲第一名设为第1,然后二甲第一名紧接着算第4;也有版本把三大甲的起点单独计数,导致总排名出现跳跃。我在项目里统一采用“连续总排名”的规格:即状元=1,榜眼=2,探花=3,二甲第一名=4,依次往后排。这个规则至少保证在我自己的数据集内部是自洽的,做对比时不会因为版本差异而乱套。

基于这个规则,我写了个小脚本自动计算总排名。输入是甲第和甲内名次,输出是连续排名;反过来也可以由总排名反推甲第。需要提醒的是,不同点校本可能存在总排名相差一两位的情况,这种系统误差在个别记录里难以避免。我处理方式是在数据表里加一个ranking_source字段,标明该排名来自哪个版本,方便事后校验。

3. 数据清洗实录:繁体异体、缺字、避讳与通假

数据录入之前的字段设计是蓝图,真正动起手来,才发现蓝图再漂亮也挡不住泥泞的现实。碑录的文本又脏又乱,每个字都可能给我上一课。

3.1 异体字与缺字:一个“歴”和“历”的教训

第一关是异体字。明清刻书里的“历”可能写成“歴”或“曆”,甚至还会出现一个字有四种写法的情况。如果把这些直接存进表里,将来按姓名、地名检索就会漏掉很多结果。我建了一张异体字映射表,把常见异体字统一映射到现代规范字。比如“寜”统一成“宁”,“歴”“曆”统一成“历”。不要小看这一步,没有它,统计某姓氏时同姓异写会导致数量严重偏小。

缺字问题更麻烦。碑文残缺、刻本漫漶、Ocr识别为空,很多记录会漏字。我的策略是给缺失字符留特殊占位符“▢”,绝不直接删除该字段,同时在清洗过程中把“疑似缺字”的标记记录下来。比如某人名中缺第二个字,我会把 name 存成“赵▢”,并备注name_missing_flag=1。这样后续如果找到补充资料,就能直接定位。

我还发现一个细节:古人习惯用“某”代替不知道的名,比如“王某”。这类记录在清洗时不能当成普通姓名,需要标记为“姓氏可见、名缺失”,否则统计分析中这些人会被归入同名同姓的样本,干扰家族追踪。

3.2 避讳改字的识别与还原

清代避讳尤其严重。为了避皇帝的名讳,很多字会被改写或缺笔。比如某个字可能因避讳被换成另一个近义字。清洗时必须先建立一份避讳字对照表,把“避讳后写法”映射回“本字”。我看到的常见做法是:先收集该朝代避讳字表,再把文本中出现的对应字全部映射回未避讳的形式。不过要小心:不是所有看似避讳的字都是避讳,比如某些地名本身就有常用异体。所以我只在特定帝王统治年份范围内启用避讳映射,尽量精确。

这一步骤非常考验耐心。好在碑录中的姓名、籍贯文本大多较短,逐条人工复核一遍比想象中快。我当时的流程是:批量替换避讳字,然后随机抽样人工比对原文,抽样比例10%左右,确保替换没有造成误伤。

3.3 籍贯文本的清洗流水线:去括号、补省、并同义词

籍贯字段的原始文本五花八门,常见格式有“某府某县人”“某县某里人”“某卫某所人”(军籍)以及“某省某府某县”。我设计了一条清洗流水线:

  1. 去掉句尾“人”字,只留行政地名;
  2. 去除括号内的注释文字,比如“(后改某县)”这种,把注释放到备注字段;
  3. 补全省名:如果原文只有“某府某县”,需要根据历史政区表补上它所属的省;
  4. 统一简称与别称:比如某些地区有古称、别称,都要映射到标准名称;
  5. 拆分到省、府、县三级字段。

这条流水线里,最受影响的是“某卫某所”这类军事单位。明代有大量军籍进士,籍贯写的是卫所名而非普通州县。处理时,我会把卫所名映射到对应的实际地理位置,同时在geography_note里标注“军籍”。否则直接按普通州县字段统计,地理位置会偏移。

4. 地理维度:把“某县某里”变成可计算的空间坐标

数据表里有了“某省某府某县”还不够,想画图、想算空间集聚,就必须把文字籍贯变成经纬度。

4.1 历史地名与现代区划的映射思路

我最初走了弯路:直接用今天的行政区划去匹配清代的地名,结果很多地方张冠李戴。比如某县在清代属于另一个府,今天又归了邻市,如果机械地套用现代区划,历史分布就会失真。

后来我采用了一个折中方案:以清代《一统志》和《明史·地理志》为底本,先整理出一套“明清历史政区词典”,每条记录包含:当时的地名、所属上级政区、大致经纬度、对应今天的大致位置。这样在清洗时,先查历史政区词典,得到标准地名和坐标;再做一次“古今对照”,把历史坐标的经纬度归到现代地图底图上,同时保留历史政区名称。这个过程也是半自动的:先用词典批量匹配,匹配不上的再人工判断。

矩阵化的成果是:每个县对应一个经纬度点,并不要求精确到县城中心,只要保证同一县域内记录点的相对位置一致即可。毕竟进士籍贯只到县一级,不需要街道尺度,精度十来公里就够用。

4.2 用经纬度打点之后能玩出什么

有了经纬度,我做了几张分布图,效果很直观。把明清两个朝代的进士点放在同一张地图上,密度高低的区域一目了然;再按甲第高低给点着色,能看出顶尖进士的空间分布是否和一般进士一致。例如我发现某些区域虽然进士总量不多,但高名次比例偏高,说明这个问题不能用“人海战术”解释,可能存在特定的教育传统或选拔资源集中。

更深入一点,我把同一县在不同年代的录入点按时间串成序列,可以观察“科举家族”的迁移流动。比如某个家族从A县迁到B县后,后代都占籍B县,对应在图上就会出现一条“点从A移向B”的轨迹。这种空间叙事,靠纯文字文本几乎不可能构建,但结构化之后一拖就能看到。

当然,地理映射也有误差。历史政区边界并非固定,同一地名在不同时期可能指代不同区域;我用的点精度有限,只能做宏观格局,不能用来做村庄级别的战场复盘。

5. 用总排名能做的三种分析:分省登科率、家族链条与时间趋势

数据清洗完毕、地理坐标挂上之后,真正有意思的部分才开始。我选了三个方向来验证数据集的复用价值,每个方向都足以撑起一篇研究小文。

5.1 分省实力对比:不只是看人数,还要看排名分布

很多人统计“某省出了多少进士”,其实只看了总量。总量当然有参考价值,但忽略了一个关键因素:不同省份的乡试名额和应考人口基础不同,录取率差异很大。我更关心的是“优质进士”的比例。我的做法是:把总排名前N名(比如前100名)视为尖子生,然后计算各省在尖子生中的占比,以及各省进士总人数的“中位排名”。通过这个指标,我发现了几个有趣的现象:有的省总量排名靠前,但中位排名靠后,说明大量进士集中在三甲中后段;而有些小省,总量虽少,顶尖比例却很高。

这种分析对字段要求很直接:只要有province_orig和ranking_total就能算。需要注意,使用总排名时,要先剔除那些排名缺失或不连续的记录,否则中位排名会被虚值拉偏。

5.2 姓氏与家族追踪:从同姓连榜看“学阀”现象

第二个方向是用姓名反推家族链条。我把数据按“姓氏-省份-县”分组,再按年份排列,计算每组在连续三科内出现两位以上进士的次数。结果果然找到一批“连榜家族”:某个县里某个姓氏,每隔一两科就有人上榜,延续好几十年。这些家族的进士往往在高名次区间也表现得比周围人好。

这个分析有个陷阱:同姓不一定是同宗。尤其是某些大姓,一个县里可能有无数个互不相干的本家。所以严格意义上,这只能叫“同姓同地连续登科”,并不能直接证明“家族性”。要坐实家族关系,还得去翻族谱、齿录和同年齿录等资料。但数据表的价值在于能迅速圈定“疑似家族”的候选名单,给后续考据省掉大量检索时间。

5.3 时间趋势:年号更替与科场大小的变化

第三个方向是时间序列。我把每一科的进士数量统计出来,连成一条时间线。可以看出明清两代,开科情况的波动非常明显:战乱时期减科或停科,承平年代会加科或増额。再把每年总排名第1名的省份、每科的高名次区域也放到时间轴上,还能看出政治中心迁移对进士地理来源的影响。

这里要特别留意“年号”与“年份”的关系。同一个年号可能跨多年,比如某年号使用了十年,而科考不是年年有。如果只按年号聚合,会把十年间的所有科次混在一起,导致趋势失真。因此我强调:任何时间趋势分析都必须以公历年份为准,年号字段只作为辅助标签。

6. 踩过的坑和给新手的建议

这个项目前后花了几个月,中途翻车无数次。挑几个最典型的写出来,希望后来者别在同一条沟里翻船。

6.1 五大翻车现场及处理方案

第一个坑是Excel打开大文件直接卡死。几万行数据再加十几个字段,用Excel做过滤和排序还算勉强,但一跑公式就崩溃。我的解决方案是:数据录入和清洗阶段用Python的pandas,纯表格浏览和人工抽查再用Excel。要注意保存格式时都用UTF-8 CSV,避免Excel把历史字符弄成乱码。

第二个坑是重复录入。早期人工录入时,同一科次的人因为排版换页被录了两遍。后来我用科次+姓名+籍贯+排名四键联合查重,把所有重复行揪出来合并,才没有让总量虚高。建议你在录入阶段就养成“每批导入后立刻跑查重脚本”的习惯。

第三个坑是版本冲突。手头有两个版本的碑录,同一个人排名不一致,差一两位。因为版本A用了连续总排名,版本B用了跳跃总排名。我花了半天才搞明白,之后就在每个字段旁边加来源标记,并固定以某一个点校本为基准,其他版本只做校正参考。

第四个坑是地名“同名异地”。比如某县在两个府里都存在,仅按县名匹配会张冠李戴。我的对策是必须用“省-府-县”三级联合匹配,绝不单独用县名查坐标。

第五个坑是迷信Ocr文本。我最初偷懒,用Ocr识别印刷体碑录,错误率感人,尤其是“之”“廷”“之”等字形相似的字。最后的处理方法是:Ocr结果只能当辅助索引,所有关键字段(姓名、籍贯、名次)必须经过二次人工校验。这个建议虽然残忍,但真的能救命。下表汇总一下:

坑后果解决办法
Excel处理大数据崩溃、假死主力用pandas,Excel仅抽查
重复录入总量虚高多键联合查重
版本排名不一致统计误差固定基准+标注来源
同名异地坐标错位三级地名联合匹配
Ocr文本误识别字段错乱关键字段人工复核

6.2 后续扩展:加入官员履历、师承与地缘网络

如果你的目标不止是一张静态名录,还可以把这张表做为基础,继续挂接其它数据。比如接上官员的仕途履历,就能分析“名校/高排名”和“仕途高度”之间的关联;接上年齿录中的师承关系,就能构建同年、座师的门生网络,用网络分析工具研究派系形成。这些扩展都依赖同一套稳定的person_id,所以一开始就强调主键设计,绝对不亏。

就我自己而言,做完这张表后,原来很多朦胧的直觉变成了可验证的结论,也让我对明清科举地理有了更细的理解。如果你也想做类似的数据化尝试,我给的建议只有一句:挑一个小的区域、一个短的时段,先跑通清洗和可视化全流程,再考虑放眼全国。哪怕只做某一个府,那份把碑文变成数据的成就感,也足以让你上瘾。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询