主题
文档解析:文字版 PDF / 表格 / 扫描件 OCR / 版面分析
本章要回答的问题:PDF 抽出来的文字,为什么直接切块会出事?表格、扫描件、双栏版面各自会坏在哪一步?
RAG 的上限在解析这一步就定了。后面的 embedding、索引、重排再精致,也救不回一张被拆散的表、一段读串行的双栏正文。
先说结论:文字层能抽出来的只是字符流,不是文档结构。 页码、条款号、表格边界、图注归属,都得另外补。补不回来的部分,要么在切块时绕开,要么在回答时承认查不到。
三类文档,三种坏法
| 类型 | 能拿到什么 | 主要风险 |
|---|---|---|
| 文字版 PDF | 字符、坐标、字号 | 表格单元格错位、双栏读串行、页眉页脚混进正文 |
| 扫描件 | 只有像素 | 没有文本层,必须先 OCR;错字会一路带进检索与引用 |
| HTML / 网页 | DOM 结构 | 导航、版权、脚本混进正文;政府页面尤其啰嗦 |
本书的语料三者都有:法规是 HTML,事故报告与环境影响报告表是 PDF,工作指南是 215 页的文字版 PDF。 pymupdf(PyMuPDF)一次打开就能拿到按页的字符流与表格候选(引用 [4]);它不是唯一选择,但足够把问题暴露出来。
文字层:抽得全,不等于抽得对
先看完整性。对同一份 64 页的环境影响报告表,用 pymupdf 抽出的正文与语料流水线里已有的 pypdf 结果做 4-gram 比对,覆盖率是 100%(实测,见 benchmarks/rag/parse.csv)。 63 页有文字,剩下 1 页是封面图。这说明:只要 PDF 有文本层,拿到字不是难点。
难的是字与字之间的关系。PDF 里没有“段落”这个东西,只有一行行带坐标的文本。抽取器按坐标猜阅读顺序;猜错的时候,正文读起来仍然通顺,只是顺序错了——这种错误不会在字符数上暴露。
双栏文档是最典型的例子。按 y 坐标从上往下扫,会把左栏第一行、右栏第一行、左栏第二行……交替拼在一起。人眼一看就知道不对,检索器不知道。 公开的版面分析基准(DocLayNet、PubLayNet)之所以要单独标注阅读顺序,就是因为这个问题靠字符流本身解决不了(引用 [1]、[2])。
本书的语料全是单栏,没有做双栏实测;这一点写在 corpus/GAPS.md 里,不在正文里假装测过。
表格:最容易“看起来抽到了”
环境影响报告表里有 52 张表、2768 个单元格(实测,见 benchmarks/rag/parse.csv)。数字看着不少,但拆开看:
| 指标 | 数值 | 含义 |
|---|---|---|
| 空单元格占比 | 45.8% | 合并单元格被拆成了空格 |
| 单元格内含换行 | 22.0% | 一个格被拆成多行文本 |
| 小数点可疑的单元格 | 158 个 | 如 01989.,原本应是 0.1989 |
前两项还能靠后处理合并;第三项是硬伤:小数点被搬到了数字末尾,pymupdf 的表格抽取按几何位置还原,位置一偏,数值就错了。
拿两张人工转录的小表做对照,差别很清楚:
- 3×3 的“公用工程消耗”表,单元格精确率 100%(9/9);
- 4×5 的“风险物质储存量”表,精确率 70%(14/20),错的全是带小数的量。
(实测,见 benchmarks/rag/parse-ground-truth.json)
这不是 pymupdf 一家的问题。表格识别要么靠规则(线框、对齐),要么靠模型(TableBank 这类检测与识别基准就是为此而生,引用 [3])。 工程上的稳妥做法是:表格不切、整块保留,并在块上标 table 类型;数值型问题让模型看着原表回答,而不是指望检索命中某个单元格。
OCR:没有文本层时的唯一出路
扫描件的处理链是:页面渲染成图 → OCR → 带坐标的文本 → 再走上面的流程。命令本身不复杂:
bash
# 单页渲染 + 中文 OCR(需要 poppler 的 pdftoppm、tesseract 与 chi_sim 语言包)
pdftoppm -f 4 -l 4 -r 300 -png scan.pdf /tmp/page
tesseract /tmp/page-04.png stdout -l chi_sim --psm 6真正难的是验收:OCR 的错字率随扫描质量、字体、表格线变化很大,而且错字会原样进入向量库。 一份 20 页的扫描件,抽完不抽样对照原文,就不知道它是 2% 错还是 20% 错。
本书没有做 OCR 实测:已登记的扫描件样本 acc-meizhu-0116 没有下载,本机也没有 chi_sim 语言包。 5.2 的 OCR 部分只给方法与命令,不给数字。补这一步的方法写在「动手实验」里。
解析结果要带什么元数据
切块之前,每个块至少要带四样东西,否则后面 5.9 的权限、5.8 的出处都没法做:
doc_id:哪份文件;page:第几页(PDF)或条款号(法规);section:章节 / 标题路径;type:正文 / 表格 / 图注。
页码不是装饰。用户问“这条规定在哪一页”,模型只能回答到块,页码得由检索结果带出来。 条款号同理:法规类文档的引用粒度是“第 X 条”,不是“某个 400 字的块”。
工程含义
解析质量要抽样验收,不能只看字符数。 抽 10 页,人工对照:段落顺序对不对、表格数值对不对、页眉页脚有没有混进来。字符数只能证明“有字”,证明不了“有序”。
表格与正文分开处理。 表格整块入库、单独标注类型;正文按条款/标题切。混在一起切,最容易被切坏的就是表格。
扫描件要么做 OCR 并验收,要么明确不支持。 最糟的是“看起来支持”:OCR 出一堆错字,检索命中率低,用户以为是没有资料。
版面分析是独立的一层。 字符流 → 版面块 → 阅读顺序 → 语义块,每一步都可能出错;把每一步的输出落盘,出错时才知道是哪一层的问题。
动手实验
动手实验:补上双栏与 OCR 两块空白
假设:一份双栏论文用当前的单栏顺序抽取,会出现阅读顺序错乱;OCR 的中文字错率会随分辨率明显变化。
做法:
- 找一份公开的双栏论文 PDF(只放本地,不进仓库),用
pymupdf分别按get_text("blocks")与按栏切分抽取,人工比对前两页的段落顺序; - 下载
acc-meizhu-0116扫描件,安装chi_sim,用 200/300/400 dpi 各跑一遍tesseract,人工转录一页做参照; - 把结果补进
benchmarks/rag/parse.csv,并在corpus/GAPS.md里销掉对应缺口。
记录:阅读顺序错误数、图注归属错误数、字错率(CER)、耗时。注意:这两项都是本机单样本结论,不能外推成“所有双栏/扫描件都这样”。
参考资料
- Pfitzmann, B. et al. DocLayNet: A Large Human-Annotated Dataset for Document-Layout Analysis(版面类别与阅读顺序标注,KDD 2022). https://arxiv.org/abs/2206.01062
- Zhong, X. et al. PubLayNet: largest dataset ever for document layout analysis(文档版面分析基准). https://arxiv.org/abs/1908.07836
- Li, M. et al. TableBank: A Benchmark Dataset for Table Detection and Recognition(表格检测与识别基准,LREC 2020). https://arxiv.org/abs/1903.01949
- PyMuPDF 项目. Documentation: Text extraction and table detection(
get_text、find_tables的行为与限制). https://pymupdf.readthedocs.io/