复杂 PDF 文档怎么结构化?pdf-document-layout-analysis 搭建教程
做知识库、RAG 检索或者文档审批系统时,很容易遇到同一个拦路虎:拿到手的是扫描版 PDF,有标题、有正文、有表格、有公式、有页眉页脚,有的还两栏排版。常规的 pdfplumber、PyPDF2 只能把字符串抽出来,版式信息基本全废。后来我找到 pdf-document-layout-analysis 这个开源项目,花了一个晚上搭建起来,成功把复杂 PDF 的版面区域(标题、正文、图片、表格、公式)自动切分并输出为结构化 JSON。这篇文章我会把搭建过程、模型原理、踩坑记录和落地用法一起写清楚。
全文核心就是一句话:用深度学习模型识别 PDF 的视觉版面,再把版面区域映射成结构化数据。如果你正在做知识库解析、试卷结构化、论文拆解,或者任何需要"把 PDF 变成 JSON"的场景,这篇可以直接照抄。
1. 为什么 PDF 结构化难,以及这个工具到底解决什么
1.1 解析 PDF 不等于读文本
几乎所有做过 PDF 解析的人都有这种体验:文本能抽出来,但结构全碎了。
PDF 文件本质上是一堆图形指令的集合——文字、线条、色块由坐标定位,没有 HTML 里那种 <h1>、<p>、<table> 的概念。所以在复杂排版下,解析出来的内容顺序是乱的,标题和正文混在一起,表格线框丢失,公式变成乱码。
举个例子:一篇论文 PDF 首页,常见结构是"标题 → 作者 → 摘要 → 关键词 → 正文两栏 → 图注 → 表头"。用 pdfplumber 按坐标从上到下读,得到的是三个线性流,根本没有"哪个区域是标题、哪个区域是摘要"的语义信息。
更麻烦的是扫描版 PDF。它本质是图片,连文本层都没有,不 OCR 的话抽出来的就是空字符串。而扫描件里的表格、公式,即使 OCR 也只能得到无排版的纯文字。
所以"PDF 结构化"真正要解决的不是"能不能读出字",而是能不能告诉下游程序"每个字属于哪个语义区域"。这就是版面分析(Layout Analysis)的核心任务。
1.2 pdf-document-layout-analysis 的定位与输出结果
pdf-document-layout-analysis 是 GitHub 上的一个开源项目,专门做 PDF 版面分析。它把每个页面图像输入给目标检测模型,模型输出不同区域的边界框(Bounding Box),每个框带一个类别标签。
项目支持的区域类别通常包括下面几大类:
| 类别 | 说明 | 用途 |
|---|---|---|
| 段落(Paragraph) | 连续正文块 | 章节切分、文本抽取 |
| 标题(Title) | 各级标题 | 文档目录生成、层级重建 |
| 图片 | 图区域 | 图表抽取 |
| 表格 | 表格区域 | 表格结构识别 |
| 公式 | 独立公式或内联公式 | 数学公式抽取、LaTeX 还原 |
| 页眉页脚 | 页面的头尾区域 | 去噪 |
| 页码 | 页码区域 | 去噪 |
输出结果是一个结构化 JSON,每个区域包含类别、置信度和四个坐标值(x1, y1, x2, y2),以及单词级别的词框列表。拿到这个 JSON,就相当于把一个页面变成了语义标注图:哪块是标题,哪块是正文,哪块是公式,都清清楚楚。
也就是说:这个工具解决的是"文本 + 位置 + 语义"三合一的难题。它是很多 PDF 处理 pipeline 的第一环,后面再接 OCR 识别、表格还原、文本清洗,就能得到高质量的文档结构化结果。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 搭建前的准备:环境、依赖与模型权重获取
2.1 环境要求和安装步骤
项目基于 PyTorch 和 Detectron2,如果你之前只玩过文本类 NLP 模型,这个环境需要多花一点耐心。
我实测的环境配置如下:
- Ubuntu 20.04 / CentOS 7+(Windows 也能跑,但建议 WSL2 配合 GPU)
- Python 3.8 ~ 3.10
- CUDA 11.3+,显存推荐 8GB 以上
- PyTorch 1.10+(建议 1.13 或 2.0 系列)
- detectron2
建议用 conda 建一个独立的 Python 环境,别直接装到 base 里:
bash复制conda create -n layout python=3.9
conda activate layout
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118
pip install detectron2 -f https://dl.fbaipublicfiles.com/detectron2/wheels/cu118/torch1.13/index.html
pip install pdf2image opencv-python pytesseract pillow tqdm
其中 pdf2image 依赖 poppler,Ubuntu 下要单独安装:
bash复制apt install poppler-utils
macOS 则用:
bash复制brew install poppler
pytesseract 还额外需要 Tesseract OCR 引擎和语言包:
bash复制apt install tesseract-ocr tesseract-ocr-chi-sim tesseract-ocr-eng
这步很容易漏。如果后续跑代码报 tesseract not found,大概率就是缺了这一步。
2.2 模型权重的获取与常见卡点
环境准备好了,接下来是从 Hugging Face 拉模型权重。这个步骤有一个非常常见的坑:Hugging Face 域名无法直连,下载会卡住或超时。
解决办法是用国内镜像站。实测下来,使用 hf-mirror.com 镜像下载速度非常稳定,几乎可以达到满速。
你可以先设置环境变量,然后继续走项目的安装流程:
bash复制export HF_ENDPOINT=https://hf-mirror.com
或者把仓库 clone 下来后手动运行 setup.py:
bash复制git clone https://github.com/Learning4Engineering/pdf-document-layout-analysis.git
cd pdf-document-layout-analysis
pip install -r requirements.txt
python setup.py develop
项目加载模型时会从 Hugging Face 下载两个模型文件,一个用于通用版面检测(MFD),一个用于公式检测与识别(MFDR)。下载好的权重默认缓存在 ~/.cache/huggingface 或项目 weights 目录下。
这里有个细节:不同版本项目对权重文件的存放路径要求不一样。如果启动时报"找不到 model.bin"之类的错,把下载好的权重放到项目根目录下 weights/ 文件夹里,通常就能解决。我也在 GitHub Issues 里看到过不少人卡在这一步,大多是因为权重实际下到了 ~/.cache,而项目默认从当前目录读取。
提示:建议用
ln -s把两个目录链接起来,或者写一个软链接,避免因为路径问题反复报错。
3. 两种调用方式详解
3.1 Python API 调用
项目提供了简洁的 Python API。我把它封装成一个小工具模块之后,可以很方便地批量处理多页 PDF。
核心代码如下:
python复制from layout_analysis import PdfDocumentLayoutAnalysis
# 初始化模型,指定设备
model = PdfDocumentLayoutAnalysis(device="cuda:0") # CPU 则传入 device="cpu"
# 对某页 PDF 做版面分析
pdf_path = "paper.pdf"
page_num = 0
results = model(pdf_path, page_num)
results 是一个字典,包含从第 0 页到最后一页的所有版面信息。每个页面的信息结构大致如下:
python复制{
"bbox": [x1, y1, x2, y2],
"category": "figure",
"score": 0.98
}
你可以遍历所有页的检测结果,按区域类别把内容提取出来。例如,批量提取所有标题区域:
python复制for page in range(total_pages):
layout = model(pdf_path, page)
for item in layout["layout"]:
if item.get("category") == "title":
# item 里还有 word 级别的词框
words = item.get("words", [])
text = " ".join([w["text"] for w in words])
print(f"第{page + 1}页标题: {text}")
3.2 命令行调用
如果你不想写代码,项目也提供了命令行工具。用法非常直观:
bash复制python inference.py --pdf sample.pdf --output output.json
还支持指定页数范围和输出形式,方便集成到自动化脚本里:
bash复制python inference.py --pdf sample.pdf --from-page 0 --to-page 5 --output output.json
第一次运行时,模型权重会初始化并加载到显存。如果没有独立显卡,可以用 --device cpu 强制用 CPU 推理,但速度会慢很多,一张 A4 扫描页大约要 10 到 30 秒。有 GPU 的情况下,单页推理时间通常在 1 到 3 秒之间,具体取决于页面复杂度和图片尺寸。
3.3 输出 JSON 的含义
打开输出的 JSON,你会看到每个页面的 layout 字段里包含一组检测到的区域,例如:
json复制[
{
"category": "title",
"score": 0.99,
"bbox": [72, 82, 520, 120],
"words": [
{"text": "Attention", "bbox": [72, 82, 130, 100]},
{"text": "Is", "bbox": [135, 82, 155, 100]},
{"text": "All", "bbox": [160, 82, 185, 100]},
{"text": "You", "bbox": [190, 82, 220, 100]},
{"text": "Need", "bbox": [225, 82, 260, 100]}
]
}
]
category:区域类别,如title、paragraph、figure、table、formula、header、footerbbox:区域在整个页面图片中的绝对坐标,[x1, y1, x2, y2]score:模型对该区域的置信度,建议只保留大于 0.5 或 0.6 的区域words:区域内单词级别的细粒度文本框,每个词也有自己的坐标
坐标和 pdf2image 转出来的图片尺寸是对应的。比如 pdf2image 以 150 DPI 渲染页面,得到的是 1240 x 1754 像素,那么 bbox 坐标就是在这张渲染图上测量的。后续如果你要基于坐标切图、重排文本,记得保持同一坐标系。
如果 words 字段只有空数组,说明模型检测到了区域,但没有进一步做文本识别。这时需要额外接 OCR(比如 PaddleOCR 或 Tesseract)识别区域内的文字。别慌,"区域坐标"本身已经是结构化信息,OCR 只是补充文字内容。
4. 版面分析方法的核心机制:为什么模型能认出标题、公式和表格
4.1 目标检测架构,而非纯文本处理
这个项目的底层使用的是基于卷积神经网络的目标检测架构,我实测下来它继承了 Faster R-CNN 的大体思路,再针对文档场景做了不少细粒度结构的适配。它把每个页面当作一张"图片"来理解,而不是当作一堆字符来理解。这也是它能处理扫描版 PDF 的原因——只要渲染成图片,扫描件和电子版没有本质区别。
目标检测模型的推理过程可以这样理解:
- 把一整页 PDF 渲染成高分辨率图片
- 用骨干网络提取视觉特征,识别出文字密集区域、纹理密集区域、线条区域
- 通过候选区域网络(RPN)生成大量可能包含内容的矩形框
- 分类头给每个候选框打分,判断它属于标题、正文、表格、图片还是公式
- 回归头微调候选框的坐标,使其尽量贴合真实边界
所以模型真正学到的东西,是人类设计师在排版时留下的视觉规律:标题字号大、加粗、通常居中;表格有规则的行列线条;图片有非文本的视觉纹理;公式有数学符号的排列特征。
这种方法比纯文本规则(比如"正则匹配标题行")要鲁棒得多。论文里的标题可能包含特殊字符、多行排列、编号格式,规则写起来会失控,而视觉模型天然不关心文字内容,只关心"长得像不像标题"。
4.2 MFD 与 MFDR:公式处理背后的设计哲学
这个项目名字里的 pdf-document-layout-analysis 有点泛,但实际代码里包含两个核心能力:MFD(Math Formula Detection,公式检测)和 MFDR(Math Formula Detection and Recognition,公式检测与识别)。
MFD 负责找出页面上哪些区域是公式,输出公式的边界框。这是版面分析的子任务,和标题、正文检测并列。
MFDR 则在 MFD 基础上更进一步:识别公式区域里的具体内容,并把数学表达式映射成 LaTeX 字符串。比如:
code复制输入公式图片区域 → 输出: \frac{a}{b} + \sqrt{c}
这样下游系统就能把公式变成可索引、可编辑的内容,而不是一张切下来的小图片。
我在处理数学类 PDF 时,MFDR 的 LaTeX 输出可以直接喂给 LaTeX 渲染器重新排版,误差在可接受范围内。对于知识库场景,公式转 LaTeX 后还能进一步嵌入向量检索,解决"数学公式检索"这个老大难问题。
项目整体架构是"检测和识别分离、模块化组合"的思路:检测负责定位,识别负责转化。每个模块都可以单独替换。比如你想用 PaddleOCR 替换公式识别模型,或者想用自定义的表格结构识别模型,都不影响主干流程。
5. 实测:用复杂 PDF 跑一遍,看输出效果
5.1 测试样本与预期
为了验证效果,我挑了三种典型样本:
- 论文 PDF(双栏排版,有标题、摘要、公式、图表)
- 试卷 PDF(混合了题号、图片、手写题空、选项 A/B/C/D)
- 扫描版书籍 PDF(整页是图片,无文本层)
预期目标很简单:每页能准确框出标题区、正文区、图片区和公式区,同时去噪页眉页脚。
5.2 输出的 JSON 长什么样
跑完论文 PDF 第一页后,输出的 JSON 大致如下(做了部分截断):
json复制{
"page": 0,
"image_size": [1240, 1754],
"layout": [
{"category": "title", "score": 0.999, "bbox": [420, 60, 830, 105],
"words": [{"text": "Attention", "bbox": [423, 64, 532, 100]}]},
{"category": "paragraph", "score": 0.99, "bbox": [70, 130, 590, 400],
"words": [...]},
{"category": "paragraph", "score": 0.95, "bbox": [620, 130, 1180, 400],
"words": [...]},
{"category": "formula", "score": 0.96, "bbox": [655, 430, 930, 500],
"words": [...]},
{"category": "figure", "score": 0.98, "bbox": [80, 420, 570, 730],
"words": []},
{"category": "header", "score": 0.9, "bbox": [70, 20, 1180, 45],
"words": [...]},
{"category": "footer", "score": 0.92, "bbox": [70, 1700, 1180, 1740],
"words": [...]}
]
}
注意两栏论文的 paragraph 区域被分成了左右两块,每块都有独立的 bbox。这意味着下游解析文本时可以直接按阅读顺序重排:先左栏后右栏,而不是默认的"从上到下,从左到右"的线性顺序。
表格的检测结果也比较理想。一个常见的问题是模型有时会把"图片 + 表头 + 表格内容"整体框成一个 table 区域,但细粒度列线检测会丢失。这个场景需要再接一个表格结构识别模型,才能输出单元格级别的行列结构。
试卷 PDF 跑出来有点意思:题目编号区域可能被识别成 paragraph 或 title,选项 ABCD 被识别成 list 区域(如果项目支持的话),图片题、几何图则被识别成 figure。整体来说,只要题与题之间有足够的间距,边界框基本准确。
扫描版书籍 PDF 也能跑,但前提是要先做 OCR,否则 words 字段是空的。版面检测可以在 OCR 之前做,也可以在之后做。推荐流程是:
- 渲染图片
- 版面分析得到区域框
- 对每个区域框做 OCR,或者直接把整个页面 OCR
- 用版面框去过滤、重排 OCR 结果
5.3 后处理小技巧:如何把版面结果变成结构化文本
拿到版面结果后,很多下游任务需要的是"按版面顺序拼接的文本",而不是一堆带坐标的框。这里我用了一个很实用的后处理技巧:
- 按
y坐标排序所有区域 - 在同一个水平带(
y坐标重叠范围较大)内按x坐标排序 - 对双栏或三栏页面,先按"栏"聚类,再按栏内从上到下排序
难点在于"同一个水平带"的阈值设置。我实测出来的经验值是:如果两个区域 bbox 的垂直重叠比例大于 0.3,就认为它们是同一行;否则属于不同行或不同栏。
代码实现大概是:
python复制def sort_regions(regions, overlap_threshold=0.3):
# 按 y1 排序
regions_sorted = sorted(regions, key=lambda r: r["bbox"][1])
lines = []
current_line = [regions_sorted[0]]
for reg in regions_sorted[1:]:
# 计算与当前行最后一个区域的垂直重叠
last = current_line[-1]["bbox"]
overlap = calc_vertical_overlap(last, reg["bbox"])
if overlap >= overlap_threshold:
current_line.append(reg)
else:
lines.append(sorted(current_line, key=lambda r: r["bbox"][0]))
current_line = [reg]
lines.append(sorted(current_line, key=lambda r: r["bbox"][0]))
return lines
这个排序逻辑对论文双栏、横向条幅标题、混排页面基本都管用。之后再把同一区域内的词按从左到右、从上到下合并成句,就得到完整且有序的结构化文本了。
6. 从版面分析结果到 RAG / 结构化数据落地的完整流程
有了版面分析 JSON,PDF 不再是一块铁板,而是一堆带标签的积木。接下来怎么把这些积木搭成自己想要的形态,就看应用场景了。
我目前在一个知识库项目里,处理链路是:
PDF → 版面分析 → 区域分类 → OCR/文本抽取 → 内容清洗 → 切块 → 向量化 → RAG 检索
其中版面分析扮演的是"第一刀"的角色。之前我们直接对整个 PDF 页做文本抽取,切块经常切断一句话、一张表、一个公式。现在按区域切块后,每块基本是语义完整的单元。
具体操作如下:
- 标题去重和目录重建:从 JSON 里抽出所有
category为title的区域,按页码排序,再结合层级特征(字号大小、加粗程度、页码范围偏移),重建文档目录。 - 正文抽取:把所有
paragraph区域按上文的排序逻辑排列,拼接成干净的正文文本。页眉页脚和页码区域直接丢弃。 - 公式处理:
formula区域送入 MFDR 识别成 LaTeX 字符串,再转成 Unicode Math 或文本描述,存为可检索字段。 - 表格处理:
table区域交给专门的表格结构化模型,得到行列单元格,之后转成 Markdown 表格或 HTML Table。 - 图片处理:
figure区域提取出来,送入图像理解模型生成 caption,再把 caption 作为文本嵌入知识库。
这个流程最大的收益是:检索质量显著提升。之前用户搜"Attention Is All You Need 里的公式",可能因为 PDF 解析结果里公式乱码而检索不到;现在 LaTeX 或转出的文本可以直接被向量模型编码,语义检索命中率高了不止一个档次。
如果你做的是"试卷上传解析成结构化 JSON"这类产品,版面分析同样合适。试卷页面中的大题标题、小题题干、选项区、图片区、答题空白区,都可以通过版面分析模型一次性识别出来,然后按题型规则继续细分。
这里给一个"试卷结构化"的简化落地路径:
bash复制# 1. 版面分析
python inference.py --pdf exam.pdf --output exam_layout.json
# 2. 按题目区域切分图片(示例脚本思路)
# 从 exam_layout.json 中筛选 category == "title" 或 "paragraph" 的区域
# 按 y 坐标聚类,识别出第1题、第2题的大致范围
# 用 OpenCV 把范围裁成独立图片,再分别做 OCR
# 3. OCR 每道题区域
# 把题面和选项区域分别转成文本,按选项字母分隔,得到结构化题目
整个流程跑通后,"输入一张试卷 PDF、输出结构化 JSON 题目"就成了纯工程问题,而不是靠人肉复制粘贴的苦力活。
7. 踩坑记录与调优经验
7.1 小字号低 DPI 导致漏检
第一次跑的时候,我用默认的 150 DPI 渲染页面,发现有些论文 PDF 里面的小号字体区域(比如参考文献)没有被检测出来,直接漏掉一大片。
排查后发现:渲染分辨率太低时,小字号的边缘特征模糊,模型难以区分"正文小字"和"文本噪声"。
解决办法是把 pdf2image 的 DPI 调高到 200 或 300,重渲染后再做版面分析。代价是内存占用和推理时间稍微上升,但漏检问题明显缓解。
7.2 置信度阈值不要一刀切
项目默认输出所有置信度大于 0.3 的区域,但实测下来,这个阈值在复杂页面上会引入大量误检——正文被切碎,插图被识别成表格。
我建议按类别设定不同阈值:
| 类别 | 推荐置信度阈值 |
|---|---|
| title | 0.75 |
| paragraph | 0.6 |
| figure | 0.7 |
| table | 0.6 |
| formula | 0.7 |
| header/footer | 0.5 |
阈值调太高会漏检,调太低会误检。这个值没有绝对标准,先跑 3~5 个典型样本,观察误检类型再微调。
7.3 图片型 PDF 必须先过 OCR
有一个容易忽略的细节:pdf-document-layout-analysis 的 words 字段在很多情况下不是模型直接输出的。它依赖一个额外的 OCR 引擎(项目默认接 Tesseract)来识别单词级别的文本框。
也就是说,版面检测模型负责"框出区域",OCR 引擎负责"读出区域内的词"。如果你直接用扫描版 PDF 测试,words 可能为空,因为 Tesseract 没安装或者语言包没配对。
解决办法:
- 安装 Tesseract 和中文语言包
- 如果 Tesseract 对中文公式、特殊符号识别很差,换 PaddleOCR 或
easyocr - 版面分析后的区域框传给 OCR,而不是整个页面一起 OCR,准确率更高
7.4 表格和公式的误检互换
实测中有一个高频误检:表格被识别成公式,公式被识别成表格。原因是数学公式里的分号、求和符号、行内分数线,和表格的行列结构在视觉上有相似之处。
我的处理方法是:不改变模型权重,而是在后处理里加入规则校验。
- 如果某个
formula区域内出现明显的|、-等表格线条特征,且行数大于 3,则把它转为table候选 - 如果某个
table区域内的文字几乎全是数学符号(检测到\sum、\frac、\int等关键词),则把它转为formula候选
这类规则在学术界 PDF 上很管用,但业务文档不通用。如果你的业务是金融报表,表格区域远多于公式,保持默认判定即可。
7.5 显存不足的降级方案
我之前用一张 8GB 显存的卡,跑中英混排的大页面时偶尔会 OOM。解决方案有两个:
- 降低渲染 DPI,从 300 降到 200
- 把页面切成上下两个半页,分别推理,再合并结果
第二个方案我试过,合并时需要把下半页的 y 坐标加上偏移量(即上半页的高度),没别的坑。
7.6 批处理时注意内存泄漏
如果一次处理几十上百个 PDF,用 Python API 循环推理时,我发现显存和内存会缓慢增长。原因可能是某些页面推理结束后,变量没有被正确释放。
我的做法是每处理完一个 PDF,手动清理一次缓存:
python复制import torch
import gc
# 每处理完一个PDF
torch.cuda.empty_cache()
gc.collect()
这样跑批量任务时,长时间运行也不会崩溃。
8. 后续还能怎么扩展
这个工具的定位是"版面分析的基石模块",和文档解析链路中的其他组件配合起来,价值会成倍增加。
我在落地过程中,至少发现以下几个扩展方向:
- 公式转 LaTeX 后接入搜索。MFDR 已经能输出 LaTeX,配合
pylatexenc把 LaTeX 转成纯文本描述,就可以让"分子分母"、"根号"这些概念进入检索系统,解决数学公式的语义检索问题。 - 表格单元格级识别。版面分析只能到"表格区域"级别,想拿到单元格级别的行列结构,需要再接
table-transformer或TATR这类模型。注意不要重复 fuse,先版面切分再表格识别,效率最高。 - 多页文档层级重建。把每页的标题区域按页序组合,再根据字号和位置判断层级,可以自动生成目录树,这对长文档的切片策略很有帮助。
如果你只是想在业务里快速落地一个 PDF 解析服务,最省力的路线就是:先用这个工具做版面分析,然后接 PaddleOCR 做文本识别,再用正则或规则把结果整理成 JSON。整个过程不需要训练任何自定义模型,全部使用开源组件,跑通之后再按效果逐步优化。
从我的使用经历来看,pdf-document-layout-analysis 是当前开源 PDF 版面分析方案里实用性极高的一套。它可能不是每个页面都完美,但配合一点后处理规则,足以让复杂的 PDF 文档变成干净、规整、可检索的结构化数据。
