1. 为什么图文与 PDF 解析是 RAG 落地最脏最累的活
做过 RAG 项目的人都有一个共识:检索效果差,八成不是模型不行,而是数据没洗干净。尤其是当你的知识库里混进了扫描件、产品手册、带图表的技术文档、合同 PDF 之后,纯文本抽取那一套直接歇菜。我见过太多团队在 demo 阶段用几页干净的 Markdown 跑得飞起,一上生产环境导入几百份 PDF,召回率直接腰斩。
这一篇聚焦的就是 RAG 数据导入流水线里最硬的一环:图文混排内容与 PDF 的解析。核心要解决三件事——扫描件里的文字怎么捞出来(OCR)、图片里的语义信息怎么保留(多模态大模型)、市面上九种主流 PDF 解析工具到底怎么选。适合正在搭建 RAG 知识库的工程师、做文档智能的产品同学,以及被 PDF 折磨过的数据侧开发者。
先说一个反直觉的结论:PDF 不是一种格式,它是一个容器。同样后缀的.pdf,底层可能是纯文本流、可能是矢量图形、可能是整页扫描的位图、也可能是这几种的混合。你用一个工具去通吃,必然在某些文件上翻车。所以选型的前提是先搞清楚你手里的 PDF 属于哪一类,这也是后面工具选型章节的底层逻辑。
2. 先搞清楚你的 PDF 属于哪一类:三种底层结构
2.1 文本型 PDF:最理想但别高兴太早
文本型 PDF 的文字是以字符编码形式存储的,抽取工具能直接读到 Unicode。这类文件处理起来最省事,pdfplumber、PyMuPDF这类库几行代码就能拿到文字。但坑在于:排版信息是分离的。文字的位置靠坐标定位,你按流式读取拿到的顺序,未必是人类的阅读顺序。双栏论文、带侧边栏的报告,直接抽取经常出现左右栏文字交错的情况。
判断方法很简单,用pdffonts命令或者PyMuPDF读一下页面对象,如果page.get_text()返回的内容长度和肉眼看到的字数接近,基本就是文本型。如果返回一堆空白或者乱码,那就要往下面两类靠了。
2.2 扫描型 PDF:OCR 的主战场
扫描件本质是图片,每一页就是一张位图,没有任何文字层。这类文件必须走 OCR。OCR 的准确率受三个因素影响极大:分辨率、倾斜角度、字体清晰度。我实测下来,300 DPI 是个分水岭,低于 200 DPI 的中文识别错误率会明显上升。倾斜超过 5 度,行分割就会出问题,需要先做纠偏。
这里有个经验:很多扫描件是手机拍的,带透视变形。直接丢给 OCR 引擎,识别率惨不忍睹。正确做法是先做透视校正 + 二值化,把图片"摆正"再识别。OpenCV 的warpPerspective配合轮廓检测能解决大部分场景。
2.3 混合型 PDF:最容易被低估的麻烦
混合型就是同一份文档里,有的页是文本,有的页是扫描图,甚至同一页里既有文字又有嵌入的图片。技术手册、财报、学术论文大量属于这一类。处理策略必须是逐页判断类型,分流处理,而不是整份文件一刀切。我一般会写一个页面分类器:先用PyMuPDF尝试抽取文字,如果某页文字量低于阈值(比如少于 50 个字符),就判定为图片页,转交 OCR 或视觉模型处理。
提示:不要用整份文件的平均文字密度来判断类型,一定要逐页判断。我踩过这个坑,一份 200 页的手册前 50 页是文本、后面全是扫描图,整份判断直接误判成文本型,后面 150 页全部丢失。
3. OCR 方案怎么选:从传统引擎到多模态大模型
3.1 传统 OCR 引擎的适用边界
传统 OCR 引擎(Tesseract、PaddleOCR、以及各家云服务)本质是"检测 + 识别"两阶段流水线:先定位文字框,再逐框识别字符。它们的优势是快、便宜、可控,适合大批量、版式规整的文档。
Tesseract 是老牌选手,中文需要单独下载语言包(chi_sim),实测对印刷体还行,但对手写体和复杂版式基本没辙。PaddleOCR 在中文场景下明显更强,尤其是竖排文字和表格识别,PP-Structure 模块能直接输出表格结构。云服务 OCR(百度、腾讯等)准确率最高,但涉及数据出域和调用成本,敏感文档慎用。
选型时有个容易被忽略的点:OCR 语言包和实际内容要匹配。我见过有人用默认的英文包去识别中文文档,结果全是乱码还找不到原因。PaddleOCR 初始化时一定要显式指定lang='ch',Tesseract 要确认chi_sim.traineddata已正确安装。
3.2 多模态大模型:图文理解的新解法
传统 OCR 只给你文字,丢掉了版面、图表、公式的语义。而多模态大模型(如 GPT-4V 类、Qwen-VL 类)可以直接"看图说话",把一整页 PDF 截图丢进去,让它输出结构化的 Markdown,连表格和公式都能保留。这对 RAG 来说价值巨大,因为图表的语义信息恰恰是传统 OCR 丢失的部分。
但多模态大模型不是银弹。三个现实问题:一是成本,按页计费,几百页文档跑下来费用不低;二是幻觉,模型可能"脑补"出原文没有的内容,这在知识库场景是致命的;三是速度,比传统 OCR 慢一个数量级。我的建议是分级处理:普通文本页走传统 OCR,含图表、公式、复杂版式的关键页才调用多模态模型,用成本换质量。
3.3 一个实用的混合 OCR 流水线
结合上面的分析,我常用的流水线是这样的:
- 用
PyMuPDF逐页判断类型,文本页直接抽取; - 图片页先做图像预处理(纠偏、去噪、二值化);
- 规整的印刷体页面走 PaddleOCR 批量识别;
- 含表格、公式、图表的页面截图后送多模态模型,要求输出 Markdown;
- 所有结果统一做后处理(去重、段落合并、乱码过滤)。
这套流程在保证质量的同时把成本压到了可接受范围。关键在第 4 步的 prompt 设计,要明确要求模型"只输出图中可见内容,不要补充解释",能显著降低幻觉。
4. 九种 PDF 解析工具横向选型
下面这张表是我在实际项目中反复对比后的结论,覆盖了从轻量库到重型框架的主流选择。选型没有绝对优劣,关键看你的文档类型、吞吐量和质量要求。
| 工具 | 类型 | 强项 | 短板 | 适用场景 |
|---|---|---|---|---|
| PyMuPDF | 库 | 速度快、API 简洁、能读文字和图片 | 复杂版式顺序乱 | 文本型 PDF 批量抽取 |
| pdfplumber | 库 | 表格抽取精准、能拿坐标 | 速度偏慢 | 结构化表格文档 |
| pdfminer.six | 库 | 底层控制细、纯 Python | 慢、API 繁琐 | 需要精细控制的场景 |
| PaddleOCR | OCR | 中文强、表格识别好 | 需 GPU 才快 | 中文扫描件 |
| Tesseract | OCR | 免费、多语言 | 中文一般、版式差 | 英文规整文档 |
| Unstructured | 框架 | 一站式、支持多格式 | 依赖重、调参多 | 快速搭原型 |
| Marker | 框架 | PDF 转 Markdown 质量高 | 吃显存 | 学术论文、技术文档 |
| MinerU | 框架 | 公式表格识别强 | 部署稍复杂 | 科研文献 |
| 多模态大模型 | 模型 | 图文语义全保留 | 贵、有幻觉 | 关键复杂页面 |
4.1 轻量库派:PyMuPDF 与 pdfplumber 的分工
PyMuPDF(导入名fitz)是我处理文本型 PDF 的首选,速度极快,一份几百页的文档几秒钟就能抽完。它的get_text("blocks")能返回带坐标的文字块,方便你做版面重排。缺点是遇到多栏排版时,块的顺序需要自己按坐标排序。
pdfplumber的杀手锏是表格。它的extract_tables()能基于线条和文字对齐关系还原表格结构,对财报、报表类文档特别有用。代价是速度慢,我一般只对确认含表格的页面调用它,而不是整份文档跑。
这两个库配合使用的思路是:PyMuPDF 负责快速分流和文字抽取,pdfplumber 负责表格页的精细处理。
4.2 框架派:Unstructured、Marker、MinerU 怎么挑
Unstructured的定位是"文档 ETL 全家桶",PDF、Word、PPT、HTML 都能吃,输出统一的元素列表(标题、正文、表格、图片)。优点是省心,缺点是依赖重、参数多,而且默认配置下的 PDF 解析质量一般,需要针对性地调strategy参数。
Marker专注 PDF 转 Markdown,对学术论文的处理质量很高,公式和引用都能较好保留。它底层也用了深度学习模型,所以需要 GPU,显存占用不小。
MinerU是国内团队做的,对中文科研文献的公式、表格识别效果突出,支持扫描件和文本件混合处理。部署比 Marker 稍复杂,但中文场景下我更倾向它。
选这三个框架的逻辑是:英文技术文档选 Marker,中文科研文献选 MinerU,多格式混杂的通用场景选 Unstructured。
4.3 选型决策树:三步锁定你的方案
第一步,看文档类型。纯文本型且版式简单,PyMuPDF 足够;含大量表格,加 pdfplumber;扫描件为主,直接上 OCR 或框架。
第二步,看语言和领域。中文为主优先 PaddleOCR 和 MinerU;英文为主 Tesseract 和 Marker 都能打。
第三步,看吞吐和质量要求。日处理量上千份、质量要求中等,走传统 OCR 流水线;日处理量小但要求高(比如法律、医疗文档),关键页上多模态模型。
注意:不要一上来就堆最重的方案。我见过团队直接上多模态模型处理所有页面,成本爆炸不说,幻觉引入的错误比 OCR 还难排查。先用轻量方案跑通,再针对薄弱环节加码。
5. 实操:搭一条可复现的图文 PDF 解析流水线
5.1 环境准备与依赖安装
先把基础环境搭起来。我习惯用 conda 隔离环境,避免依赖冲突。
conda create -n rag-parse python=3.10 -y conda activate rag-parse pip install pymupdf pdfplumber paddleocr paddlepaddle opencv-python pillow如果要用 Marker 或 MinerU,单独建环境,因为它们对 torch 版本有要求,混装容易出问题。PaddleOCR 首次运行会自动下载模型权重,国内网络建议提前配好镜像源。
5.2 逐页类型判断与分流
这是整条流水线的调度核心。核心逻辑是逐页尝试抽取文字,按文字量分流。
import fitz def classify_page(page, text_threshold=50): text = page.get_text().strip() if len(text) >= text_threshold: return "text", text else: return "image", None def process_pdf(pdf_path): doc = fitz.open(pdf_path) results = [] for page_num, page in enumerate(doc): page_type, text = classify_page(page) if page_type == "text": results.append({"page": page_num, "type": "text", "content": text}) else: # 渲染成图片,交给 OCR 或视觉模型 pix = page.get_pixmap(dpi=300) img_path = f"/tmp/page_{page_num}.png" pix.save(img_path) results.append({"page": page_num, "type": "image", "path": img_path}) return results这里dpi=300不是随便定的。低于 200 时 OCR 对中文小字的识别率明显下降,高于 400 则文件体积和耗时陡增,300 是质量和效率的平衡点。
5.3 图像预处理:决定 OCR 上限的关键一步
OCR 引擎再强,喂进去一张歪的、糊的图也白搭。预处理做得好,识别率能提升十几个百分点。核心三步:灰度化、去噪、纠偏。
import cv2 import numpy as np def preprocess_image(img_path): img = cv2.imread(img_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 自适应二值化,比全局阈值更抗光照不均 binary = cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 15, 8 ) # 基于最小外接矩形做纠偏 coords = np.column_stack(np.where(binary < 128)) if len(coords) > 100: angle = cv2.minAreaRect(coords)[-1] if angle < -45: angle = 90 + angle if abs(angle) > 0.5: h, w = binary.shape M = cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1.0) binary = cv2.warpAffine(binary, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE) return binaryadaptiveThreshold的参数15和8是经验值,块大小 15 适合常规字号,偏移 8 能有效抑制背景噪点。如果你的文档字特别小,块大小要相应调小。
5.4 OCR 识别与结果后处理
预处理完就交给 PaddleOCR。注意初始化时显式指定中文。
from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=True, lang='ch', show_log=False) def run_ocr(img): result = ocr.ocr(img, cls=True) lines = [] for line in result[0]: text, conf = line[1][0], line[1][1] if conf > 0.6: # 置信度过滤,低于阈值的丢弃 lines.append(text) return "\n".join(lines)置信度阈值0.6是我反复调出来的。设太高会丢内容,设太低会引入乱码。对于关键文档,可以把阈值提到 0.8,宁可漏也别错。
后处理阶段要做三件事:合并被错误切分的段落、过滤纯符号行、修复常见 OCR 错字。最后一项可以维护一个领域词典做替换,比如把"曰期"修正为"日期"。
5.5 多模态模型处理复杂页面
对于含表格、公式、图表的页面,把预处理后的图片送多模态模型,prompt 要写得克制。
def parse_complex_page(img_path, client): with open(img_path, "rb") as f: img_data = f.read() prompt = ( "请将这张文档图片转换为 Markdown 格式。" "要求:1) 只输出图片中可见的内容,不要补充或解释;" "2) 表格用 Markdown 表格表示;" "3) 公式用 LaTeX 表示;" "4) 无法识别的内容用 [模糊] 标注。" ) response = client.vision(img_data, prompt) return responseprompt 里"只输出可见内容"这句至关重要,能大幅降低幻觉。另外要求对模糊内容显式标注,方便后续人工复核。
6. 常见问题与排查技巧实录
6.1 OCR 识别乱码或漏字怎么排查
这是最高频的问题。排查顺序建议从图像质量开始,而不是怀疑引擎。先看预处理后的图是否清晰、是否摆正,再看语言包是否匹配,最后才调引擎参数。我整理了一张速查表:
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| 整页乱码 | 语言包不匹配 | 确认 lang 参数与内容语言一致 |
| 部分漏字 | 置信度阈值过高 | 下调阈值到 0.5 复核 |
| 文字错位 | 未纠偏 | 加图像纠偏预处理 |
| 表格串行 | 未做版面分析 | 改用表格专用识别模块 |
| 公式变乱码 | OCR 不支持公式 | 转多模态模型处理 |
6.2 多模态模型幻觉怎么压制
幻觉的典型表现是模型"补全"了原文没有的句子,或者把表格数据算错。压制手段有三个:一是 prompt 明确禁止补充;二是对输出做交叉验证,用传统 OCR 的结果做比对,差异过大的段落标记出来人工复核;三是降低 temperature 参数,让输出更确定。
6.3 大文件处理的内存与速度优化
几百页的 PDF 一次性加载容易爆内存。我的做法是流式处理:用fitz逐页打开,处理完一页释放一页,图片临时文件及时删除。OCR 部分可以开多进程,但要注意 PaddleOCR 的模型加载有开销,进程池要复用而不是每页新建。
提示:处理大批量文档时,一定要加断点续传。我吃过亏,跑到第 800 页程序崩了,前面全白跑。把每页结果落盘,重跑时跳过已完成的页面。
6.4 解析结果如何衔接后续的切分与向量化
解析只是第一步,输出格式直接影响后续切分质量。我的经验是:解析阶段就保留结构信息,比如标题层级、表格边界、图片位置,用带标记的 Markdown 输出。这样切分时可以按标题切、按语义切,而不是傻乎乎地按固定字数切。表格要整体保留,绝不能从中间切断,否则检索出来的片段毫无意义。
7. 我在实际项目里踩过的几个坑
第一个坑是过度依赖单一工具。早期我用 PyMuPDF 通吃所有 PDF,结果遇到扫描件全军覆没。后来才建立起"先分类、再分流"的思路,这是整个流水线设计的转折点。
第二个坑是忽视图像预处理。有段时间 OCR 准确率死活上不去,换了好几个引擎都没用,最后发现是扫描件本身有 3 度倾斜,加了纠偏后准确率直接涨了 15 个百分点。引擎不是万能的,喂好数据才是根本。
第三个坑是多模态模型用得太随意。一开始图省事把所有页面都丢给视觉模型,成本高不说,还引入了不少幻觉,检索时经常召回一些原文根本没有的内容。后来改成只对复杂页面调用,并且加了交叉验证,问题才解决。
最后一个体会是关于评估。解析质量不能靠肉眼看几页就下结论,要建立量化指标:字符准确率、表格还原率、关键字段召回率。我一般会人工标注 20 页作为测试集,每次调整流水线都跑一遍,用数据说话。这套评估机制建立起来之后,优化方向就清晰多了,不再靠感觉调参。
如果后续还要扩展,我会在解析层和切分层之间加一个质量打分模块,对每页的解析结果给出置信度,低置信度的页面自动进入人工复核队列。这样既保证了知识库的整体质量,又不会让人工审核成为瓶颈。