在知识库问答场景中,处理 PDF 是不可回避的基础工作。大多数初次接触 PDF 文本提取的开发者都会遇到相似的问题:提取出来的文字缺少换行,双栏论文左右横向读串,每页的页眉、页脚和页码被切碎插入到了段落正文中间。
这些问题的根源并非提取库出现错误,而是由 PDF 这门格式的物理特性决定的。
为了从底层解决这一问题,BibGenie 的 PDF 提取引擎选用知名的开源渲染引擎 MuPDF(通过 WebAssembly 运行于前端与 Node 环境)作为基础解析器。MuPDF 擅长处理复杂的 PDF 二进制流、字体 CMap 映射与矢量图元解码,但在排版理解上,它给出的依然只是物理画布层面的字符与文本块。
本文基于 BibGenie 的实际工程实现,探讨如何在 MuPDF 提取的原始几何数据之上,逆向重构出符合人类自然阅读顺序的一维文本流。上篇聚焦于宏观层面的几何降噪、字体特征分析与多栏阅读顺序恢复。
一、 PDF 的真实世界:没有段落,只有绘制指令
在 HTML 或 Word 文档中,内容以结构化的树形节点组织,包含天然的 <p>、<h1>、<table> 等语义标签。换行由排版引擎根据容器宽度自动计算。
PDF 的设计初衷是精准打印。它是一张二维矢量画布,继承自 PostScript 语言。PDF 文件中存储的不是文章内容,而是一组给渲染器使用的坐标绘制指令:
BT
/F1 10.5 Tf
72.0 710.0 Td (In this paper, we propose a novel approach...) Tj
ET
这段指令告诉阅读器:选择字号为 10.5pt 的字体 F1,移动画笔到坐标 (72.0, 710.0),绘制一段文字。
这一机制带来了三个直接事实:
- 没有段落与分栏概念:PDF 内部没有单栏、双栏标记,也没有哪两行文字属于同一段的元数据。
- 绘制顺序与阅读顺序完全解耦:PDF 流中的指令顺序取决于排版软件的输出逻辑。排版引擎完全可能先在页面底部画注脚,接着画右上角页眉,最后画中间正文。如果直接按文件内的字节流顺序读取文字,必然得到混乱的文本。
- 坐标系统差异:MuPDF 渲染引擎以页面左上角为原点(Y 轴向下增长),而 PDF 国际标准和阅读器标注规范通常以页面左下角为原点(Y 轴向上增长),且页面常伴随裁剪偏移与旋转角度。
因此,职责边界非常明确:MuPDF 负责将 PDF 二进制数据解析为离散的二维字符坐标、四边形(QuadPoint)与矢量图元;版面重构算法则在此基础上进行逆向工程,推导回文档的层次结构与阅读流。
整个宏观版面重构由以下四个递进环节构成:
MuPDF 原始文本块 (Raw Blocks) & 矢量图元
│
▼
1. 字符级样式统计 ─── 统计字符频次,确定主正文字号基准
│
▼
2. 几何与频次降噪 ─── 剔除周期性页眉页脚与单调递增页码
│
▼
3. 多栏排版重构 ─── 结合 MuPDF 矢量底色与分割线聚合文本块
│
▼
4. 阅读顺序拓扑排序 ─── 计算左侧重叠栏排序权重,生成自然阅读流
二、 字体样式分析:利用字符频次定位正文基准
页面上散落着各种字号的文本:标题、正文、作者单位、表格内容、脚注。在进行版面切分前,BibGenie 必须先回答一个基础问题:这篇文档的正文字号到底是多少?
许多解析器通过统计文本片段(Span)的出现次数来猜测正文字号。但在真实的学术论文中,这种统计方式极易失真。
1. 为什么不能统计片段数量?
一段 500 字的长正文往往只构成一个完整的文本片段,而页面底部的 5 条参考文献可能包含 20 个不同样式的作者名、斜体刊名和年份片段。如果简单对片段数量做直方图统计,注脚和引用的微型片段会在数量上占据主导地位,导致将注脚错误判定为文档正文。
解决方式是字符加权统计(Character-Frequency Analysis):
遍历文档中所有的文字片段,提取其复合样式键:
对于每一个文字片段,统计其包含的有效字母与数字字符总数,将字符数累加到对应的样式键上,而不是累计片段的出现次数。以字符数作为度量衡,长段正文的绝对体量会直接压过零散的元数据。
2. 确定主正文与相对层级
统计完成后,按字符总数降序排列:
- 主正文样式:字符数累计最高的样式即为主正文。
- 包容正文集:部分论文正文中穿插有少量加粗、斜体或微小的字号微调(例如 10pt 与 10.5pt 混用)。将所有字符数达到主正文 以上的样式都纳入正文集合。
- 相对层级推导:
- 大标题与小标题:字号 ,或者字号略大且带有加粗属性。
- 脚注与版权申明:字号 。
- 图表注记:字号处于 区间。
有了这套样式基准统计,后续的版面过滤器就能明确区分核心正文与边缘修饰文本。
三、 边距降噪:过滤页眉、页脚与页码
跨页段落经常被页面顶部的期刊名(如会议名称、出版商信息)和页面底部的页码打断。如果直接把它们提取进正文,会将一个完整句子切裂成两截。
1. 固定几何裁剪的局限
最朴素的清洗方式是在页面四周划定固定裁剪区,例如顶部留 50pt,底部留 50pt,超出部分直接丢弃。
但在实际场景中:
- 某些紧凑排版的会议论文,正文距离页底甚至只有 25pt,固定裁剪会直接削掉正文末行;
- 图书或专著的页码可能印在正文内侧,固定裁剪无法触及;
- 某些页面带有宽边栏注释,简单裁剪会彻底丢失有效信息。
因此需要采用几何候选区与跨页统计验证相结合的双层策略。
2. 跨页频次分析消除固定页眉页脚
算法先设定一个宽松的观察区(例如页面上下各 80pt 的边缘带)。落在这个区域内的文本不会被直接删除,而是加入候选池进行全文档频次分析:
- 文本归一化:去除首尾空格,统一转换为小写。
- 跨页投票:统计相同文本在不同页面出现的次数。
- 频次判定:若相同文本在 3 个或更多页面中重复出现,判定为周期性页眉或页脚。
由于正文内容在每页都不同,哪怕紧凑排版让正文行延伸到了顶部 30pt 位置,因为该行文本仅在当前页出现一次,频次过滤机制会将其完整保留。
3. 严格单调递增序列过滤页码
页码是特殊的边距元素:它位于页眉或页脚区域,但每页的字符都不相同,无法通过重复频次找出。
页码过滤引入了序列单调性验证(Monotonic Sequence Verification):
BibGenie 检测匹配常见页码形式的字符串:纯阿拉伯数字(12)、带前缀形式(Page 12)、罗马数字(iv)、以及连字符包围形式(- 12 -)。
算法按以下流程运行:
- 从页面顶部和底部的候选区提取符合页码格式的字符串,转换成整数值。
- 追踪这些候选值在页码索引递增时的变化。
- 严格判定条件:只有当候选值满足后一页比前一页严格递增 1(),并且在连续页面中稳定出现时,才将该元素作为页码剔除。
如果正文段落恰好以一个单独的数字结尾,或者列表项包含 1.,由于无法在连续页面形成空间位置对齐的严格递增链,BibGenie 不会将其误删。
四、 多栏重构:从二维空间到自然阅读流
学术论文中最常见的排版是双栏,或者混排(例如顶部的标题和摘要为单栏全宽,底部的正文分为双栏)。
如果直接按照视觉从上到下的 Y 轴坐标读取,提取器会先读左栏第一行,接着读右栏第一行,随后读左栏第二行,导致文本彻底错乱。
多栏排版的重构通常需要经历块合并、底色阻断和阅读流拓扑排序三个步骤。
1. 矩形块聚合与对齐容差
底层引擎提取出的初始文本块往往粒度较细,一个段落有时会被拆成两个垂直相连的块。
算法首先对文本块进行聚类:
- 水平重叠检查:若两个块在水平方向有重叠,并且它们的左右边界差距在容差范围内(例如 3pt 的对齐误差),尝试将它们合并。
- 垂直间距约束:两个块之间的垂直距离不能超过设定的段间距上限(例如 10pt)。
- 非干涉原则:两个块联合形成的包围盒,绝对不能与页面中已存在的第三个块相交。如果相交,说明中间隔着其他栏的内容,必须放弃合并。
2. 矢量图层引导:背景框与分割线
在杂志和现代论文中,常出现带浅灰色背景的侧边栏、重要结论提示框,或者双栏之间的一根细竖线。
如果仅靠文字外接矩形判断,提示框很容易与相邻的正文栏被误认为在同一列。
因此需要利用 MuPDF 提供的图形设备遍历接口,捕获底层的矢量绘图事件:
- 背景填充矩形(
fill_rect):如果发现页面上存在面积较大的有色底框,将其记录为隔离容器。算法规定:禁止将容器内的文本块与容器外的文本块合并。 - 描边线条(
stroke_path):提取细长的水平线或垂直线。若两个文本块之间横亘着一条垂直分割线,即便它们在几何距离上很近,合并操作也会被物理阻断。
3. 左侧重叠拓扑排序算法
将页面分割为若干矩形栏后,最关键的一步是确定各个栏之间的阅读先后顺序。
考虑如下经典单双栏混排布局:
┌──────────────────────────────────────┐
│ Title & Abstract (Column A) │ (全宽单栏)
├──────────────────┬───────────────────┤
│ │ │
│ Left Column │ Right Column │
│ (Column B) │ (Column C) │ (双栏正文)
│ │ │
└──────────────────┴───────────────────┘
正确的阅读顺序必须是:A -> B -> C。
简单的排序规则往往顾此失彼:
- 如果按“水平 X 轴优先(从左到右)”排序:由于 Column B 在最左侧,会被排在最前面,导致左栏正文抢在顶部的标题 A 之前被读取。
- 如果按“垂直 Y 轴优先(从上到下)”排序:Column B 和 Column C 顶端处于同一水平高度,微小的排版波动可能导致 B 和 C 的顺序错位。
解决这一问题的核心是基于空间拓扑关系的排序权重算法:
对于页面中的任意一个栏 :
- 在所有已检测出的栏中,寻找所有满足以下两个条件的栏 :
- 几何位置严格处于 的左侧;
- 在垂直 Y 轴方向上与 存在有效投影重叠。
- 如果左侧没有重叠栏(如 Column A 和 Column B),说明自己是这一横向分区的起始栏,使用自己的顶部 Y 坐标作为排序键。
- 如果左侧存在重叠栏(如 Column C),取其最左侧重叠栏的顶部 Y 坐标作为基准权重,再附加上微小的水平 X 偏移。
针对上述布局的计算过程:
- 处理 Column A(全宽标题):左侧没有重叠列,排序键为其自身的顶部坐标 。
- 处理 Column B(左正文栏):左侧没有重叠列,排序键为其自身的顶部坐标 。
- 处理 Column C(右正文栏):左侧存在重叠列 Column B。算法取 Column B 的顶部 Y 坐标作为其基准,再附加微小的水平位移量 ,得到排序键为 。
最终得到的排序键关系为:
输出序列严格为 。
无论页面是不对称分栏,还是嵌入了横跨半页的宽图表格,这种寻找左侧重叠链的拓扑算法都能保证一维阅读流顺畅连续。
五、 本篇小结
通过上述几何规则与统计分析,BibGenie 的 PDF 提取引擎完成了对 PDF 宏观版面的重构:
- 通过字符频次统计,消除了零散注脚对正文字号的干扰;
- 通过跨页频次分析与严格单调序列检查,在保护正文的同时剥离了页眉、页脚和页码;
- 通过矢量底色隔离与左侧重叠拓扑排序,将错综复杂的双栏与混排页面拆解为符合自然人类习惯的一维阅读流。
此时,文档已经被整理为排序正确的干净文本块。但在每一个独立的栏目内部,文字依然是由零散的行构成的。
在下篇 PDF 文本提取与版面重构(下):段落切分与字符级坐标对齐 中,内容将转向微观排版语义的重构:探讨如何利用中位数绝对偏差(MAD)准确切分段落、如何还原行尾连字符截断,以及如何建立从提取出的句子反向定位到 PDF 像素坐标(BoundingBox)的严格映射。