用PyMuPDF解析《中华字经》PDF:提取、清洗与结构化入库实战
2026/9/21 19:02:48 网站建设 项目流程

简介:《中华字经》是一部以四字韵文形式编排的国学识字篇目,涵盖自然地理、历史文化、社会生活、现代科技等多元主题,适合幼小衔接识字、语文拓展及传统文化爱好者研读。这份PDF将全文与注释汇编于一体,逐段标注字词释义与语义背景,便于读者在诵读中理解字义、积累词汇,也可供教师直接用于课堂讲析或学生课后自学。资源包为1个PDF文件,大小1.05MB,轻巧便携,支持在手机、平板及电脑端随时翻阅,按三大板块系统呈现。已有370人学习下载,可帮助学习者通过韵文节奏快速识记常用汉字,同时感受汉语音韵之美与传统文化内涵。

1. 为什么值得为一本汉字启蒙书的 PDF 写一套解析脚本

上个月接到一个“给小学语文课件做字频统计”的小需求,翻遍了开源词库都不顺手——要么是通用词频表,要么是新闻语料,和小学教材的用字场景差了一截。而《中华字经》全文及注释版[汇编].pdf 正好是一份难得的“结构化汉字语料”:全文按天文地理、人伦器物、鸟兽虫鱼分类,四字一句、押韵易背,覆盖常用字面广,注释版还把“字 → 释义 → 归类”打包在一起了。如果你只拿它当普通 PDF 读,复制粘贴出来的文本会夹杂页码、断行错位、注释混排,几乎没法直接用。把“提取 → 清洗 → 入库 → 校验”这套脚本跑通后,就能得到一份可复用的 JSON/SQLite 语料库,后续做汉字教学、OCR 测试集和前端检索都能接着用。这篇文章不搬运 PDF 内容,而是按一线开发的做法,把这套解析链路里的工具选型、参数设置和容易踩的坑讲清楚。

2. 用 PyMuPDF 把《中华字经》PDF 抽成干净的纯文本

2.1 先比较三套工具:pdftotext、pdfplumber、PyMuPDF

《中华字经》注释版 PDF 属于典型的“双栏排版 + 页眉页脚 + 正文与注释混排”,转文本时最怕两栏文字被揉在一起、注释行抢在正文前面。常见的三种工具各有侧重,我在一个双栏样例页上实测过它们的差异:

工具中文准确率分栏处理脚本友好度维护状态
pdftotext中上,依赖字体映射用 -layout 看运气命令行为主,适合快速看Poppler 持续维护
pdfplumber中上拿 words 坐标自己拼代码多,速度慢更新频率一般
PyMuPDF高,对 CJK 优化好clip + sort 可控API 简洁,快活跃维护

之所以选 PyMuPDF,是因为它的get_text()支持按坐标区域裁剪,也支持sort=True按阅读顺序排序,且内部对中文字体映射做了大量兼容。pdftotext 的-layout在单栏页面很好用,但遇到左右双栏时经常把左栏尾部的一两个字捡到右栏开头;pdfplumber 虽然能拿到每个 word 的坐标,但要自己拼行、拼列,处理注释段落时工作量陡增。所以我一般会先装 PyMuPDF:pip install pymupdf,然后用一个最小脚本把全文倒出来,先看结构再定清洗规则。

2.2 最小提取脚本:按坐标排序提取正文

import fitz # PyMuPDF doc = fitz.open("zhonghuazijing_all.pdf") out_lines = [] for pno in range(doc.page_count): page = doc[pno] # 裁掉上下页眉页脚,避免目录页码和注释标题混入正文 top = 80 bottom = page.rect.height - 60 rect = fitz.Rect(0, top, page.rect.width, bottom) txt = page.get_text("text", clip=rect, sort=True, flags=fitz.TEXT_PRESERVE_WHITESPACE) out_lines.append(f"<!-- page {pno+1} -->") out_lines.append(txt) open("raw_zijing.txt", "w", encoding="utf-8").write("\n".join(out_lines))

fitz.open()打开 PDF;page.rect返回页面尺寸,单位是点,与分辨率无关。clip=rect限制提取范围,把页眉页脚挡在区域外。sort=True让提取结果按阅读顺序输出,对单栏排版足够。flags=fitz.TEXT_PRESERVE_WHITESPACE保留原始空白,这样换行结构不会因为空行被吃掉而粘连。每页插入一个<!-- page N -->注释行,后面清洗时能定位某一行的来源。

参数调整上,top=80bottom=page.rect.height - 60是经验值。如果页眉里还有“第 X 篇”这种标题,就把 top 再加大;如果正文底部被裁掉,就把 60 改成 40,再跑一遍比对。先输出head -n 50 raw_zijing.txt看有没有缺字,再批量处理。

2.3 左右双栏的注释版怎么切

《中华字经》注释版如果排成左右两栏,sort=True并不总能理解“先左后右”,它可能按文本块的内部坐标先输出右栏上半部分。这时按中缝把页面切两半,分别提取再拼接:

page = doc[pno] W = page.rect.width H = page.rect.height margin_x = 40 # 左右留白 gap = 20 # 中缝宽度 top, bottom = 80, H - 60 left_rect = fitz.Rect(margin_x, top, W / 2 - gap / 2, bottom) right_rect = fitz.Rect(W / 2 + gap / 2, top, W - margin_x, bottom) left_txt = page.get_text("text", clip=left_rect, sort=True, flags=fitz.TEXT_PRESERVE_WHITESPACE) right_txt = page.get_text("text", clip=right_rect, sort=True, flags=fitz.TEXT_PRESERVE_WHITESPACE) if left_txt.strip() and right_txt.strip(): out_lines.append(f"<!-- page {pno+1} two-col -->") out_lines.append(left_txt) out_lines.append(right_txt) else: out_lines.append(f"<!-- page {pno+1} full -->") out_lines.append(left_txt + right_txt)

这里的gapmargin_x是关键。gap太小,中缝附近的一两个字会串到另一栏;gap太大,又会把栏内最后一个字吞掉。我的做法是先导出一页 PDF 图片,量出正文左右边界和中缝坐标,再回头改这两个常量。页码那一行通常落在bottom之外,切栏时已经被挡掉了。

提示:清洗阶段能解决的是文本位置错位和页眉页脚混入。如果 PDF 字体把字形映射到 Unicode 私有区,提取出来是方块或乱码,后面洗不掉,那一页要单独走 OCR 或换字体解析,具体兜底方案在第 5 章。

2.4 抽取后先看结构,再决定清洗规则

raw_zijing.txt生成后,先不要急着写清洗脚本。用head -n 50或者编辑器打开,观察这几类行:

<!-- page 1 --> 第X篇 天文篇 日月星辰 循轨有常 ——日月星辰是指太阳、月亮和星星,循轨有常是说…… 雨露霜雪 四时成章 <097> 第 12 页

篇名通常带“篇”或“X篇”;正文是四个汉字加空格再四个汉字;注释以“——”开头或跟在句后;页脚页码很好认。把这几类特征的样例记下来,清洗规则才有依据。如果看到类似\ue000的字符,说明是私有区乱码,在 3.3 和 5.2 会用字频白名单处理。

3. 清洗与校验:把韵文句子变成一行一条的规范语料

3.1 先确定目标结构:篇名、正文句、注释行

《中华字经》正文是四字一句,注释版会在每句后面或页脚给出释义。清洗目标不是把 PDF 里所有文字都保留,而是只把“四字正文句”按顺序摘出来,同时记录它属于哪一篇。这样后续做字频统计、按篇检索才有意义。

抽取后的文本往往是这种混合结构:

第X篇 天文篇 日月星辰 循轨有常 ——日月星辰是指太阳、月亮和星星,循轨有常是说…… 雨露霜雪 四时成章

正文句之间的换行是可靠的,因为排版就是一行一句。注释行以“——”开头,页脚行含“第 X 页”。所以清洗的核心思路是:先丢掉整行噪音,再校验剩下的行是否符合“四字正文”的形态。常见做法是先把所有行拆开,逐行分类,而不是上来就用大段正则替换全文。

3.2 用两组正则做去噪与断行合并

import re def clean_raw(raw: str) -> list[str]: # 去掉上一阶段插入的页面标记 raw = re.sub(r"<!-- page \d+ -->", "", raw) # 去掉页码行和页脚版权行 raw = re.sub(r"^.*第\s*\d+\s*页.*$", "", raw, flags=re.M) # 去掉以 —— 或 — 开头的注释行 raw = re.sub(r"^[——\-—].*$", "", raw, flags=re.M) lines = [ln.strip() for ln in raw.splitlines() if ln.strip()] result = [] for ln in lines: # 正文是纯四个汉字,用 fullmatch 精确过滤 if re.fullmatch(r"[\u4e00-\u9fff]{4}", ln): result.append(ln) return result

re.sub(r"^.*第\s*\d+\s*页.*$", "", raw, flags=re.M)re.M^$按行匹配,把页码整行删掉。re.sub(r"^[——\-—].*$", "", raw, flags=re.M)处理注释行时,方括号里的——是三个字符,外部加^.*$,能把整行注释吞掉。最后re.fullmatch(r"[\u4e00-\u9fff]{4}", ln)只保留四位汉字整行,不是四字的内容直接丢。

这里有个边界要注意:如果 PDF 里正文句之间用全角空格分隔,比如“日月星辰 循轨有常”,那整行不是 4 个汉字,fullmatch 会失败。遇到这种情况,先把行内空白去掉再做匹配:

ln_clean = re.sub(r"[\s\u3000]+", "", ln) if re.fullmatch(r"[\u4e00-\u9fff]{4}", ln_clean): result.append(ln_clean)

\u3000是全角空格,\s覆盖半角空格和换行,两个都去掉再匹配,正文行就不会漏掉。篇名“第X篇 天文篇”会被这条规则丢弃,如果后面需要按篇分组,就在 3.4 之前先用^第.*篇把篇名单独抓出来,不要和正文混在一起处理。

3.3 异体字与繁体字统一

注释版 PDF 可能出现“朙”“脩”“臺”这类古字形或繁体写法。如果目标语料是简体常用字,这些不统一会影响字频统计和检索命中。我会维护一个很小的映射表,在清洗后做一遍替换:

variant_map = { "朙": "明", "脩": "修", "臺": "台", "佈": "布", } def unify_variants(s: str) -> str: for k, v in variant_map.items(): s = s.replace(k, v) return s

映射表怎么确定?先跑一遍collections.Counter统计所有出现的字符,把不在常用字表里的字挑出来,逐个看。几十个字符的规模,人工映射比写自动繁简转换更可控。需要注意,别把“台”一概替代成“臺”,这个映射只针对原文里出现的异体字,范围越小越安全。如果搜出来的异常字符落入 Unicode 私有区,replace 之前还要先解决编码问题,见 5.2。

3.4 校验:按句长与字频双检查

清洗脚本跑完,不能只看输出漂不漂亮,要用两个硬指标验证:句长和字频。

from collections import Counter def validate(lines: list[str]) -> None: bad_len = [(i, ln) for i, ln in enumerate(lines) if len(ln) != 4] counter = Counter("".join(lines)) print("总句数:", len(lines)) print("总字数:", sum(counter.values())) print("不重复字数:", len(counter)) print("非四字句示例:", bad_len[:10]) validate(clean_lines)

句长校验抓的是漏网的注释行或错切的中缝。字频校验用来判断覆盖度:《中华字经》通说全文四千余字,处理完若“不重复字数”只有一千多,多半是清洗时把大量句子误删了;若出现大量生僻字,则可能是字体映射问题。除此之外,还可以用一个高频字集合做交集比对,把“的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞” 做差集,缺失多的就回头查清洗规则。

检验结果里如果出现非四字句,看两种典型情况:

异常类型可能原因处理方式
行为三个字篇名被误判进正文提前用^第.*篇抓篇名并移除
行为五个字以上注释行没删净检查注释开头是否为全角破折号,补正则
字符是方块或乱码字体映射到私有区走 5.2 的私有区兜底
总字数远低于预期中缝裁掉了字符把 2.3 的 gap 调小,重新抽取

4. 结构化入库与检索:给《中华字经》加索引并秒查某个字

4.1 JSON 还是 SQLite:数据量决定实现方式

《中华字经》正文约四千字,四字一句约一千句。这个量级用 JSON 文件几 KB 就能放下,前端直接 fetch 也很快。但注释版带释义、篇目、拼音后,字段变多,检索需求也会变多,比如“查一个字出现在哪些句子里”“按篇目导出字频”,这时 SQLite 更顺手。我的选择标准很简单:只要有两类以上的检索维度,就用 SQLite;只有纯展示,JSON 够了。

先设计最常用的字段:

字段类型说明
idINTEGER PRIMARY KEY自增主键
chapterTEXT篇名
seqINTEGER在篇内序号
sentenceTEXT四字正文
full_textTEXT该句四字,便于 LIKE 检索

full_textsentence在数据上是重复的,但建索引时直接对full_text建索引比sentence更明确,后边查询能少写一层转换。

4.2 把清洗后的语料转成 JSON 并落库

清洗结果是一行一句的clean_lines,写库前先把篇目信息补上。如果之前没有抓篇名,可以用一个简单的规则:遇到“第X篇”标记时切换当前章节,后续句子都归属这个章节。

import sqlite3 def init_db(db_path: str) -> sqlite3.Connection: conn = sqlite3.connect(db_path) conn.execute("DROP TABLE IF EXISTS zijing") conn.execute(""" CREATE TABLE zijing ( id INTEGER PRIMARY KEY, chapter TEXT, seq INTEGER, sentence TEXT ) """) conn.commit() return conn def insert_items(conn: sqlite3.Connection, items: list[tuple[str, int, str]]): conn.executemany( "INSERT INTO zijing (chapter, seq, sentence) VALUES (?, ?, ?)", items, ) conn.commit()

executemany批量插入,第二个参数是一个元组列表,每个元组对应(chapter, seq, sentence)seq用该句在当前篇内的序号,从 1 开始。chapter为空字符串时,说明篇名规则没有匹配到,此时需要回头在 3.4 之前抓取篇名,而不是让数据带空章节入库。

落完库后顺手导出一份 JSON,方便前端直接用:

import json with open("zijing.json", "w", encoding="utf-8") as f: rows = conn.execute("SELECT chapter, seq, sentence FROM zijing ORDER BY id").fetchall() data = [{"chapter": r[0], "seq": r[1], "sentence": r[2]} for r in rows] json.dump(data, f, ensure_ascii=False, indent=2)

ensure_ascii=False让 JSON 保持中文明文,检查 diff 时一眼能看出内容问题。

4.3 检索示例:查一个字返回它所在的句子、篇目和序号

数据量只有一千句时,LIKE 检索足够快,不需要上 FTS5。下面这个命令行脚本可以查一个字:

import sqlite3 import sys def search(word: str): conn = sqlite3.connect("zijing.db") rows = conn.execute( "SELECT chapter, seq, sentence FROM zijing WHERE sentence LIKE ? ORDER BY chapter, seq", (f"%{word}%",), ).fetchall() for chapter, seq, sentence in rows: print(f"[{chapter or '未分篇'}·{seq:>3}] {sentence}") if __name__ == "__main__": search(sys.argv[1])

查询语句里使用参数占位符?,传入值为f"%{word}%",避免拼接 SQL。这里可以加一个CREATE INDEX idx_zijing_sentence ON zijing(sentence);,虽然千行数据不建索引也能跑,但后续如果要扩展到五万多字的古籍语料,索引的优势就出来了。输出格式里seq:>3是把序号右对齐,日志或终端输出时更整齐。

4.4 生成“字 → 出处”映射

教学场景里常见需求是“点这个字,看它在全书哪些句子里”。这句 SQL 查出来的结果可以直接转成映射:

def build_char_mapping(conn: sqlite3.Connection) -> dict[str, list[tuple[str, int, int]]]: cur = conn.execute("SELECT chapter, seq, sentence FROM zijing") mapping: dict[str, list[tuple[str, int, int]]] = {} for chapter, seq, sentence in cur: for pos, ch in enumerate(sentence): mapping.setdefault(ch, []).append((chapter, seq, pos)) return mapping

mapping的键是单个汉字,值是(篇目, 句序号, 字在句内位置)的列表。导出成char_index.json后,前端输入一个“明”字,直接拿 JSON 查就能返回它在哪些句子、哪些位置出现。这个结构也可以用来做字频表,len(mapping[ch])就是某个字的总出现次数。

5. 排错清单与两个进阶玩法

5.1 扫描件先过 OCR,参数决定准确率

如果 PDF 是扫描版,get_text提取出来的是空白或乱码。先把页面转成 PNG,再用 Tesseract 中文语言包识别:

tesseract scanned_page.png stdout -l chi_sim --psm 6

--psm 6表示把整页当作统一文本块,适合排版规整的正文;带注释的页面改用--psm 4按列识别,再用第 2 章的中缝裁剪逻辑切图分别 OCR。PaddleOCR 对中文小字注释的识别率更高,但模型下载和依赖安装重一些,批量跑之前先拿三页试效果。

5.2 Unicode 私有区乱码的兜底

字体映射错误时,提取结果里会出现\ue000\uf8ff区间的字符。用这个脚本检测:

def check_private(line: str) -> list[str]: return [hex(ord(c)) for c in line if 0xE000 <= ord(c) <= 0xF8FF]

检测结果如果只有少量字符,人工映射到正确汉字后替换;如果大量出现,说明字体 cmap 表被破坏,清洗救不回来,只能对受影响页面走 OCR。

5.3 进阶玩法:生成 Anki 卡组与篇目字频曲线

把库里的句子导出成 CSV,可以直接导入 Anki 做字词卡片:

sqlite3 -header -csv zijing.db \ "SELECT sentence, chapter FROM zijing ORDER BY chapter, seq" > anki_cards.csv

每行是一条卡片,正面是四字句,背面是篇目信息。配合 TTS 朗读插件,背诵复习的闭环就成型了。

按篇目统计不重复字数的走势,能看出学习量随篇目推进的坡度:

import sqlite3 from collections import Counter conn = sqlite3.connect("zijing.db") seen = set() for chapter, sentence in conn.execute( "SELECT chapter, sentence FROM zijing ORDER BY seq" ): new_chars = set(sentence) - seen seen.update(sentence) print(chapter, len(new_chars), len(seen))

输出每一篇的新增字数和累计字数,画出来就是字频曲线。课程设计时可以据此调整每篇的教学时长。

5.4 覆盖率自测:用高频字集合核对清洗结果

清洗是否漏字,用高频字交集检验最直观:

from collections import Counter actual = Counter("".join(clean_lines)) high_freq = set(open("high_freq_200.txt", encoding="utf-8").read().strip()) missing = high_freq - set(actual) print("高频字缺失:", len(missing), "个") print("缺失样例:", sorted(missing)[:20])

把这段存成check.py,每次清洗完语料跑一遍,缺失数量明显下降再继续下一步。如果某次调整清洗规则后,高频率字缺失数反而上升,说明正则把正文误删了,要回到 3.2 检查匹配范围。

本文还有配套的精品资源,点击获取

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

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

立即咨询