阅读文本 / Reading text
本文使用合成样本演示处理方法。代码已通过隔离环境中的示例检查;这不代表对所有真实文件或产品效果的保证。处理重要文件前,请保留原件,并按文中的边界与验收项复核。
当你用 PyMuPDF 的
extractDICT()
提取 PDF 文本时,可能会发现视觉上连贯的一句话,在返回的字典里被切成了七八个甚至更多的
span
。这通常不是解析错误,而是 PDF 底层结构的真实反映:渲染引擎为了处理字体切换、颜色变更或局部样式调整,必须将文本流断开。把这种“样式切片”误当作“段落分割”来处理,是后续清洗、翻译或检索环节出现断句错误的根源。
Block、Line 与 Span 的真实层级关系
PyMuPDF 的字典输出严格遵循 PDF 的内容流结构。最外层是
block
,代表页面上一个独立的排版区域,比如一段正文、一个表格单元格或一个图注。
block
内部包含若干
line
,对应视觉上的单行文本。而
line
内部的
span
,则是具有相同字体、字号、颜色和样式的连续字符序列。
这意味着,只要一行文字中间出现了加粗、斜体、换色或字体回退(例如中文里夹杂英文数字),就会产生新的
span
。这些
span
属于同一个
line
,共享相同的基线和垂直位置。真正的段落结束标志是
line
列表的终结以及新
block
的开始,而不是
span
的数量。混淆这两者,就会把“
重要
提示”错误地识别为两个独立句子,导致翻译记忆库无法匹配完整术语。
合成示例:正确输出与边界情况
为了说明问题,我们构造一个合成场景。假设输入 PDF 中有一行文字:“请注意:系统将在5分钟后重启。”其中“请注意:”使用黑体,“系统将在”使用宋体,“5”使用等宽数字字体,“分钟后重启。”又回到宋体。
正确的
extractDICT()
输出应在一个
line
对象下包含四个
span
,每个
span
的
bbox
在垂直方向上高度重叠,且
origin
的 y 坐标基本一致。这四个片段拼接后才是完整语义单元。
一个典型的错误输入是扫描版 PDF 未经过 OCR,或者 PDF 生成器将每个汉字都作为独立文本对象写入。此时
extractDICT()
可能返回数十个只含单字的
span
,且它们的
bbox
之间可能存在微小间隙或不规则重叠。这种情况不能简单通过字符串拼接修复,因为缺乏可靠的排序和分组依据。处理方式应是标记该页面为“低质量结构化数据”,转入人工校对或专用 OCR 流程,而非强行合并。
基于几何特征的 Span 合并逻辑
以下 Python 示例演示如何在内存中模拟
extractDICT()
的输出结构,并实现一个安全的同行 span 合并函数。
注意:本例纯内存构造合成数据,未读取任何真实PDF文件,仅用于验证合并算法逻辑
。实际使用时需确保 PyMuPDF 版本支持
extractDICT
方法(推荐 1.23.x 及以上),并在你自己的锁定依赖环境中运行。
# 合成示例:演示同行span合并逻辑,非真实PDF解析结果
# 依赖: PyMuPDF (pymupdf) >= 1.23.0,仅用于类型参考,本例纯内存构造
def merge_spans_in_line(line_dict, y_tolerance=2.0):
"""合并同一line内y坐标相近的span,保留原始顺序"""
if not line_dict.get("spans"):
return ""
spans = sorted(line_dict["spans"], key=lambda s: s["bbox"][0]) # 按x排序
merged_text = []
current_y = spans[0]["origin"][1]
buffer = [spans[0]["text"]]
for span in spans[1:]:
y = span["origin"][1]
if abs(y - current_y) <= y_tolerance:
buffer.append(span["text"])
else:
merged_text.append("".join(buffer))
buffer = [span["text"]]
current_y = y
merged_text.append("".join(buffer))
return "\n".join(merged_text)
# 构造合成line数据,模拟四种字体混排(正常情况)
synthetic_line = {
"spans": [
{"text": "请注意:", "bbox": (100, 200, 160, 220), "origin": (100, 215)},
{"text": "系统将在", "bbox": (160, 200, 220, 220), "origin": (160, 215)},
{"text": "5", "bbox": (220, 200, 230, 220), "origin": (220, 215)},
{"text": "分钟后重启。", "bbox": (230, 200, 310, 220), "origin": (230, 215)}
]
}
result = merge_spans_in_line(synthetic_line)
assert result == "请注意:系统将在5分钟后重启。", f"合并失败: {result}"
# 边界测试:y坐标差异超过容差,应视为不同行片段(即使在同一line dict中也应分隔)
bad_line = {
"spans": [
{"text": "上行", "bbox": (100, 200, 140, 220), "origin": (100, 215)},
{"text": "下行", "bbox": (100, 230, 140, 250), "origin": (100, 245)}
]
}
bad_result = merge_spans_in_line(bad_line, y_tolerance=2.0)
# y差值30 > 2.0,应被分隔为两行
assert bad_result == "上行\n下行", f"异常行未正确分隔: {repr(bad_result)}"
注意:
y_tolerance
的值 2.0 仅为合成示例设定,并非行业标准。实际项目中应根据文档平均字号动态计算,或直接信任
line
层级的分组——如果
line
本身已正确划分,通常无需额外 y 校验。
验收标准与人工审查边界
观察现象
可能原因
处理决定
是否适用自动合并
同一 line 内多个 span,y 坐标一致
字体/颜色切换
安全拼接 span.text
是
span 间有明显水平间隙(>空格宽度)
制表符、分栏残留
检查 bbox 间距,插入空格或制表符
谨慎,需规则
同一 line 内 span y 坐标差异 > 字号 10%
行内公式、上下标
保留原始 span 结构,不合并
否
单字 span 且 bbox 无序
扫描件或损坏 PDF
标记为低质量,转人工或 OCR
否
block 间文本语义连续
分页断段
跨 block 合并需上下文判断
否,需 NLP 辅助
此表仅覆盖代码演示层面的结构校验。端到端效果还需结合语言模型或人工抽检。
特别注意
:自动合并绝不应用于法律合同、医疗记录等对格式敏感的文档,除非经过领域专家验证。使用前请自行准备脱敏测试PDF,运行
page.get_text("dict", sort=True)
导出JSON后逐项核对。
参考资料与下一步
PyMuPDF TextPage 官方文档:https://pymupdf.readthedocs.io/en/latest/textpage.html
PyMuPDF extractDICT 结构详解:同上页面“Structure of Dictionary Outputs”章节
下一步行动:在你的测试 PDF 上运行
extractDICT(sort=True)
,导出 JSON 并用本文表格逐项核对 span 分布;若发现大量异常拆分,优先评估文档源质量而非调整合并参数。
将这份检查清单用于文档翻译
先把页面顺序、来源位置与文本完整性检查清楚,再把同一份脱敏样本交给翻译流程。译后仍要检查标题、正文和图注是否正确对应,不能只看文字是否通顺。
OmniDoc(产品入口)
提供多格式文档翻译入口,官网介绍了 OCR、术语表与双语对照功能。对希望直接从文件开始处理的团队,这是一条值得用实际样本评估的路径。请使用有权上传的脱敏文件,并用本篇的检查项复核结果;这里不声称 OmniDoc 使用了本文的开源库或内部算法,也不以示例代替产品性能测试。
- 1 -
更新于 2026-10-08