最近帮朋友排查一个 RAG 项目,他吐槽检索质量差得离谱:问“合同金额是多少”,返回的是整页合同扫描件里的杂乱文本,关键数字全没召回到。我把他的导入链路看了遍,问题就出在 PDF 解析和图文处理这层——原始 PDF 文件直接往向量库里塞,别说图文混排,连排版层级都是乱的。
做 RAG 数据导入与解析,最难啃的骨头从来不是文本提取,而是图片、扫描件、表格和复杂版面。这篇是“数据导入与解析全攻略”的第二篇,专门梳理图文与 PDF 解析方案,覆盖 OCR 选型、多模态大模型应用,以及我实测过的九种 PDF 解析工具。适合正在搭 RAG 知识库、或者被检索效果困扰的工程师参考。
1. 图文数据进 RAG 之前,先想清楚这三件事
1.1 RAG 知识库存的从来不是源文件
很多人踩的第一个坑,是把 PDF、图片当作“知识”直接入库。向量数据库里存的是文本经过 embedding 模型转换后的向量,检索时通过向量相似度召回文本片段,再交给大模型生成答案。如果你把二进制文件塞进去,embedding 模型根本不认识,要么报错,要么召回一堆噪音。
所以“RAG 知识库能存图片吗”这个问题的答案是:能,但存进去的不是图片本身,而是图片经过解析后生成的结构化文本和语义描述。图片文件本身可以作为附件保存在文件系统或对象存储里,知识库中只存对应的文本 chunk,并在元数据里记录文件路径。
1.2 图片入库的两条可行路径
在实际项目里,图片进入 RAG 只有两条路走得通:
- 解析成文本入库:用 OCR 提取图中文字,再用多模态大模型(或专门的字幕生成模型)生成图片内容描述,文字和描述一起作为 chunk 入库。检索“图表里说了什么”时,召回的是这段描述文本。
- 多模态向量化入库:用支持图文联合 embedding 的模型(如 CLIP 类、多模态 embedding 模型)直接对图片生成向量,检索时把用户 Query 也转成向量做匹配。这条路对基础设施要求高,召回质量不如“解析成文本”稳定,而且大模型生成答案时还是需要 readable 的文本,绕不开解析环节。
我的建议是:把“解析成文本”当作主路径,多模态向量化只作为补充索引。原因很简单——RAG 最终目的是让 LLM 基于上下文生成答案,而 LLM 读不了向量,只能读文本。
1.3 按文档类型定解析目标
PDF 和图片千差万别,解析策略必须分而治之。我在项目中一般先把文档分为三种类型:
| 文档类型 | 典型特征 | 解析目标 | 推荐工具组合 |
|---|---|---|---|
| 电子版 PDF | 文字可选中、无扫描痕迹 | 提取文本+保留结构 | PyMuPDF、pdfplumber、Unstructured |
| 扫描件/图片型 PDF | 整页就是一张图 | OCR 转文字,必要时多模态模型做版面理解 | PaddleOCR、Tesseract、云端 OCR、多模态大模型 |
| 图文混排复杂版式 | 含表格、多栏、图表注释、页眉页脚 | 版面分析+结构化提取 | Unstructured、Marker、LlamaParse、多模态大模型 |
一个常见误区是“PDF=好解析”。电子版 PDF 直接 get_text 就能拿文字,但扫描版 PDF 本质是图片,不经过 OCR 一个字都提不出来。所以拿到 PDF 先判断是文本型还是图片型,可以用 PyMuPDF 快速检测:如果页面 get_text() 返回的字符数几乎为 0,基本就是扫描件。
2. OCR 方案实测:Tesseract、PaddleOCR、云端 OCR 怎么选
OCR 是图文解析的底座。市面上的 OCR 方案说多不多,说少不少,我实际用下来主要就三类:开源本地部署、云端 API、以及多模态大模型。三类各有适用场景,下面按我的使用经验逐个拆。
2.1 Tesseract:免费开源老牌选手,中文精度要打问号
Tesseract 是老牌 OCR 引擎,优点是完全免费、支持语言多、社区成熟。你装上语言包就能识别中文,韩文、日文也有对应包。但它的短板非常明显:对复杂排版和中文识别精度都一般,尤其遇到多栏混排、表格线干扰、低分辨率图片时,输出经常是乱的。
如果你要跑 Tesseract,建议这样处理:
import pytesseract from PIL import Image # 识别前先做图像预处理,能显著提升准确率 img = Image.open("scan_page.png").convert("L") # 灰度 img = img.resize((img.width * 2, img.height * 2)) # 放大2倍 img = img.point(lambda x: 255 if x > 140 else 0) # 二值化,阈值需按实际图调试 text = pytesseract.image_to_string(img, lang="chi_sim+eng") print(text)Tesseract 有语言识别不了某个外语时,先检查是否安装了对应语言包,lang="kor"之类的是需要额外下载的。这点和 PaddleOCR 一样,默认只带中文和英文模型,换了语言得单独加载。
2.2 PaddleOCR:中文场景的性价比之王
在中文文档场景,PaddleOCR(PP-OCR 系列)目前是开源方案里准确率最高的选择之一。它自带版面方向分类、文本检测、文本识别三条流水线,还能顺便输出文本坐标。下面是最基本的使用方式:
from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=True, lang="ch") result = ocr.ocr("contract_page.png", cls=True) for line in result[0]: box, (text, confidence) = line print(f"{text}: {confidence}")要注意几个实操细节:
- 版本差异很大:老版本是
ocr.ocr()直接返回结果,新版 PP-OCRv4/v5 接口和返回结构都有变化,网上代码动不动就报错,先确认自己装的版本。 - 多语言支持需要单独配置:有网友用 PaddleOCR 识别韩文识别不出来,多半是
lang="ch"导致的,韩文要设为lang="korean"并安装对应模型。 - 表格识别要另走流水线:PP-Structure 才是做表格结构还原的组件,单纯的 PaddleOCR 只能出文字和坐标,拼不出表格行列关系。
PaddleOCR 是本地部署,GPU 可用时识别速度很快,CPU 上也可以跑但会慢不少。如果只是偶尔解析几十页,本地完全够用;如果每天处理上千页,再考虑 GPU 或换云端方案。
2.3 云端 OCR:适合快速上线和长尾需求
阿里的、百度的、腾讯的开放平台都有 OCR API,识别精度普遍高于开源方案,而且对表格、印章、手写体有专门模型,某些特定场景(如合同、身份证、发票)直接调用垂直模型,比通用 OCR 好用得多。
我用云端 OCR 最大的感受是省心:不用部署模型,不用维护语言包,返回的 JSON 里带文字坐标和置信度。代价是收费、有 QPS 限制、而且把文档内容发到云端要考虑数据合规。如果公司对数据安全敏感,本地部署仍是首选。
有一个小经验:做验证码 OCR 和文档 OCR 是两回事。验证码识别靠的是图像分类/序列模型,对变形、干扰线有特殊处理;文档 OCR 靠的是检测+识别流水线。别用 PaddleOCR 去识别验证码,也别用验证码的模型来解析 PDF,这两个场景模型设计思路完全不同。
2.4 OCR 预处理决定识别率的上限
同样的引擎,预处理做不做,识别率能差出 20 个百分点。我踩过不少坑之后总结出三个最关键的点:
- 分辨率不够就放大:OCR 引擎对字号很敏感,300 DPI 以下的扫描件直接识别效果差,先放大 2 倍再跑,注意放大后要保留灰度,别直接放大会糊。
- 二值化阈值别迷信固定值:光照不均的扫描件,全局二值化会把浅色字切没。用自适应阈值(如 OTSU)或分块处理更稳。
- 去噪不等于模糊:中值滤波能去椒盐噪声,但高斯模糊会把细笔画糊掉,细字场景慎用。
2.5 什么时候 OCR 结果不能直接用
这是我想重点提醒的:OCR 出来的文字只是“素材”,不是“答案”。扫描合同的 OCR 文本是逐行输出的,没有段落结构、没有表格行列关系、没有标题层级。直接把 OCR 结果切片入库,检索是能用,但召回的信息往往是碎片而非完整语义。
所以扫描件 OCR 之后,必须再做一步结构化处理。可以按版面从上到下重组段落,也可以在 OCR 文本基础上让大模型做标题识别、表格还原、关键字段抽取。这一步的产出质量,直接决定了 RAG 后续检索的天花板。
3. 多模态大模型做图文解析:什么时候值得用,怎么用
3.1 端到端多模态模型解决什么问题
如果是《图解 Transformer》这类图解型 PDF,或者图表占比很大的论文,OCR 再强也提炼不出图表里的逻辑关系——图里画的是模型结构、数据流、对比表格,OCR 只能给出图里嵌入的文字片段,语义是断的。
多模态大模型(Qwen-VL、GLM-4V、GPT-4o、Claude 等)可以直接把整页图片作为输入,输出对图表的语义理解。这就解决了 RAG 里“图说了什么”的难题:图文混排 PDF 可以按页渲染成图片,让模型输出结构化 Markdown 或 JSON,包含正文、图表描述、表格内容。检索时用户问“Transformer 结构图里 Attention 怎么画的”,模型能基于图表描述文本回答问题。
3.2 两类落地姿势对比
多模态解析主要有两种姿势,我在不同项目里都试过:
| 姿势 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| OCR + LLM 后处理 | OCR 先提文字,再用普通 LLM 重排、结构化 | 成本低、速度快 | OCR 漏字时后处理也救不回来 |
| 页面整体输入多模态模型 | 把 PDF 页渲染成图,直接让多模态模型读图输出结构化内容 | 不丢图表语义、对版面理解强 | token 消耗大、速度慢、贵 |
我的经验是:纯文字扫描件用 OCR + 传统 LLM 后处理就够了,只有图文混排、图表解释类内容才值得上端到端多模态方案。RAG 系统里往往两种方案混着用——先版面分析,判断哪些页是纯文本页走 OCR,哪些页是图表密集页走多模态。
3.3 实际操作步骤:页面渲染与 Prompt 模板
走页面整体输入这条路,核心是把 PDF 页变成图片。我常用的做法是 PyMuPDF 渲染高分辨率图:
import fitz doc = fitz.open("mixed_document.pdf") page = doc[2] # 取第3页 mat = fitz.Matrix(2.0, 2.0) # 2倍分辨率 pix = page.get_pixmap(matrix=mat) pix.save("page_3.png")得到页面图片后,把它喂给多模态模型。下面是我用 OpenAI SDK 调用兼容接口的示例(以 Qwen-VL 系列为例,本地 vLLM 部署同理):
from openai import OpenAI client = OpenAI( api_key="your-api-key", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", ) resp = client.chat.completions.create( model="qwen-vl-max", messages=[{ "role": "user", "content": [ {"type": "image_url", "image_url": {"url": "file:///path/to/page_3.png"}}, {"type": "text", "text": "请把这一页内容提取为Markdown。要求:1. 保留标题层级;2. 图片位置用[图片描述]替换,描述要说明图中表达的核心信息;3. 表格用Markdown表格还原;4. 不要遗漏页脚页码。"} ] }] ) print(resp.choices[0].message.content)Prompt 模板是这篇的经验关键,我踩过不少次“让模型自己发挥”的坑。你给的信息越具体,输出越可解析。建议让模型输出固定结构,比如:
- 标题用
#/##标记 - 图片统一替换为
 - 表格用标准 Markdown
- 末尾增加“这段内容的关键实体和对应页码”JSON 块
这样解析结果可以直接进 RAG 的 chunk 切分流程,不需要再写一大堆正则去清洗。
3.4 成本与效果的天平怎么调
多模态大模型确实贵。把一页 PDF 渲染成图片送给商用多模态模型,单页 token 消耗可能等于几百字的文本,一批几万页的文档跑下来费用不小。我的建议是分三档:
- 十页以内、图表密集:直接全量端到端多模态
- 几十页到几百页、少量图表:先版面分析识别图表页,只把图表页送多模态,纯文本页走 OCR
- 批量生产、内容敏感:私有化部署 7B~14B 参数的开源多模态模型(如 Qwen2.5-VL-7B、InternVL 系列),速度和成本都可控,效果略逊于商用旗舰但够用
生成结束后务必抽样检查模型输出的 Markdown,看看有没有漏页、幻觉图表描述。多模态模型在密集小字场景(如表格角落)出错率偏高,纯文字页用 OCR 准确性反而更高。
4. 九种 PDF 解析工具逐一拆解:选型与避坑
这一节是本文的核心,我把实践里用过的九种 PDF 解析工具按“适合场景、核心用法、踩坑点”逐一讲清楚。先给一张全景对比表,再展开说注意事项。
| 工具 | 定位 | 核心优势 | 主要局限 | 适合谁 |
|---|---|---|---|---|
| PyMuPDF (fitz) | 全能型解析库 | 速度快、文本/图片/坐标全都能拿 | 版面语义弱,表格还原一般 | 批量 PDF 文本提取、页面渲染 |
| pdfplumber | 精细文本与表格提取 | 表格提取友好、支持字符坐标定位 | 速度中等,大文档内存吃紧 | 需要精确坐标和表格结构的场景 |
| PDFMiner.six | 底层解析库 | 解析逻辑透明可控 | 接口繁琐、上手成本高 | 想彻底搞懂 PDF 内部结构的开发者 |
| Camelot | 表格专项 | 基于线条检测,表格还原精度高 | 对无线表格束手无策 | 有线框表格批量提取 |
| Tabula | 表格专项(Java系) | 部署简单,表格转 DataFrame 方便 | 中文和复杂表格表现一般 | 简单表格、临时性处理 |
| pypdf | 轻量工具库 | 合并拆分、基础文本提取 | 功能有限,复杂版式无力 | 简单 PDF 文本读取、元数据操作 |
| Unstructured | 文档结构化分区 | 输出 JSON 元素级结构,LLM 生态标准 | 依赖项重,首次安装易踩坑 | RAG 流水线标准预处理 |
| Marker | 版面分析+Markdown化 | 版面理解强,输出 Markdown 质量高 | 模型较重,速度一般 | 需要高质量 Markdown 入库的场景 |
| LlamaParse | 托管解析服务 | 解析效果标杆,兼容 RAG 框架 | 收费、数据走云端 | 不差预算、追求解析质量的生产项目 |
4.1 PyMuPDF:批量处理的绝对主力
PyMuPDF(fitz)是我使用频率最高的 PDF 库。它用 C 底层实现,解析速度是同类库里数一数二的,十页文档毫秒级提取文本毫无压力。除了提取文本,它还能拿页面尺寸、坐标、链接、图片,足够做“页面渲染+文本定位”的组合操作。
import fitz doc = fitz.open("report.pdf") for page_num in range(len(doc)): page = doc[page_num] text = page.get_text("text") # 默认纯文本 blocks = page.get_text("blocks") # 带坐标的块信息 print(f"--- Page {page_num + 1} ---") for block in blocks[:3]: print(block) # (x0, y0, x1, y1, text, block_no, block_type)坑点在于:get_text 出来的文本顺序有时候不是视觉阅读顺序,多栏排版尤其明显。PyMuPDF 的新版提供了get_text("dict")和 sorting 参数,可以按坐标二次排序,但对复杂分栏仍需手动按 x/y 坐标重排。批量解析时,建议对每一页先检测分栏,再按栏切分排序,避免把左右两栏文字混在一起。
4.2 pdfplumber 与 PDFMiner.six:表格提取兄弟组
pdfplumber 底层基于 PDFMiner.six,但对外接口友好很多。它最大的优势是能拿到字符级别的位置信息和表格结构,适合做精细的版面还原。下面是最常用的表格提取方式:
import pdfplumber with pdfplumber.open("financial_table.pdf") as pdf: page = pdf.pages[3] table = page.extract_table() for row in table: print(row)extract_table()的适用前提是表格有相对清晰的线条或明显空白分隔。遇到无线表格(只有文字排班没有线框),它经常把多列并到一列。这种情况我一般先用page.lines看线条是否存在,或者直接切到 Camelot 的 stream 模式。
PDFMiner.six 作为底层库,能让你精准控制解析逻辑,但也意味着要自己处理布局分析、字符聚合等一堆问题。项目里除非有特殊定制需求,否则用 pdfplumber 就好,没必要直接摸底层。
4.3 Camelot 与 Tabula:专啃表格硬骨头
RAG 场景里表格一直是老大难。很多 PDF 里的表格不是真表格,而是一堆绘制线条加绝对定位的文字,传统 extract_text 根本还原不了行列关系。Camelot 在这方面是专项工具,支持两种模式:
- Lattice(格子)模式:检测页面中的表格线条,按线条分割单元格。有线表格效果极好。
- Stream(流)模式:按文本之间的空白间隔推断行列,适合无线表格,但误判率偏高。
import camelot tables = camelot.read_pdf("table_doc.pdf", pages="1", flavor="lattice") tables[0].df.to_csv("table_1.csv", index=False)Tabula 是另一款表格提取工具,Java 实现,本地需要有 Java 环境。它提取简单规整表格很方便,输出直接就是 DataFrame,但中文字体边界处理一般,复杂合并单元格经常错位。我的建议是:表格解析先用 Camelot 试 Lattice,不行再换 Stream 或 pdfplumber。如果表格里大量是合并单元格、斜线、复杂表头,表格工具们基本都拉胯,那就把表格区域裁出来丢给多模态大模型。
4.4 pypdf:轻量场景的好帮手
pypdf(旧称 PyPDF2)定位就是“轻量”。合并 PDF、拆分页面、提取简单文本、读元数据,这些操作用它最省事,API 非常简洁:
from pypdf import PdfReader reader = PdfReader("simple.pdf") text = reader.pages[0].extract_text()别指望它处理复杂版面——没有坐标、没有表格、对扫描件无能为力。它适合在自动化脚本里做 PDF 操作的基础环节,真正的内容解析还是要上重型工具。小项目中可以用 pypdf 做“PDF 是否为文本型”的快速判断,识别到文本长度为零就转到 OCR 流程。
4.5 Unstructured:RAG 流水线的标准预处理件
Unstructured 在 LLM/RAG 生态里越来越常见,它把 PDF、HTML、图片统一解析成带类型的元素(Title、NarrativeText、Table、ListItem等),输出成 JSON 或按元素分块。这种“元素级”粒度对 RAG 非常友好,因为分块时可以直接按元素类型决定切分逻辑。
from unstructured.partition.pdf import partition_pdf elements = partition_pdf( filename="doc.pdf", strategy="hi_res", # 高精度模式,会调用 OCR 和布局模型 ) for el in elements: print(el.category, el.text[:80])坑点是要提醒的:Unstructured 依赖项很多,装的时候容易在 PIL、opencv、detectron2 这些深层依赖上报错。建议用虚拟环境专门安装,别把公司的主环境搞乱。strategy参数也很关键,hi_res慢但准,fast快但版式识别差很多。
4.6 Marker 与 LlamaParse:高质量解析的生产级选择
Marker 是一个把 PDF 转为高质量 Markdown 的工具,内部用了深度学习模型做版面分析、公式识别、表格还原。实测下来的输出质量比普通库高一个档次,尤其对多栏文档和带公式的论文页处理得不错。缺点是模型体积大,解析速度偏慢,大批量处理要排队。它的典型用法:
marker_single /path/to/input.pdf --output_dir /path/to/output输出是 Markdown 文件,可以直接喂给 RAG 的分块模块,省去大量清洗工作。个人项目或对成本敏感的中小团队,Marker 是“离线高质量方案”的好选择。
LlamaParse 是 LlamaIndex 团队的托管解析服务,我把它当作解析效果的天花板来看。它对复杂版面、嵌套表格、图文混排支持很好,而且原生支持 RAG 工作流,解析结果直接生成适合检索的节点。缺点是收费、文档要传到云端,数据敏感的项目基本无缘。有预算且解析质量要求极高的商业项目,值得优先评估。
4.7 九种工具的组合策略
工具不是越多越好,关键是组合。我现在生产项目里的默认策略是:
- 超过 80% 的常规 PDF:PyMuPDF 提取文本(按坐标排序),pdfplumber 辅助提取表格,分块入库。
- 扫描件:PaddleOCR 本地识别,结果送 LLM 做分段和字段抽取。
- 图表密集的论文/图解书:页面渲染 + 多模态模型(本地 Qwen-VL 或云端)。
- 表格极多且有线框:Camelot Lattice 提取,失败页兜底丢给多模态。
- 预算充足、文档格式刁钻:LlamaParse 托管解析,Unstructured 做中间格式标准化。
5. 从解析结果到入库:清洗、分块与质量验证
5.1 清洗这一步千万别省
无论是 OCR 还是 PDF 库提取,原始结果里都带着一堆垃圾。常见的有:页眉页脚反复出现、连字符断词残留、OCR 误识别的孤立字符、多余空白和换行。我处理时会把页眉页脚、页码位置先标记出来删除,再按规则清理连字符(conti- nue->continue)。
OCR 误识别是无法完全避免的,所以清洗后的文本建议再做一次“文本质量检测”:统计异常字符比例、长度分布、重复段落。异常比例高的页单独抽出来人工检查,或者降级重跑高精度模型。
5.2 分块策略要跟着文档结构走
解析结果进入 RAG 前的最后一步是切分 chunk。很多人直接按固定字数硬切,效果通常很差——一个表格被切断、一个段落被腰斩,检索召回的信息全是残片。
我的建议是先按语义边界切。Unstructured 的输出天然按元素分好块,可以直接用;常规文本则可以按标题层级、段落边界、表格边界切分。切分时设一个重叠窗口:
def split_text(text, chunk_size=800, overlap=100): chunks = [] start = 0 while start < len(text): end = start + chunk_size # 尽量在段落边界切 while end < len(text) and text[end] not in "\n。;": end += 1 chunks.append(text[start:end]) start = end - overlap return chunks重叠的目的是避免切断语义,让相邻 chunk 之间保留上下文缓冲。具体数值不是固定的,中文场景 800~1200 字的 chunk 加 10%~15% 重叠是我常用的起点。
5.3 表格和结构化数据单独入库
对于合同、财务报表这类表格密集型文档,我最推荐的做法是表格不走普通文本 chunk,而是转成结构化 JSON 后单独建立字段索引。比如“合同金额”“签订日期”这类键值对,抽取出来后放进元数据,检索时直接按字段过滤,比全文召回准确得多。
这一步可以借助 LLM 做字段抽取。解析出表格文本后,让 LLM 输出固定 JSON 结构:
{ "table_caption": "2024年度收支总览", "headers": ["项目", "收入", "支出"], "rows": [["办公费", "12000", "8000"]], "key_terms": ["办公费", "收支"] }将key_terms和table_caption作为 chunk 文本,rows单独存结构化字段。检索“办公费支出多少”时,向量召回“table_caption 和 key_terms”命中率会非常高。这也是我最近把知识图谱(KG)和 RAG 结合用的经验:结构化字段进 KG,非结构化文本进向量库,两者互补比单一方案稳妥。
5.4 入库之后要闭环验证
解析做得对不对,最终要看检索效果。我搭 RAG 项目时一定会留一个验证集:准备 20~50 个真实用户的提问,对每个问题标注正确答案出自哪个文档、哪个页面。入库后逐个跑检索,计算 hit 率,看召回结果里有没有正确答案片段。
当命中率低于 70% 时,优先排查三件事:chunk 是否切碎了语义块、表格是否没结构化、OCR 是否漏字。别一上来就换 embedding 模型,数据导入层的收益通常远大于换模型。
我现在还会统计每批文档的“解析失败页”,把连续解析质量差的页面单独导出,定期抽检。这个习惯帮我积累了很多工具在特定场景下的表现数据,比任何官方文档都管用。
写在最后
图文与 PDF 解析没有银弹,工具选型永远跟着文档类型和预算走。我现在的小项目会先跑 PyMuPDF + PaddleOCR 的组合救急,能覆盖七八成场景;等文档多了、检索要求细了,再把 Marker 或 LlamaParse 接进来做增量。多模态大模型是真能解决图表语义理解的问题,但开销不小,适合按页按需调用,而不是一股脑全量跑。
做 RAG 这一年多,我最大的体会是:数据导入环节粗糙,后面所有调优都是在还债。解析这步多花点精力把结构化做好,检索和生成的体验会省很多心。下篇如果有机会,我再聊聊 embedding 与重排序的实际选型对比,以及知识图谱与 RAG 结合的具体做法。