干过 RAG 项目的人都有同感:前期最难的不是模型选型,也不是向量检索调参,而是数据导入和解析。反过来你就知道了,如果你的 PDF 是从网上下载的“文字层很干净”的报告,用简单工具就能搞定;但一旦遇到扫描件、票据、PPT 导出的 PDF、多语言文档,整条链路马上进入“谁都能做一点,但谁都不敢保证不丢信息”的尴尬状态。
这篇文章是 RAG 数据导入与解析攻略的第二篇,重点锁定图文与 PDF。先把“RAG 知识库能不能直接存图片”这个老问题聊透,再带你们过一遍 OCR 的实战细节、多模态大模型的用法,以及我实测过的九种 PDF 解析工具的选型逻辑。适合正在搭 RAG 知识库、或者想把 PDF 文档处理成高质量文本 chunk 的开发者,也适合那些被扫描 PDF 折磨得想骂人的运维和内容负责人。
1. 为什么说图文与 PDF 是 RAG 的第一道坎
1.1 “RAG 知识库能存图片吗”这个问题要拆开看
很多人一上来就问:知识库能存图片吗?如果单纯从向量数据库的角度回答,当然能——你可以把图片送到一个视觉嵌入模型里,生成图片向量,存进同一个向量库,然后用文本或图像查询去召回。CLIP 这类模型就是干这个的,多模态向量检索也是成熟方案。
但实际落地时你会发现,绝大多数 RAG 场景要的是“基于图片里的内容回答问题”,不是“把图片原样找出来”。比如一张扫描的合同,用户会问“乙方在什么时间付款”,这时候直接存图片向量意义不大,因为问题涉及的是图片里的文字信息。所以更务实的做法是:
- 把图片中的文字、表格、版式结构抽取成文本,进入常规文本检索链路。
- 如果业务确实要求按图找图、或召回后给用户展示截图,再单独建一个视觉向量库,不与文本检索混在一起。
我见到很多项目团队一开始兴致勃勃搞“图片+文本双路召回”,最后发现双路向量空间不一致,融合排序很麻烦。这里最朴素的建议是:先搞清楚你要的是“内容可检索”还是“图片可检索”,前者走 OCR/多模态抽取,后者才值得上视觉嵌入。多数知识库场景,其实前者就够用了。
1.2 PDF 并不是一种格式,而是一堆格式的集合
PDF 的麻烦在于它的内部构造差异极大。同样是.pdf后缀,背后可能是完全不同的数据形态:
- 原生数字 PDF:有文字层,有字体编码,可以通过
page.get_text()直接抽文本。 - 扫描 PDF:每一页都是一张图片,没有任何文字层,必须走 OCR。
- 混合 PDF:一部分页是文字,一部分页是图片,最常见的就是“扫描件 + 我加了一页备注”这种文件。
- 版式复杂的 PDF:双栏排版、表格线、页眉页脚、悬浮批注,这类文档抽出文本容易乱序,尤其是有表格的时候。
这些差异决定了没有任何一个工具能通吃所有 PDF。我在实际项目中反复踩过这种坑:拿 PyMuPDF 把文字抽出来,发现顺序按照“页眉→右栏→左栏→页脚”跳来跳去,后来才知道双栏 PDF 需要做版面分析。所以我会在后面的章节里,把不同工具的使用场景说得具体一点,你会发现选工具其实是选“你当前 PDF 的最大摩擦点”。
2. OCR 实战:从传统识别到多语言关键时刻
2.1 传统 OCR 什么时候够用
OCR 的本质是把图像里的文字像素转成可编辑文本。它不负责理解语义,也不擅长处理“上下文”,但它成本低、速度快、稳定,尤其适合大量文本简单、版式固定的扫描件。
什么时候我会首选传统 OCR?典型场景是发票、运单、标准合同、期刊扫描页。这些页面字迹清晰,版式相对固定,不需要太多上下文推理。另一个优势是本地化部署容易,不需要昂贵的多模态硬件支持。
但传统 OCR 有明显天花板。比如一张流程图,文字都能识别出来,但“A 指向 B、B 又分支到 C”的逻辑关系无论如何都无法从纯文字里还原。还有复杂表格,OCR 能把单元格内容识别出来,但表格的行列归属经常丢,导出后变成一串乱文字。这时就需要考虑多模态模型或者专门的表格解析工具。
2.2 Tesseract vs PaddleOCR 的实测经验
Tesseract 是老牌开源 OCR,前后处理简单,适合轻量场景。它的 Python 接口一般长这样:
from PIL import Image import pytesseract text = pytesseract.image_to_string( Image.open("page.png"), lang="chi_sim+eng" ) print(text)但注意,chi_sim语言包需要单独下载,装完 Tesseract 主程序不代表就能识别中文。另外 Tesseract 对清晰英文扫描件的识别效果不错,可一旦遇到低分辨率图片、复杂背景或者手写体,识别率会明显下滑。
PaddleOCR 是另一个路线,内置检测加识别的完整流程,对中文支持更好。用 PaddleX 的管道式接口写起来也很简单:
from paddlex import create_pipeline pipeline = create_pipeline("OCR") result = pipeline.predict("invoice.png") for res in result: texts = res["ocr_res"]["texts"] print(texts)我第一次跑通这条链路时很惊喜,普通中文发票上的数字和字段基本都能抽出来,而且它自带方向分类器,竖排文字也能处理。不过 PaddleOCR 的模型文件比较大,首次加载会有明显延迟,部署在服务器上要注意显存或内存占用。
实战里我还会在喂图片前先做预处理:灰度化、二值化、去噪点。简单三步对扫描件有奇效,很多原本识别失败的模糊文档,预处理后正确率能提升一大截。常用做法是用 OpenCV 做个自适应阈值:
import cv2 img = cv2.imread("scan.png", cv2.IMREAD_GRAYSCALE) thresh = cv2.adaptiveThreshold( img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 15, 8 ) cv2.imwrite("scan_clean.png", thresh)2.3 韩文、日文等非中英场景经常翻车
网上搜 OCR 相关问题时,经常看到有人拿默认配置去识别韩文,结果一团乱。比如有人用paddlex默认管道识别含韩文的图片,输出的却是中文乱码。这不是引擎不行,而是语言配置不对。
Tesseract 识别韩文需要指定lang="kor",并且要有kor.traineddata语言包。PaddleOCR 这边的多语言模型则用参数指定语言。如果你不指定,默认模型基本都是按中英文训练的,遇到韩文字符就会硬生生映射到近似的汉字或字母上。
混排文档就更麻烦。中文、英文、数字混排还好,中日韩混排时我不会指望单次 OCR 全搞定。我的做法是先做版面分析和段落切分,把每个区块单独裁剪出来,再按区块语言去识别。这样虽然工程量大,但准确率远比“一张大图跑到底”靠谱。别嫌麻烦,这恰恰是 RAG 知识库真正落地时最值钱的处理细节。
2.4 OCR 和验证码不是一回事
经常有人搜 “OCR 识别验证码”,甚至有人问我能不能直接把验证码识别模块接进 RAG 流程。我的态度很明确:别这么干。验证码本身是反识别的对抗场景,需要针对性训练和处理,跟知识库里的文档提取完全是两个方向。而且从合规角度讲,绕过验证机制本来就不该是 RAG 系统该做的事。
如果只是在自己的测试环境里学习 OCR 基础,可以拿验证码图片练习图像预处理和字符分割;但生产系统里千万别把验证码识别当成通用 OCR 来用。把精力放在票据、扫描件、合同上,收益会大得多。
3. 多模态大模型:把“识别”升级为“理解”
3.1 为什么 OCR 之后还需要多模态模型
传统 OCR 输出的是文字序列,但 RAG 检索和问答需要语义结构。举个很常见的例子:一张 PPT 页面里,左边是流程图,右边是说明文字,下面还有一个总结表格。OCR 能识别出所有字符,但分不清哪个文字是流程节点、哪个是单元格、哪个是图片里的注释。
多模态大模型在这类场景里优势明显。它把整张图片作为视觉输入,再结合语言指令去理解布局、内容和逻辑关系。你可以说“把这张图里的流程图转成 Mermaid 语法”,也可以说“把表格转成 Markdown”,它能返回结构化的结果,而不仅仅是扁平文本。
我自己最常用的一条链路是:PDF 页面转 PNG → 多模态大模型直接输出 Markdown → 再对 Markdown 做分块。这样保留了标题层级、表格结构、列表语义,检索时的上下文质量提高非常明显。
3.2 本地多模态模型怎么选
市面上能跑的多模态模型很多,本地部署常用 MiniCPM-V、Qwen-VL 系列等。选择逻辑很简单:显存够不够、推理速度可不可以接受、对中文排版和表格的理解行不行。
- 8GB 左右显存:可以跑 7B 级别的小模型,适合做中文单据、规范文本抽取。
- 24GB 以上显存:可以上更大的 VLM,处理复杂图表能力更强。
- 没有 GPU:建议先用云端 API,或者只在少量样本上做标注验证,别硬跑本地。
还有个容易被忽略的点:多模态模型把页面当图片理解时,DPI 会直接影响效果。一般建议把 PDF 页转成 150-220 DPI 的 PNG,分辨率太低模型看不清小字,太高则推理时间暴涨。我通常对含小字表格的页面用 220 DPI,纯文字页面 150 DPI 足够。
3.3 提示词怎么写得能直接落到 RAG 管道
我给多模态模型的提示词已经稳定成一套模板,核心是“结构化输出”四个字:
请把这张图片的内容转成 Markdown 格式输出: 1. 标题用 # 表示,小节用 ##; 2. 表格必须用 Markdown 表格; 3. 流程类内容用有序列表表达; 4. 图片内的图例、说明文字不要遗漏; 5. 如果看不清某个字,标注 [无法识别],不要猜测。这个提示词既约束了输出格式,又给了“不知道就说不知道”的安全出口。模型返回的内容我会直接当成 Markdown 文档,再做分块和向量化。相比传统 OCR 输出的裸文本,这种带结构的输出检索效果更好,问答时能看清楚标题层级,不会把两个小节的文字混在一起。
3.4 成本和准确率之间怎么平衡
多模态模型比传统 OCR 贵很多,推理也慢。所以我在生产管道里做了一套兜底逻辑:先用传统 OCR 抽文本,如果抽出来之后发现某页文本量极低,比如扫描件图片页、或者包含明显图表的页面,再把这一页单独交给多模态模型做结构化抽取。这样既控制成本,又保证复杂页面的效果。
实际操作上,这个决策不需要很精确。我是按“每页提取到文本字符数”来兜底的:少于 50 个字符,说明这一页基本是图像内容,直接转多模态;多于 500 个字符,说明还是以文字为主,OCR 结果已经可以接受;中间地带,看版面是否有表格,有表格就优先多模态。这条规则我跑了几个项目,效果稳定。
4. 九种 PDF 工具选型:一张表看清方向
4.1 九种工具对比一张表收好
我筛选了九个在实际项目里用过的 PDF 相关工具,把它们的主打能力和适用场景整理成一张表。这样你在选型时可以直接定位,不用每个都试一遍。
| 工具 | 核心能力 | 适用场景 | 输出形式 | 部署复杂度 |
|---|---|---|---|---|
| PyMuPDF | 文本抽取、页面转图片、PDF 合并拆分 | 原生文本 PDF、需要页面渲染 | 纯文本、图片 | 低 |
| pdfplumber | 表格抽取、坐标定位 | 表格密集的报表、账单 | 文本、表格结构 | 低 |
| pypdf | 纯文本抽取、页面操作 | 简单文本 PDF,不需要高精度 | 纯文本 | 低 |
| PDFMiner | 底层解析、文本位置信息 | 需要精细坐标与字体信息 | 文本、布局数据 | 中 |
| Camelot | 基于线和区域识别的表格抽取 | 有线表格 | 表格、DataFrame | 中 |
| tabula-py | 基于 Java 的表格抽取 | 规则表格,需要识别边框 | 表格、DataFrame | 中 |
| unstructured | 文档元素级切分、分区 | 多格式文档统一进 RAG | element 对象 | 中 |
| Marker | PDF 转 Markdown | 需要保留结构、高质量文本 | Markdown | 高 |
| Mathpix | 公式、复杂表格转结构化 | 学术论文、公式密集文档 | Markdown 等 | 高(商业) |
这张表不是让你选最强大的,而是让你按当前文档类型选最省心的。很多人上来就看unstructured和Marker,觉得它们功能全,结果部署依赖一堆、速度还很慢,最后连原始文本都没跑通。我建议从小而美的工具开始。
4.2 PyMuPDF:先抽文本,再用图片兜底
PyMuPDF 是我的首选第一棒,因为它既能快速抽文本,又能极低门槛地把 PDF 页转成图片。遇到原生 PDF 时,它的准确度和速度都让人放心。基本用法如下:
import fitz doc = fitz.open("report.pdf") for page in doc: text = page.get_text("text") if page.get_text("words"): # 原生文字层,直接进入后续处理 pass else: # 没有文字层,转图片后交给 OCR pix = page.get_pixmap(dpi=220) pix.save(f"page_{page.number}.png")page.get_text("words")会返回单词列表,如果列表为空,基本可以确定这一页是扫描图片,需要转图片走 OCR。这个方法极其简单,却能判断整个文档的扫描率,帮你决定后面要不要接入 OCR 管道。
我还会经常用page.get_text("blocks"),因为它按文本块返回,能保留一定的阅读顺序。注意双栏 PDF 依然可能乱序,这时候不要急着硬跑,可以观察块坐标,按y0坐标重新排序。
4.3 pdfplumber 和 Camelot:把表格从 PDF 里“捞”出来
表格是 RAG 里的重灾区。很多 PDF 用纯文本抽取后,表格里的数字和内容会被打散,完全看不出行列关系。pdfplumber 这时就派上用场:
import pdfplumber with pdfplumber.open("table_report.pdf") as pdf: for page in pdf.pages: for table in page.extract_tables(): for row in table: # 每行就是一个列表,可以转 DataFrame print(row)pdfplumber 通过解析页面上的线条和文本坐标来还原表格结构,对有线框的表格效果尤其好。但遇到无线框表格,它会力不从心,这时候可以试试 Camelot 的lattice模式,或者用多模态模型兜底。
Camelot 的典型用法是:
import camelot tables = camelot.read_pdf("report.pdf", pages="1-5") df = tables[0].df它对“有明显线条”的扫描件有过人之长,但在原生文字版式表格上反而没有 pdfplumber 稳。我的建议是:这两个工具都配好,同页面先用 pdfplumber 试,表格行列对不上,再换 Camelot,谁输出对用谁。
4.4 unstructured 和 Marker:面向 RAG 的“全套西装”
如果你不想自己拼装那么多零件,unstructured是个不错的起点。它把 PDF 解析成一系列Element对象,比如Title、NarrativeText、Table,并直接把表格输出成 HTML 格式,正好适合后续分块。它的 API 风格是这样的:
from unstructured.partition.pdf import partition_pdf elements = partition_pdf("docs.pdf", strategy="auto")strategy="auto"表示自动判断页面有没有文字层,没有就启动 OCR。它能直接输出带类型标记的 element 列表,分块时非常方便。但代价是依赖复杂,模型文件多,新手安装经常会遇到一堆依赖错误。
Marker 则更偏向“把 PDF 变成高质量 Markdown”。它对版面分析、标题层级和表格结构的保留做得非常好,适合想把整个文档干净地变成 RAG 输入的团队。代价是模型重、部署条件高,对纯文本 PDF 有点杀鸡用牛刀。
4.5 我的选型决策套路
总结一下我实际用下来的选型路径:
- 先抽样 5 页,判断扫描率和表格比例。
- 扫描率低、表格少:直接用 PyMuPDF 抽文本,分块入库。
- 表格多且线框清晰:pdfplumber + Camelot。
- 扫描率很高、语言单一:PaddleOCR 批量识别。
- 扫描率高、版式复杂、有大量逻辑图表:多模态大模型抽取 Markdown。
- 对统一格式有要求且团队人力足:unstructured 统一承载。
这套路径的好处是先快后慢,不会一上来就陷入复杂工具的学习成本里。你完全可以根据自己的文档特点只取其中一环,不必所有工具都精通。
5. 常见问题与排查笔记
5.1 识别出的文本顺序混乱,怎么办
PDF 文字层其实带有坐标信息,PyMuPDF 按“块”抽取时,如果原文档是双栏,顺序很有可能是“左栏上半、右栏上半”交叉输出。这时候我不直接使用get_text("text"),而是基于块的坐标自行排序:
import fitz doc = fitz.open("column.pdf") page = doc[0] blocks = page.get_text("blocks") blocks.sort(key=lambda b: (round(b[1] / 50), b[0])) import fitz doc = fitz.open("column.pdf") page = doc[0] blocks = page.get_text("blocks") blocks.sort(key=lambda b: (round(b[1] / 50, b[0])))代码第二行用了round(b[1] / 50)把相近高度的行归并,再按b[0]即 x 坐标排序。实测下来能把大多数双栏文章恢复成正确的阅读顺序。如果你的文档是有三栏或更复杂的版式,干脆转图片交给多模态模型,让模型理解版面,比人工写排序规则快得多。
5.2 识别出来全是乱码 / 空字符
原生 PDF 抽取有时会得到乱码,常见原因是字体编码问题,尤其是方正字体、某些中日韩字体子集。这种情况下别纠缠文字层,直接用页面转图片再 OCR,反而更可靠。我见过有人在字体映射问题上折腾一整天,最后换 OCR 十分钟解决。RAG 是结果导向,没必要在 PDF 底层解析上死磕。
5.3 扫描 PDF 识别率很低
扫描件识别率低的原因大多是:分辨率不够、倾斜角度、阴影遮挡、页面底色偏灰。我的处理顺序是:
- 转图片时设置 DPI 不低于 300;
- 先用 OpenCV 做倾斜校正;
- 再做自适应二值化;
- 最后才交给 OCR。
如果这样还不行,多半是原图本身质量差,只能尝试多模态大模型。多模态模型在低清晰度图片上的抗干扰能力比传统 OCR 强,虽然也不是万能,但很多时候能靠上下文猜出正确内容。
5.4 表格行列错乱,怎么保留结构
表格行列错乱的核心是没有利用坐标信息。pdfplumber 可以直接读取单元格坐标,你也可以把每个文字块的坐标一起存进元数据,后续分块时按坐标重组。另一个更省心的办法是直接输出 HTML 表格,unstructured和部分多模态模型都支持这种输出。HTML 表格在检索时反而好处理,因为你可以按<tr>、<td>标签切分。
5.5 韩文、日文混排的文档
别指望一个语言模型解决所有语言。我把多语言文档先按“语言区块”切分,再分别交给对应语言模型。PaddleOCR 虽然支持多语言,但混排时参数设置麻烦。如果页面少,直接交给支持多语言的多模态大模型更省事。我在韩文测试里用过的经验是:模型输入图片,把整段提示词里的语言要求写清楚,比如“韩文部分必须原样输出”,效果比设置各种参数更直观。
5.6 C#、Java、PHP 环境怎么办
网上很多人搜 C# OCR、Java OCR、PHP OCR 识别验证码之类的问题。如果你不是 Python 技术栈,我的建议也很简单:OCR 和 PDF 解析这部分已经高度服务化和平台化,不必非要在一个语言里什么都自己做。独立跑一个 Python 解析服务,通过 HTTP 接口对外提供,C#、Java 直接调用,是最稳妥的架构。RAG 管道本质上是数据处理系统,处理层的语言不必跟业务系统强绑。
6. 我习惯的本地落地流程
说这么多,最后分享一下我在具体项目里面跑得最顺的落地流程,也当作你入手时的默认模板。
我的完整管道是五段式:
- 入参统一转 PDF。Word、PPT、图片全部转成标准 PDF,简化后续处理。
- 每页抽取文字层,统计空页与非空页,自动判断扫描率。
- 空页走 OCR,文本页走 PyMuPDF 抽取,表格页走 pdfplumber。
- 对复杂图表页面单独截取,交给多模态大模型输出 Markdown。
- 把文本和 Markdown 统一分块、向量化,最终进入 RAG 检索。
这个流程不用一上来就套重型工具,能最大化利用原生 PDF 的文字层,只在必要的时候引入 OCR 和多模态,成本控制得比较好。
关于“图片到底存不存向量库”这个开头的问题,我现在的回答是:知识库以文本检索为主,图片拆成文本信息使用;只有确需按图找图,才单独建图像向量库。这套组合在多个业务系统里跑得都很稳,后续如果文档量继续增大,我还会在分块和版面分析上继续做优化,但至少目前,这套流程解决了我遇到的所有扫描件和复杂版式问题。