如果要把一批与经济相关的出土文献整理成可检索、可统计的数据库,最麻烦的往往不是历史考证本身,而是资料格式混乱、释文版本不一致、关键词难以统一。手工维护这样的资料库,效率很低,也容易漏查漏统计。本文围绕甲骨文等出土文献中的经济类记录,分享一套基于 Python 的文本清洗、关键词提取和本地查询工具,适合历史数字化项目、资料整理脚本和入门 NLP 练习。
需要说明的是,本文不打算对“古代经济治理”下定论,更不会去争议某一种历史叙事是否正确。文章的核心是把分散的释文材料,用工程化方式组织成干净、可检索、可统计的数据集。只要资料本身来自公开可靠的渠道,这套工具就可以复用到碑刻、简牍、书信档案等各类文献数字化场景中。
1. 背景与核心概念
1.1 什么是出土文献文本挖掘
文本挖掘并不是新的概念,它指的是从非结构化文本中提取结构化信息的过程。对于古代文献来说,原始材料往往以拓片、照片、扫描图或学者手录释文的形式存在,计算机不能直接理解其中的句读和语义。文本挖掘的第一步,是把这些材料转成可编辑的文字文本,再做清洗、分词、统计和索引。
经济类记录在出土文献中非常常见,例如田猎、农作物收成、贡纳物品、牲畜数量、手工器物等。研究者在整理这类材料时,最关心的通常不是单个字词的长篇考证,而是三个问题:哪些记录涉及经济主题?这些记录在哪些时间段或地点出现?不同关键词的频次分布如何?这正好是结构化文本检索能够解决的问题。
1.2 为什么选择甲骨文作为示例
甲骨文是商代晚期以占卜记录为主的出土文字,主要发现于河南安阳殷墟一带。其内容常涉及农业、田猎、贡纳、战争、祭祀等,对研究当时的经济活动有重要参考价值。由于甲骨文释文存在大量异体字、假借字和残辞,数字化整理比普通古籍更难,也更需要规范的数据结构。
不过要特别注意,甲骨文释文具有很强专业性。同一片甲骨在不同著录书中的隶定写法可能不同,补字、缺字、通假字的处理方式也不一样。本文的工具逻辑建立在已经由专业学者整理好的释文或现代译文之上,不负责自动识别拓片,也不参与古文字释义层面的判断。
1.3 技术方案的边界
本文给出的方案属于“资料整理后半段”:假设你已经拿到了可编辑的文本数据,需要做清洗、统计和查询。至于拓片如何识别、文字如何隶定、真伪如何判断,不属于这套脚本的处理范围。
在工程上,这个项目会用到 pandas 处理表格数据,使用规则清洗文本,再用关键词词典统计经济类词汇,最后通过 Flask 提供一个简单的本地查询接口。整个项目可以在普通笔记本上运行,不依赖服务器和大数据平台。
2. 环境准备与版本说明
2.1 运行环境
本文示例以 Python 3.9 及以上版本为准,操作系统可以是 Windows、macOS 或 Linux。下面的命令在 Windows 命令提示符和 Linux/macOS 终端中都可以使用,只是个别中文字体路径需要根据系统调整。
建议先创建独立的虚拟环境,避免依赖冲突。在项目根目录执行:
python -m venv venvWindows 系统激活虚拟环境:
venv\Scripts\activateLinux 或 macOS 系统激活虚拟环境:
source venv/bin/activate2.2 依赖清单
项目需要以下第三方库:
| 库名 | 用途 |
|---|---|
| pandas | 读取和处理 CSV 数据 |
| flask | 提供本地查询接口 |
| wordcloud | 生成关键词词云 |
| matplotlib | 渲染并保存词云图片 |
| jieba | 对现代译文做分词演示 |
创建一个requirements.txt文件,内容如下:
pandas flask wordcloud matplotlib jieba安装依赖:
pip install -r requirements.txt版本需要根据你的实际环境调整,本文重点演示完整实现思路。
2.3 项目目录结构
建议按下面的目录组织工程,原始数据、处理脚本、词典、输出结果分开存放:
economic-doc-mining/ ├── data/ │ ├── jiaguwen_records.csv │ └── jiaguwen_records_clean.csv ├── config/ │ ├── econ_terms.txt │ └── stopwords.txt ├── output/ │ ├── keyword_stats.csv │ └── keyword_cloud.png ├── scripts/ │ ├── clean_text.py │ ├── extract_keywords.py │ ├── tokenize_text.py │ └── app.py ├── requirements.txt └── README.md这种结构的好处是数据、代码、配置相互隔离。后续如果新增字典或扩充数据,不需要改动脚本主体。
3. 核心语法与原理拆解
3.1 数据表设计
要把零散的文献资料变成可统计的结构化数据,第一步是设计表格字段。对于出土文献,建议至少包含以下列:
| 字段名 | 说明 |
|---|---|
| id | 记录编号,保持唯一 |
| source | 来源著录或数据库名称 |
| category | 主题分类,如 agriculture、hunting、tribute |
| period | 时期或分期信息 |
| original_text | 原始释文或原文 |
| translated_text | 现代汉语译文或整理说明 |
| notes | 备注、补字说明、审核状态 |
推荐统一使用 CSV 格式存储,因为 CSV 轻量、跨平台、容易用 pandas 读取。不过要注意,原始释文里可能包含特殊符号,例如□表示缺字,[]表示补字。这些符号在清洗阶段需要特别处理,不能简单删除,否则会丢失释文信息。
3.2 文本清洗的作用
原始数据通常存在换行、多余空格、全角半角混用、特殊符号等问题。如果不做清洗,关键词匹配时很容易出现“田”能查到,但加了全角空格后“田”查不到的情况。清洗的目标是统一格式,但不要破坏文献原意。
对于经济类统计,推荐清洗策略如下:
- 将换行符替换为空格,避免跨行文本被硬拆开。
- 多个连续空格压缩为一个空格。
- 删除不需要的 URL、英文字符以外的干扰符号。
- 保留汉字、数字、常用中文标点。
- 对
□等缺字符号,可以保留或按需替换为占位符,不能直接丢弃。
3.3 关键词词典设计
关键词统计有两种常见做法:一种是用 jieba 通用分词后统计高频词,另一种是使用自定义经济类术语词典直接匹配。前者的优势是发现未知词,后者的优势是结果更可控、解释成本更低。
本文推荐“词典匹配为主、分词为辅”的组合策略。先在config/econ_terms.txt中维护一份经济类词表,词表需要由熟悉文献的整理者审核。示例词表如下:
田 禾 麦 黍 年 刈 获 猎 渔 贡 献 牛 羊 马 贝 丝 帛 车 舟 仓 廪 谷 粟 酒 鬯注意这只是一个演示词表,不代表甲骨文中这些字全部具有经济含义。实际项目中,词表应当经过专题文献综述和专业人员确认。
3.4 为什么不能直接把甲骨文原文交给分词器
jieba 是基于现代汉语语料训练的分词工具,对碑刻、简帛、甲骨文的切分效果并不稳定。甲骨文原文没有现代句读,频繁出现一字多义、通假字、缺字,直接分词会产生大量无意义片段。
更合理的做法是:把原始释文与整理后的现代译文分开存放,统计操作在translated_text或标注后的文本上执行。这样既能保留原文资料,又能让 NLP 工具有一个相对规范的句子边界。
下面是一个简单的分词演示脚本,用于观察现代译文的分词效果。注意它不对原始释文生效。
# 文件路径:scripts/tokenize_text.py import jieba import pandas as pd df = pd.read_csv("./data/jiaguwen_records_clean.csv", encoding="utf-8") for text in df["clean_text"].head(5): words = [w for w in jieba.lcut(text) if w.strip()] print(words)运行后会输出类似下面的分词结果:
['商王', '卜问', '今年', '农作物', '是否', '有', '好收成']这里只是一个通俗示例。如果你要做更严谨的学术文本分析,建议配合专家整理的专名表一起使用。
4. 完整实战案例
4.1 准备演示数据
为了演示流程,先准备一份模拟数据。以下内容仅用于说明数据分析过程,不是真实甲骨文释文,也不代表任何学术判断。请勿把它用于正式研究引用。
文件路径:data/jiaguwen_records.csv
id,source,category,period,original_text,translated_text,notes a001,演示资料,agriculture,商代晚期,(示例原文),商王卜问今年农作物是否有好收成,示例数据 a002,演示资料,hunting,商代晚期,(示例原文),田猎并记录捕获猎物的种类和数量,示例数据 a003,演示资料,tribute,商代晚期,(示例原文),记录某地进贡的牲畜和物资,示例数据 a004,演示资料,agriculture,商代晚期,(示例原文),卜问禾苗生长是否会遇到灾害,示例数据 a005,演示资料,trade,商代晚期,(示例原文),记录交换贝和丝的记录,示例数据 a006,演示资料,fishing,商代晚期,(示例原文),记录渔获数量较多,示例数据 a007,演示资料,animal_husbandry,商代晚期,(示例原文),登记牛栏中的牛数量,示例数据 a008,演示资料,storage,商代晚期,(示例原文),检查仓廪中粮食储存情况,示例数据 a009,演示资料,agriculture,商代晚期,(示例原文),麦子成熟后准备收割,示例数据 a010,演示资料,hunting,商代晚期,(示例原文),猎获鹿若干只并记录用途,示例数据original_text一栏保留原文信息,这里示例用“(示例原文)”代替。translated_text是整理后的对照文本,是后续统计的主要对象。
4.2 编写文本清洗脚本
清洗脚本负责读取原始 CSV,清洗译文并生成新文件。
文件路径:scripts/clean_text.py
# -*- coding: utf-8 -*- import re import pandas as pd def clean_text(text): """清洗单个文本字段。""" text = str(text) if text is not None else "" text = text.replace("\n", " ") text = re.sub(r"\s+", " ", text) # 保留中文字符、数字、常用标点,去掉其余噪音字符。 text = re.sub(r"[^\u4e00-\u9fff0-9A-Za-z,。:;、()《》]", "", text) return text.strip() def main(): df = pd.read_csv("./data/jiaguwen_records.csv") if "clean_text" not in df.columns: df["clean_text"] = "" df["clean_text"] = df["translated_text"].fillna("").map(clean_text) df.to_csv("./data/jiaguwen_records_clean.csv", index=False, encoding="utf-8-sig") print(f"清洗完成,共 {len(df)} 条记录") if __name__ == "__main__": main()这里的关键是encoding="utf-8-sig"。它会在文件开头加入 BOM,让 Windows 下的 Excel 直接打开 CSV 时不出现中文乱码。如果只在 Linux 终端中使用,改成utf-8也可以,但为了兼容办公场景,建议保留utf-8-sig。
清洗后的数据仍然保留original_text,不会破坏原始释文信息。clean_text只作为检索和统计字段存在。
4.3 编写关键词统计脚本
关键词统计脚本会读取config/econ_terms.txt词典,在清洗后的文本中匹配词条,并输出频率表。
文件路径:scripts/extract_keywords.py
# -*- coding: utf-8 -*- import re from collections import Counter import pandas as pd def load_terms(path): """加载关键词词典。""" with open(path, encoding="utf-8") as f: terms = [line.strip() for line in f if line.strip()] return terms def count_terms_in_text(text, terms): """统计单个文本中的关键词次数。""" counter = Counter() for term in terms: # 这里使用正则转义,避免特殊字符影响匹配。 count = len(re.findall(re.escape(term), text)) if count: counter[term] += count return counter def main(): df = pd.read_csv("./data/jiaguwen_records_clean.csv", encoding="utf-8") terms = load_terms("./config/econ_terms.txt") total_counter = Counter() for text in df["clean_text"].fillna(""): total_counter += count_terms_in_text(text, terms) print("关键词频次 TOP 20:") for word, count in total_counter.most_common(20): print(f"{word}\t{count}") result = pd.DataFrame(total_counter.most_common(), columns=["term", "count"]) result.to_csv("./output/keyword_stats.csv", index=False, encoding="utf-8-sig") print("结果已保存到 output/keyword_stats.csv") if __name__ == "__main__": main()这段代码的统计逻辑很简单:遍历词典中的词,统计它在每一条翻译文本里出现的次数。因为词典中的词大多是单字或短词,所以准确率比通用分词高。缺点是如果词表存在包含关系,例如同时包含“田”和“田猎”,统计数据会重复计算。实际使用时可以把统计结果看成“粗粒度命中情况”,而不是严格的语言学词频。
4.4 编写本地查询接口
为了方便检索某条记录是否包含指定关键词,可以再加一个 Flask 查询接口。
文件路径:scripts/app.py
# -*- coding: utf-8 -*- import pandas as pd from flask import Flask, jsonify, request app = Flask(__name__) df = pd.read_csv("./data/jiaguwen_records_clean.csv", encoding="utf-8") @app.route("/api/search", methods=["GET"]) def search(): q = request.args.get("q", "") if not q: return jsonify({"message": "请输入查询词", "data": []}) result = df[df["clean_text"].str.contains(q, na=False, regex=False)] data = result.head(20).to_dict(orient="records") return jsonify({"total": len(result), "data": data}) @app.route("/api/stats", methods=["GET"]) def stats(): category = request.args.get("category", "") if category: sub = df[df["category"] == category] else: sub = df return jsonify({"category": category, "count": len(sub), "records": sub.head(10).to_dict(orient="records")}) if __name__ == "__main__": # 本机调试时可以开启 debug,生产环境务必关闭。 app.run(debug=True, port=5000)启动接口:
python scripts/app.py浏览器访问:
http://127.0.0.1:5000/api/search?q=农作物返回结果中会包含所有clean_text含“农作物”的记录。
再访问:
http://127.0.0.1:5000/api/stats?category=agriculture可以看到 agriculture 类别的记录数量和前 10 条明细。
这里要提醒一句:Flask 自带的服务适合本机测试和内网小范围使用,不适合直接暴露到公网。如果要把查询接口开放给更多人,应加认证、限流和访问日志,并使用 Waitress 或 Gunicorn 等生产级 WSGI 服务器。
4.5 生成词云
为了快速查看核心关键词分布,可以生成一张词云图。
文件路径:scripts/gen_wordcloud.py
# -*- coding: utf-8 -*- import pandas as pd from wordcloud import WordCloud import matplotlib.pyplot as plt def main(): df = pd.read_csv("./data/jiaguwen_records_clean.csv", encoding="utf-8") text = " ".join(df["clean_text"].fillna("")) # 中文字体路径需要根据操作系统调整。 # Windows 常见:C:/Windows/Fonts/simhei.ttf # macOS 常见:/System/Library/Fonts/PingFang.ttc # Linux 常见:/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc wc = WordCloud( font_path="C:/Windows/Fonts/simhei.ttf", width=800, height=600, background_color="white", ).generate(text) plt.figure(figsize=(10, 6)) plt.imshow(wc, interpolation="bilinear") plt.axis("off") plt.tight_layout() plt.savefig("./output/keyword_cloud.png", dpi=200) print("词云已保存到 output/keyword_cloud.png") if __name__ == "__main__": main()运行:
python scripts/gen_wordcloud.py如果词云图里的中文显示成方框,说明font_path指定的字体文件不存在或该字体不支持中文。换用系统里实际存在的中文字体即可。
4.6 运行完整流程
建议每次新增数据后按顺序执行:
python scripts/clean_text.py python scripts/extract_keywords.py python scripts/gen_wordcloud.py如果还要查询接口,再执行:
python scripts/app.py清洗脚本生成干净数据,统计脚本生成频次表,词云脚本生成可视化结果,查询接口支持按关键词和类别检索。整个流程可以重复执行,数据更新后不需要改代码。
5. 常见问题与排查思路
5.1 高频问题排查表
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| Excel 打开 CSV 中文乱码 | 编码用了 utf-8 而不是 utf-8-sig | 写入 CSV 时使用 encoding="utf-8-sig" |
| 关键词统计结果为空 | 词典路径错误或文本全被清洗删除 | 检查 config/econ_terms.txt 是否存在,打印清洗后文本 |
| 词云中文显示方框 | 系统没有指定中文字体 | 修改 WordCloud 的 font_path 为可用的中文字体 |
| 查询接口启动失败 | 端口被占用 | 修改 port=5000 为其他端口,或关闭占用进程 |
| 查询不到某个词 | clean_text 为空 | 检查 CSV 中 translated_text 字段是否有值 |
| 统计词频偏多 | 词典存在互相包含的词条 | 人工审核词表,必要时只保留最大粒度词条 |
5.2 清洗后文本为空的处理
如果清洗脚本运行后,发现clean_text字段全是空值或异常短,优先检查原始 CSV 的列名。CSV 表头不能有多余空格,例如translated_text和translated_text会被 pandas 视为不同字段。
另外,如果中文文本被错误识别为NaN,在读取后要执行fillna(""),否则后续str.contains和字符串处理都可能报错或跳过记录。
5.3 词表覆盖不足的问题
当词典中缺少某个经济类词时,统计结果会偏低。不能简单通过增加通用词来提高覆盖率,因为通用词可能存在大量非经济含义。比较好的做法是分两层:先统计高频词,再由研究者从高频词里筛出经济相关词,补充进正式词表。
这种“先探索、后固化”的做法,比一次性拍脑袋列词表更可靠,也更符合实际研究的迭代节奏。
6. 最佳实践与工程建议
6.1 数据来源必须可追溯
出土文献数字化最忌讳的是来源不清。每一条记录都应该带有source字段,指向公开著录、数据库编号或整理者名称。项目里如果使用扫描件,建议单独维护一份文件清单,记录文件来源、授权状态和处理人。
来源不明或未公开授权的资料,不应上传到公共服务器,也不应在博客中直接公布完整扫描图。即使在本地研究中使用,也要遵守相关机构和数据库的使用协议。
6.2 使用多版本释文管理
同一片甲骨可能存在多种释文。有的差异很小,有的可能影响统计结果。建议增加version字段,例如version=《合集》释文或version=某学者整理本。统计分析时,默认只针对某一个版本,不要混合多个版本,否则同一个字会被重复统计。
如果需要做版本比对,可以把多个版本放在不同的 CSV 文件或分目录中,再写专门的 diff 脚本,而不是把所有版本塞进同一张统计表。
6.3 区分原文和整理译文
原始释文和现代译文是两种不同性质的数据。原始释文是历史材料,现代译文是整理者提供的解释。这两者不能混在一个字段里做关键词匹配。统计现代译文出现的“田”不代表原文中一定写的是“田”。
因此,本文在数据表中单独保留了original_text和translated_text。查询和词频统计默认以translated_text为主,如果要检索原始释文,应明确区分口径,例如搜索字段改为original_text。
6.4 安全边界与最小权限
这个项目本质上是一个文本检索工具,不涉及任何敏感系统操作。不过只要上线了 Web 接口,就要考虑权限问题。建议做法是:Flask 只监听127.0.0.1,不要监听0.0.0.0;如果要开放访问,应增加登录认证、操作审计和请求限流。
不要把数据库连接字符串、API 密钥或未公开文献扫描件放在 CSV 中一起提交。项目中的配置文件、词典、数据文件最好分开管理,必要时把敏感资料放入.gitignore。
6.5 结果解释要克制
文本挖掘工具输出的只是统计数字,不是历史结论。某个词频次高,可能只是因为保存下来的材料数量多,不代表那件事在当时的经济活动中更重要。任何数字在进入结论之前,都需要结合文献背景、考古信息和专业人士判断。
所以这套工具的定位是“辅助检索与整理”,而不是“自动得出历史规律”。它能让研究者快速定位材料,但最终解释权仍然属于研究者本人。
7. 总结与下一步学习方向
本文从资料整理的角度,完成了一个经济类出土文献检索小工具。通过 pandas 清洗表格数据,通过自定义词典统计关键词频次,通过 Flask 提供本地查询接口,再通过词云快速展示文本分布。整套代码结构简单,可以复用到简牍、碑刻、契约文书等多个文献数字化场景。
如果继续扩展这个项目,可以先从三个方向入手:一是接入 OCR 识别,让扫描拓片自动转成文本,不过这需要专门的古文字识别模型;二是把 CSV 换成 SQLite 或 MySQL,让千万级记录也能高效检索;三是引入向量检索做语义查询,例如输入“农业收成”也能匹配到“禾麦黍稷”相关的记录。
实际项目中更优先的是数据质量。先把来源字段、版本字段、审核状态补齐,再优化算法也不迟。好的词典和干净的原始数据,远比花哨的分析模型更有价值。
如果你在配置过程中遇到环境问题,可以先用文章开头的虚拟环境隔离依赖;遇到统计结果异常,先打印清洗后的文本排查,再检查词典。把每一步跑通之后,再迁移到自己的文献数据上。