去年在给一家金融机构做企业知识库的时候,我碰到一个特别典型的场景:投研部门丢过来两千多份基金合同和财报 PDF,合同里全是扫描签章页,财报里满是数据表格和折线图。当时我们天真地以为 PDF 解析就是把文字抽出来,结果第一版检索效果惨不忍睹——用户搜“2023 年管理费用率”,系统只返回半行残缺数字;搜“营收增速”,检索结果直接对不上。后来复盘才意识到,RAG 管线的实际瓶颈根本不在向量检索,也不在生成模型,而在最前面的图文解析环节。这篇是“RAG 数据导入与解析全攻略”系列的第二篇,专门讲图文类文档和 PDF 的解析,重点拆 OCR、多模态大模型,以及九种 PDF 工具怎么选、怎么配合。
1. 别再让“文档接入”拖垮 RAG 检索:图文解析究竟卡在哪里
1.1 被低估的第一公里
RAG 的标准链路是:接入解析 → 清洗切分 → 向量化 → 检索 → 生成。绝大多数团队会把注意力放在向量模型、召回策略和生成提示词上,但真正决定检索上限的,是链路最前端的“接入解析”。你可以用最好的 embedding、最强的生成模型,但只要解析阶段把一个数字从“3.5%”识别成“3.8%”,或者把表格拆得七零八落,后面所有环节都在为一个错误前提工作。
图文类 PDF 是重灾区。我归纳过知识库场景里最常见的三类难处理文档:
- 扫描件 PDF:合同、公告、历史档案,本质是一张张图片,没有文字层,传统抽取直接给空白。
- 带表格和图片的 PDF:财报、研报、尽调报告,大量数据以表格或图表形式存在,普通文本抽取会把表格内容按阅读顺序切成碎片。
- 复杂版面 PDF:双栏论文、带页眉页脚水印的正式文件、图文混排的宣传册,解析结果排列混乱,切分后检索命中率极低。
这件事被低估的另一个原因是“看起来不难”。随便装一个 PyPDF2 就能跑出文字,给人“解析已经搞定”的错觉。等知识库上线、用户开始提真实问题时,才发现检索结果糊得没法看。我的建议是:任何 RAG 项目,在上向量化之前,都应该先把解析质量测试做了,而不是先跑通 Demo 就欢呼。
1.2 解析质量到底怎么量化
在项目里,不要用“能不能抽出文字”来评估解析,要用三个指标:
- 字符级召回率:原始文档中的文本,有多少被正确提取出来。对扫描件来说,这就是 OCR 的识别准确率。
- 版面还原度:标题、段落、列表、表格单元格的相对顺序是否保持,双栏是否被错误地左右穿插。
- 表格结构化精度:表格的行列关系是否完整,跨页表是否合并,合并单元格是否丢信息。
我们当时抽样了 20 份真实文档做人工比对,发现纯文本抽取对扫描件召回率几乎为 0,对带复杂表格的 PDF,表格结构化精度不足 40%。也就是说,2000 份文档里,可能有近一半的关键数据在检索系统里是“看不见”的。这直接推翻了“向量模型不行”的结论——问题出在入口。
2. OCR 管线拆解:从传统开源引擎到深度学习实战
2.1 OCR 到底在干什么
OCR(光学字符识别)的本质,是把图像里的文字变成可检索的文本。但放到 RAG 场景里,OCR 的目标不只是“认出字符”,还要输出每个文字块的位置坐标和置信度,这样才能做版面重建、表格还原和后续切分。
一个完整的 OCR 管线通常包含四步:
- 文本检测:找到图片里哪些区域有文字,给出一系列文本框。
- 方向分类:判断文本是正向、倒置还是旋转,做纠正。
- 文字识别:把文本框里的图像内容转换为字符串。
- 结构化输出:把文本和坐标组合起来,还原段落和表格结构。
传统方案以 Tesseract 为代表,它做英文和印刷体还行,遇到中文、低分辨率扫描件、印章压字,效果就很不稳定。深度学习方案里,PaddleOCR 是我在中文场景用得最顺手的,它对中文支持、复杂版面鲁棒性、表格识别都有明显优势,而且完全开源、可本地部署,和 RAG 的隐私要求很搭。
2.2 用 PaddleOCR 把扫描 PDF 变成干净文本
这里给一套可以直接复用的流程:先用 PyMuPDF 把 PDF 的每一页渲染成高分辨率图片,再用 PaddleOCR 识别,最后按坐标拼接成文本并保留结构化信息。
import fitz from paddleocr import PaddleOCR # 1. 初始化 OCR,中英文都开 ocr = PaddleOCR(use_angle_cls=True, lang="ch", show_log=False) # 2. 把 PDF 逐页渲染成图片 def pdf_to_images(pdf_path, dpi=200): doc = fitz.open(pdf_path) images = [] for page in doc: mat = fitz.Matrix(dpi / 72, dpi / 72) pix = page.get_pixmap(matrix=mat) img_bytes = pix.tobytes("png") images.append(img_bytes) return images # 3. 逐页识别并拼接 def ocr_pdf(pdf_path): images = pdf_to_images(pdf_path) full_text = [] for img_bytes in images: result = ocr.ocr(img_bytes, cls=True) # result 里每个元素包含坐标、文本、置信度 page_lines = [] for line in result[0]: box = line[0] # 四个角的坐标 text = line[1][0] conf = line[1][1] page_lines.append({"box": box, "text": text, "conf": conf}) # 按 y 坐标排序,保证阅读顺序 page_lines.sort(key=lambda x: (x["box"][0][1], x["box"][0][0])) full_text.append("".join([l["text"] for l in page_lines])) return "\n".join(full_text)几个关键参数的经验值:
- DPI 选 200 就够。150 会明显掉准确率,300 以上对识别率提升有限,但速度成倍下降。如果文档本身是 300 DPI 扫描的,渲染就用 300,不要降采样。
- use_angle_cls=True 必须开。扫描件经常有页面旋转情况,方向分类能少很多无谓的识别错误。
- lang 按文档语言选:中文选
"ch",英文选"en",中英混排可以"ch"加上英文支持。 - 输出一定要保留坐标。后面做双栏拆分、版面还原、表格区域识别,都依赖坐标信息。只拿纯文本等于把最有价值的信息丢了。
2.3 表格和版面:OCR 进阶玩法
只做文字识别,对纯文字段落还行,遇到表格就没戏了。表格问题在于:单元格的顺序、行列对应关系、跨页合并,光靠文本坐标很难恢复。PaddleOCR 官方有 PP-Structure 系列模型,能同时做版面分析、表格识别、公式识别。它会输出表格的 HTML 结构,这个结果非常适合 RAG——直接转成 Markdown 表格存入向量库,切分和召回都会友好很多。
版面分析同样重要。一张财报页面可能是“标题 + 一段文字 + 表格 + 图注”的混合体,版面分析会把页面划分成不同区块,你再根据区块类型决定处理策略:文字区块直接抽取,表格区块走表格识别,图片区块要么 OCR 图内文字、要么交给多模态大模型做语义描述。这样做出来的解析结果,切分后的每个 chunk 才足够“内聚”,检索命中率才会高。
3. 多模态大模型的真实定位:OCR 摘“数字”,大模型补“语义”
3.1 多模态大模型能做什么、不能做什么
现在很多人一提到解析 PDF 就想到“交给多模态大模型直接读”,这个思路对一半、错一半。多模态大模型(比如 GPT-4o、Qwen-VL、GLM-4V 这类)确实能“看懂”图表和复杂版面,但直接把它当批量 OCR 工具用,既贵又慢还不稳定。
我理解的多模态大模型在图文解析里的定位,是语义补充者,不是事实提取者。具体来说:
- 能做:理解图表的趋势和结论、概括图片表达的核心意思、把非结构化图表转成一段结构化描述、对扫描模糊区域做语义推测。
- 不能做:保证百分百无幻觉、保证数字精确无误、在成本和速度上做大规模批处理。
OCR 的强项是“高精度地提取事实”,弱点是“没有语义理解”。大模型的强项是“语义理解”,弱点恰恰是“可能编造事实”。两者是互补关系,不是替代关系。
3.2 一个高效的配合范式:OCR 提事实 + 多模态做摘要
我在项目中验证过一套性价比很高的配合方式,原则很简单:普通文字和简单表格全部走 OCR,只把图表、复杂图片、扫描模糊区域送给多模态大模型。
具体步骤:
- 用 OCR 跑完整文档,提取所有文字和表格。
- 版面分析时,把“图片”“图表”类型区块单独切出来。
- 把这些区块的图片裁剪出来,送到多模态大模型,提示词要求“描述这张图表展示的核心信息和主要趋势,不超过 100 字”。
- 把大模型生成的描述,和 OCR 提取的文字一起入库。
举个例子,一份基金年报里的“投资策略”页,可能是一张散点图加几段文字。OCR 能提取坐标轴标签、图例文字,但这些零散文字没法回答“分散度如何变化”这种语义问题。多模态模型补一句“该图表显示组合在 2023 年的行业集中度显著下降”,检索系统就能用这句描述命中用户的真实意图了。
成本控制技巧:如果一份 20 页的 PDF 只有 3 张关键图表,不要整本丢给大模型,只裁剪那 3 张。我们实测下来,单份文档的多模态花销能控制在几分钱量级,而效果比整本识别好得多——至少数字没被模型“编错”。
3.3 判断规则:什么时候该上多模态
我给自己定过一个简单的决策顺序,可以分享:
- 文档有文本层?直接抽文本,别折腾 OCR,更别浪费大模型。文字层的抽取又快又准。
- 无文本层但内容以印刷体文字为主?走 OCR。
- OCR 识别率低、或文档里有大量图表需要理解语义?对失败区块用多模态大模型做补充描述。
- 只偶尔依赖多模态,绝不让它做全量解析。
4. 九种 PDF 工具选型:我按类型拆开试了一遍
针对 PDF 解析,市面上的工具早就超过九种,真正好用的其实是不同定位的组合。我把常用工具按功能类型拆开,逐个说实测感受和选型建议。
4.1 文本抽取系:pypdf、pdfplumber、PyMuPDF、PDFMiner.six
pypdf(原 PyPDF2):轻量级,适合文本层完好的普通 PDF。它的优点是简单、依赖少,缺点是版面信息基本丢弃,合并旋转过的页面时顺序容易乱。适合“只要内容、不要排版”的场景。
pdfplumber:文本抽取里我优先级最高的工具。它基于 PDFMiner,但把字符坐标、线条、矩形都暴露出来了,可以精确提取表格单元格。实测中,对规则表格的提取效果很好,尤其是带边框的财务表格。缺点是复杂嵌套表格处理吃力,速度也不算快。
PyMuPDF(fitz):性能怪兽。PDF 渲染成图片、抽文本、取坐标、拿书签,它都能干,速度领先其他库一个量级。我的习惯是用它做“预检”:快速判断 PDF 有没有文本层,有文本层就用它抽,没有文本层就渲染成图给 OCR。一个两千页的 PDF,用 PyMuPDF 抽文本层耗时按秒算,其他工具动辄几分钟。
PDFMiner.six:pdfplumber 的底层依赖,定位偏底层。如果只是项目使用,建议直接上 pdfplumber,不需要自己操作 PDFMiner。
4.2 图像与 OCR 系:pdf2image、Tesseract、PaddleOCR
pdf2image:和名字一样,它的作用就是把 PDF 转成 PIL/Pillow 图像。它本身不做识别,是 OCR 流程的关键中转站。需要注意两点:一是底层依赖 poppler,macOS 上用brew install poppler,Linux 用apt-get install poppler-utils;二是渲染质量受 DPI 影响,200 DPI 起步。
Tesseract:老牌 OCR 引擎,胜在历史悠久、多语言支持广。但它对中文复杂版面的识别率明显低于现代深度学习方案,遇到模糊扫描件就很挣扎。除非项目里全是质量极高的英文印刷体,否则我不建议在 RAG 管线里用它做主力。
PaddleOCR:前面单独介绍过。中文场景、扫描件、复杂版面、表格,综合实力最强,而且有 PP-Structure 能输出版面分析和表格 HTML。我把它作为图文解析的主力 OCR 引擎。
4.3 表格与结构化系:Camelot、Tabula、Unstructured
Camelot:专注表格提取,能根据页面线条把表格还原成 DataFrame 或 CSV,还带可视化调参工具camelot-py的 GUI。优点是对有线框表格还原度极高;缺点是依赖 ghostscript 和 OpenCV,安装容易踩坑,对无线框表格基本无能为力。
Tabula:也是表格提取,比 Camelot 轻量,原生支持导出 CSV/JSON。但底层是 PDFBox,对中文的兼容性和复杂表头的处理不如 Camelot。适合快速处理英文 PDF 的简单表格。
Unstructured:这是目前做 RAG 文档解析我个人最看好的库。它把 PDF、Word、HTML、图片统一成一个解析流程,能输出按区块划分的结构化元素列表,每个元素带类型(标题、段落、表格、图片)和元信息,天然适合下游切分和向量化。它的partition_pdf支持从 pdfminer 到 OCR、再到多模态 API 的多策略。缺点是重,依赖多,首次跑通需要一点耐心。
4.4 商业云服务系:百度/腾讯/阿里 OCR、TextIn 类平台
如果文档量巨大、团队人力有限、预算也够,商业 OCR 云服务是“花钱买省心”的选择。百度、腾讯、阿里都有自己的 OCR API,对营业执照、合同、财务报表等高频场景有专门的识别模板;TextIn 这类专业文档解析平台则在 PDF 版面分析、表格还原上做得更细。它们的共同优势是准确率高、免运维,劣势是数据出域,对强合规企业可能不友好。我处理金融客户时,通常优先考虑本地化方案,只有在客户明确允许上云时才用商业服务。
4.5 九种工具对比表格
| 工具 | 类型 | 核心优势 | 主要硬伤 | 适合场景 |
|---|---|---|---|---|
| pypdf | 文本抽取 | 轻量、简单 | 丢失版面信息 | 文字层完好的普通 PDF |
| pdfplumber | 文本/表格 | 坐标精确、表格提取强 | 速度慢、复杂嵌套吃力 | 规则表格、需要精确定位的场景 |
| PyMuPDF | 全能 | 速度快、渲染强、预检友好 | 自带文本抽取对复杂版面一般 | 预检、渲染、大批量快速处理 |
| PDFMiner.six | 底层解析 | 可定制性强 | 上手复杂 | 需要底层定制时 |
| pdf2image | 图像转换 | 简单、可靠 | 依赖 poppler | OCR 前处理 |
| PaddleOCR | OCR/结构 | 中文强、表格/版面一体化 | 依赖较重、安装略复杂 | 中文扫描件、复杂版面 |
| Camelot | 表格提取 | 表格还原度高 | 依赖多、无线框表格弱 | 有线框财务表格 |
| Tabula | 表格提取 | 轻量、上手快 | 中文兼容一般 | 英文简单表格 |
| Unstructured | 文档解析 | 区块化输出、多策略 | 重、依赖多 | RAG 文档预处理首选 |
4.6 选型决策树
给出我实际推荐的组合思路:
- 有文本层、排版简单:直接用 PyMuPDF 抽文本。
- 有文本层、表格多:PyMuPDF 预检 + pdfplumber 抽文字和表格。
- 无文本层、扫描件:PyMuPDF 渲染 200 DPI + PaddleOCR 识别 + PP-Structure 做版面表格。
- 文档格式杂、要做 RAG 预处理:Unstructured 走全流程,表格区块再交给 Camelot 或 PaddleOCR 补强。
- 量巨大、可上云、合规允许:商业云 OCR 做主力,本地工具做兜底。
5. 一条可直接复用的图文解析工作流(附避坑清单)
5.1 端到端解析流程
把前面的工具串起来,我目前在生产环境使用的流程是这样的:
- 预检:用 PyMuPDF 读取 PDF,判断每页是否有文本层,统计无文本层页面占比。
- 分流:有文本层页面走文本抽取+表格提取;无文本层页面标记为 OCR 页。
- 渲染:对 OCR 页用 PyMuPDF 渲染成 200 DPI 图片。
- OCR:PaddleOCR 识别文字,输出带坐标的 JSON。
- 版面分析:用 PP-Structure 对页面分块,区分标题、段落、表格、图片。
- 表格结构化:表格区块转成 Markdown 表格;同时用 Camelot 对规则表格做二次校验。
- 多模态补充:图片/图表区块裁剪后送多模态大模型,生成语义描述。
- 清洗:去掉页眉页脚、页码、水印、目录重复项,统一编码。
- 输出:按“页面 + 区块类型 + 文本内容”的结构化格式落库,交给切分和向量化。
这套流程的投入产出比很高,两千页文档,单机上跑,耗时主要在 OCR 环节,大约每分钟处理 3-5 页,属于可接受的离线预处理范畴。
5.2 几个踩过的坑
- 双层 PDF 的陷阱:有些扫描 PDF 其实是“扫描图 + 隐藏文字层”,直接抽文本层会得到顺序颠倒、乱码频出的内容。预检时如果发现文字层内容明显异常,要单独走 OCR,而不是信任文字层。
- 双栏版面被横向切开:OCR 按坐标排序时,如果按 x 轴排序,左右两栏会被穿插成一个句子。正确做法是先按 y 轴找行,再判断页面上是否存在明显分栏结构,分栏时按“从上到下、先左栏后右栏”的顺序重组。
- 图注和表注被当噪音清理:清洗环节很容易把“图 1:营收结构变化”这类文本当成页眉或水印删掉。但恰恰是这些注记,和图表内容绑定紧密,是用户提问时的高频关键词。清洗策略要保留任何带“图、表、注”字样的短文本。
- 跨页表格断行:一个表格从上一页延续到下一页,table 识别会变成两个表。处理办法是按表格的表头结构做合并,或至少在切分时避免把跨页表格断成两个 chunk。
- CID 字体乱码:部分 PDF 使用自定义 CID 字体映射,抽出来的全是符号。遇到这种情况,没有捷径,只能渲染成图片走 OCR。
5.3 怎么验证这套流程到底行不行
不要拍脑袋说“看起来挺好的”。我在每次调整解析策略后,都会做一次标准评测:
- 抽 20 份有代表性的文档,建立一份“标准答案集”:每份标注原文的关键段落、表格数据、图表结论。
- 跑完解析流程后,用字符召回率、表格结构化精度、区块顺序正确率三个指标打分。
- 再用 20 条真实用户 query 做端到端回测,看检索结果是否命中对应 chunk。
只有这轮评测通过了,才进入批量处理和上线。这个习惯帮我避开了很多“解析改完了但检索变差了”的无效迭代。
最后分享一个实际体会:图文解析没有一个工具能通吃所有场景,它更像一道组合题。我见过不少项目在工具选型上花了大量时间纠结,而真正该花时间的是把“预检、OCR、语义补充、清洗、验证”这条链路跑通,并且保留每一份文档的中间产物。这样随时可以回溯解析问题,也能在模型和工具升级时快速重跑对比。你只需要先拿一个小样本跑通全套流程,再放量,路上踩的坑至少能少一半。