做RAG项目,很多人一上来就冲向量库选型、Embedding模型调优,结果数据导入这一步没做好,后面全白搭。尤其是图文和PDF这类文档——你以为PDF解析就是“把文字抠出来”,真正跑起来才发现,PDF这个格式是“看起来统一,其实各长各样”:有原生文本的、有纯扫描图片的、有图文混排的,还有表格和公式混在里头的。这一篇是RAG数据导入与解析攻略的第二篇,专门把图文与PDF解析这条路走通:OCR怎么选,多模态大模型到底有没有必要上,以及我实测过的九种主流PDF解析工具分别用来干什么。适合正在搭RAG知识库、做文档智能处理,或者被领导一句“把这些PDF都导进去”压得喘不过气的朋友们参考。
1. 先拆解问题:为什么PDF是RAG导入的第一道坎
1.1 PDF的三种形态,决定了你用什么解析武器
PDF解析之所以麻烦,根源在于它是个“呈现格式”,不是“内容格式”。PDF内部存的是一堆绘图指令、字体信息、坐标位置,文字是不是“真文字”,要看这个文件是怎么生成的。我经手过的项目里,PDF基本逃不出三种形态:
- 原生文本型:由Word、LaTeX、HTML打印导出,文字可选中、可复制,内部有文本对象和字体映射表。这类最好处理,直接提取文本即可。
- 纯扫描型:本质是一张张图片打包进PDF,常见于老合同、手写单据、历史档案。没有文本层,必须走OCR。
- 混合型:部分页面有文本层,部分页面是扫描图,甚至同一页里既有可复制文字又有嵌入图片。企业年报、审计报告这种文件特别常见。
这个区分不是闲聊,它直接决定了解析管线的设计。我见过有人拿OCR去跑原生文本PDF,结果把清晰的文字重新识别成带错别字的“脏文本”,精准率反而降了几个点;也有人把一个纯扫描件丢给纯文本提取器,得到的是一整页空白。正确做法是:先探测PDF形态,再分流处理,而不是拿一个工具硬刚所有文件。
提示:判断PDF是否含文本层,最简单的办法是用PyMuPDF提取前几页文本,如果文本字符数几乎为0,而页面图片对象数量大于0,大概率是扫描件。
1.2 一个“先分类、后选路”的解析策略
我通常建议把PDF解析设计成“三通道”管线,而不是单一解析器走到底:
- 文本通道:面向原生文本型PDF,用文本提取类工具(PyMuPDF、pdfplumber)直接抽文本,保留坐标信息备用。
- OCR通道:面向无文本层页面,用PaddleOCR或RapidOCR识别文字,输出带位置坐标的文本块。
- 语义通道:面向复杂版面,比如表格密集、公式多、图文交错严重的页面,用marker-pdf或多模态大模型做结构化抽取。
为什么非要分通道?因为文本提取和OCR的侧重点完全不同。文本提取器拿到的是“已有的字”,速度快、准确率高,但遇到扫描图就抓瞎;OCR擅长“无中生有”,把图像变成文字,但慢、耗算力、对排版复杂的页面容易顺序错乱。两者混用,互相干扰,分通道之后各司其职,每个环节都能用上最合适的工具。
实际部署时,我习惯在文件入口处做一次“页级体检”:用PyMuPDF遍历每一页,统计文本字符数、图片数量、页面尺寸,然后给页面打标签。整页文本字符数超过50就走文本通道,低于阈值但有图片就走OCR通道,再拿版面复杂页去触发语义通道。这套规则实现了80%的自动分流,剩下的边缘情况再靠人工抽查兜底。
1.3 回答一个高频问题:RAG知识库到底能不能存图片
从热词榜单里看到不少人搜“rag知识库能存储图片嘛”,这个问题的答案很微妙:能存,但不能只存图片。
向量数据库本身不排斥图片模态,你可以把图片做多模态Embedding把向量存进去,也能用CLIP这类模型实现图文统一向量空间。但RAG的核心是“检索增强生成”,LLM只能读文本,你查回一张图片,模型没法直接把它当上下文理解。所以更实际的做法是:图片本身存入对象存储,向量库里存的是图片的“可文本化描述”——可以是OCR识别的文字、图片周围的上下文、图注说明,或者多模态模型生成的图片内容摘要。检索时命中图片对应的文本块,系统再从对象存储按路径把图片带回给用户查看。
这样既解决了“图片进知识库”的需求,又不打破RAG的文本链路。我在做产品手册知识库时就是这种方案:图片单独存OSS,向量库里挂一条“图片路径+文字描述”的文本记录,检索效果比硬塞图片向量要好得多。
2. OCR与多模态大模型的选型逻辑
2.1 传统OCR方案:PaddleOCR与Tesseract怎么选
OCR是整个图文解析链路的地基。目前开源社区里主流的两个选择是PaddleOCR和Tesseract,我两个都深度用过,说说真实差异。
Tesseract是传统老牌OCR,胜在轻量、跨平台,但中文识别效果一般,尤其面对手写体、低分辨率扫描件、复杂背景时,错误率会明显升高。PaddleOCR是百度开源的OCR体系,带完整的版面分析能力(PP-StructureV2),可以识别文字、标题、表格、图片这些版面元素,中文场景下识别的稳定性和准确率,我实测下来比Tesseract高不少。一个直观的数据:同样的中文扫描合同,PaddleOCR的字准确率在95%以上,Tesseract通常只有85%~90%,而且PaddleOCR的方向分类器能自动纠正扫描件旋转问题,这在批量处理时省了大事。
代码上看,PaddleOCR的使用也相当友好。新版PaddleOCR 3.x推荐用Python接口直接调用:
from paddleocr import PaddleOCR ocr = PaddleOCR(use_doc_orientation_classify=True, use_doc_unwarping=True, use_textline_orientation=True) result = ocr.predict("scan_page.png") for res in result: for item in res["rec_texts"]: print(item)几点经验:
- 如果机器有GPU,把
device="gpu"配上,识别速度能提升好几倍;纯CPU跑大批量文件会等得人想骂人。 - PaddleOCR依赖PaddlePaddle框架,和PyTorch的CUDA版本容易打架,建议在Docker或者独立conda环境里部署,别和主项目共用一套环境,这个坑我踩过不止一次。
- OCR对图片质量非常敏感,预处理(灰度化、去噪、二值化)比调模型参数更管用。扫描件分辨率最好保持在300DPI以上,低于这个,文字边缘发糊,再强的OCR也白搭。
2.2 多模态大模型解析:什么场景值得上
多模态大模型来解析PDF,是这两年特别热的方向。原理很简单:把PDF页面渲染成图片,传给GPT-4o、Qwen-VL这类模型,让模型直接输出Markdown或JSON结构。好处是理解能力强,复杂表格、跨栏排版、图文混排关系都能Hold住,不会像规则工具那样傻傻地按坐标切字。
但我要泼一盆冷水:多模态大模型不该是整个PDF管线的默认主力,而是“精读工具”。原因有两点:
- 费用高。视觉token比纯文本token贵一个量级,一本几百页的扫描书全量跑大模型,账单相当可观。
- 速度慢。走API并发生多少受并发限制,批量解析十万份文件时会排到天荒地老。
所以我的策略是:把多模态大模型用在刀刃上。先让规则工具和OCR把90%的常规页面处理掉,只剩下表格混乱、公式复杂、或者OCR结果明显不合逻辑的页面,再渲染成图片丢给多模态模型做一次结构化修正。这样既保证了精度,又把成本控制在合理范围。
调用时的工程细节也很关键。让模型输出结构化结果,必须用JSON Schema约束,别让它自由发挥。比如:
{ "type": "object", "properties": { "title": {"type": "string"}, "table": {"type": "array", "items": {"type": "array", "items": {"type": "string"}}}, "image_desc": {"type": "string", "description": "页面中图片的内容描述"} }, "required": ["title", "table", "image_desc"] }然后提示词里写明“只输出符合schema的JSON,不要输出任何其他内容”。不然模型经常附带一堆看起来很有道理、实则脏数据的话,后面清洗起来比解析还痛苦。
2.3 更聪明的组合:OCR、多模态与规则引擎的配合
实际工程里,单一方案很难打天下,组合拳才是常态。我常用的组合模式是“规则引擎初筛 + OCR兜底 + 大模型修正”三层结构。
规则引擎那层做的是快速分离:先判断PDF有没有文本层,有的话文本层质量好不好(可以用乱码率、特殊字符占比这些指标衡量)。质量好就直接用文本提取结果,质量差(比如字体映射错乱导致的乱码)就把页面渲染成图片,交给OCR重新识别。OCR识别完,再针对表格和特殊版式,让大模型做局部修正。
这套三层模式的好处在于:每一层做自己最擅长的事,互不拖后腿。规则引擎零成本但判断极快;OCR成本中等、覆盖率高;大模型贵但精度上限高。整体算下来,平均每页处理成本可能只有全量跑大模型的十分之一,而最终文档质量却能达到大模型方案九成以上的水平。
3. 九种PDF解析工具横向实测与选型
3.1 文本型PDF优先看的三个库
PyMuPDF(fitz):这是我用得最多的库,没有之一。底层是C++的MuPDF,性能极好,解析速度是纯Python库的好几倍。它能做的事很全:提取文本、渲染页面为图片、提取图片、获取文字坐标、读写PDF注释。在RAG场景里,我通常拿它做两件事:一是“体检”,快速判断PDF形态;二是渲染扫描页成图片,供OCR使用。代码用法:
import fitz # PyMuPDF doc = fitz.open("sample.pdf") for page in doc: text = page.get_text("text") blocks = page.get_text("blocks") # 带坐标的文本块 pix = page.get_pixmap(dpi=300) # 渲染成图片,OCR前处理用PyMuPDF的文本提取是按PDF内部对象的顺序走的,遇到多栏排版可能顺序错乱,所以复杂版式建议用“blocks”模式手动排序。
pdfplumber:如果说PyMuPDF是速度担当,pdfplumber就是精度担当。它基于PDFMiner.six开发,能拿到字符级的坐标信息,非常适合做表格提取和细粒度文本分析。extract_table()方法对有线表格识别效果很好:
import pdfplumber with pdfplumber.open("table.pdf") as pdf: page = pdf.pages[0] table = page.extract_table() for row in table: print(row)缺点是慢,几百页的PDF解析完可能得等上好一会儿,所以我的建议是“快刀用PyMuPDF,精细活用pdfplumber”。
pypdf:如果只是简单的文本提取、PDF合并拆分、页数统计,pypdf是最轻量省事的选择。它纯Python实现,部署无依赖,但对复杂版式和中文支持都比较弱,只适合当辅助工具。比如在管线里判断“这个PDF多少页”“能不能提取出Title”,用pypdf就够了,没必要上重武器。
3.2 表格类PDF的专用方案
PDFMiner.six:它是pdfplumber的底层引擎,单独用比较原生态,需要手动处理布局分析,但自由度极高。如果你要做一个高度定制化的解析器,不想被上层库的抽象束缚,那直接操作PDFMiner.six是可行的路子。不过日常项目里我更推荐直接基于pdfplumber开发,省时省力。
Camelot:表格PDF的专用解。它内置了lattice和stream两种模式,lattice模式适合有明确表格线的标准报表,stream模式适合无边框但结构清晰的表格。原理上Camelot会用OpenCV做表格线检测,再用PDF文本信息做单元格填充。实测下来,对带框线生成的报表类PDF,Camelot的表现相当惊艳:
import camelot tables = camelot.read_pdf("financial_report.pdf", flavor="lattice") df = tables[0].df print(df)需要注意,Camelot对扫描版PDF无能为力,它必须有文本层配合,并且依赖较多,环境配置稍麻烦。
tabula-py:Java生态的tabula-java做Python封装,同样主攻表格提取。好处是能直接输出DataFrame,跟pandas/LanceDB这类的数据链路无缝衔接,但生产环境要额外装Java运行时,容器化部署时镜像会变重。我还是那句话:能少依赖就少依赖,项目上线后每个Java依赖都可能是运维的噩梦。
3.3 扫描件与复杂版式的两个重量级工具
PaddleOCR:前面已经详细说过,它不只是OCR,还带版面分析能力。PP-StructureV2能识别标题、段落、表格、图片这些版面区块,直接输出结构化的解析结果,这是传统OCR工具做不到的。在RAG场景里,版面信息非常有用——标题可以先变成层级语义,段落按顺序组装成chunk,这比一堆平铺的文字块要容易理解得多。
RapidOCR:如果你不想引入PaddlePaddle重框架,RapidOCR是绝佳替代。它把PP-OCR模型转换成ONNX格式运行,API设计轻量,CPU就能跑得动,部署起来极其顺滑:
from rapidocr_onnxruntime import RapidOCR engine = RapidOCR() result, _ = engine("scan_page.png") for line in result: print(line[1]) # 识别文本RapidOCR和PaddleOCR的识别能力基本同源,速度上在CPU场景下甚至更快,做批量OCR时我通常优先选它。
marker-pdf:这个工具是PDF转换圈的“新贵”,它的目标很纯粹——把PDF直接转成结构化Markdown。内部集合了文字提取、版面分析、公式识别多个模型,对研究论文、技术手册尤其友好,输出自带标题层级、列表和表格。实际用下来,对复杂版面的处理效果比传统规则类工具好很多,但首次运行需要下载模型权重,GPU环境下体验更佳。它非常适合作RAG解析管线的“语义通道”备选方案。
3.4 九种工具选型速查表
| 工具 | 定位 | 强项 | 硬伤 | 推荐指数 |
|---|---|---|---|---|
| PyMuPDF | 全能型文本提取 | 速度快、能渲染、功能全 | 复杂版面顺序易乱 | ★★★★★ |
| pdfplumber | 精度型文本分析 | 字符级坐标、表格提取 | 速度慢 | ★★★★☆ |
| pypdf | 轻量辅助 | 合并拆分、简单提取 | 中文弱、版式弱 | ★★★☆☆ |
| PDFMiner.six | 底层引擎 | 高度可定制 | 开发成本高 | ★★★☆☆ |
| Camelot | 表格专用 | 表格结构化好 | 必须文本层、依赖重 | ★★★★☆ |
| tabula-py | 表格专用 | 直接输出DataFrame | 依赖Java | ★★★☆☆ |
| PaddleOCR | OCR+版面分析 | 中文强、版面结构化 | 依赖PaddlePaddle | ★★★★★ |
| RapidOCR | 轻量OCR引擎 | 部署简单、CPU可跑 | 版面分析弱 | ★★★★☆ |
| marker-pdf | PDF转Markdown | 复杂版面输出结构化 | 模型大、需GPU | ★★★★☆ |
4. 一套可直接落地的解析管线实战
4.1 文件入口与类型探测
管线入口处先做统一检查:校验文件确实是PDF(看文件头是不是%PDF-),读取页数,然后逐页体检。这一步我的经验是,不要只判断“有没有文本”,还要看“文本质量高不高”。有些PDF虽然能提取出文字,但全是CID字体映射错乱后的乱码,直接把这段文本送进Embedding模型,等于给知识库塞垃圾。
体检逻辑可以这样写:
import fitz def inspect_pdf(path): doc = fitz.open(path) page_info = [] for page_no, page in enumerate(doc): text = page.get_text("text").strip() img_list = page.get_images(full=True) page_info.append({ "page_no": page_no, "char_count": len(text), "image_count": len(img_list), "has_text_layer": len(text) > 50 }) return page_info统计完这些指标,再套用规则:has_text_layer=True走文本通道,False但图片数大于0走OCR通道,两者交叉的页面标记为“混合型”,后续再判断是否触发语义通道。这套探路逻辑写好之后,管线就能按页路由,而不是按文件一刀切。
4.2 解析器与文本后处理的实现细节
分页路由确定之后,每页解析出来的文本不能直接拼起来用。PDF里无处不在的页眉、页脚、页码、以及排版造成的换行断词,都会污染最终送入分块器的文本。我通常在解析完成后加一层后处理清洗,用坐标信息和正则把干扰去掉:
import re def clean_extracted_text(text, page_height): lines = text.splitlines() cleaned = [] for line in lines: # 简单去掉页眉页脚:根据关键内容或坐标范围过滤 if re.match(r"^\s*第\s*\d+\s*页", line): continue if line.strip() == "": continue cleaned.append(line.strip()) # 把断行拼成段落(中文场景下,行尾无明显标点说明句子未完) paragraph = "" result = [] for line in cleaned: if not line.endswith(("。", ",", ";", ":", "!", "?", "、", "”")): paragraph += line else: paragraph += line result.append(paragraph) paragraph = "" return result这里有个很重要的细节:文本通道的结果,尽量用解析工具输出的“blocks”模式而不是“text”模式。blocks模式自带坐标信息和文本块边界,做版式重组时能按物理位置排序,比原始的线性文本要可靠得多。
OCR通道的后处理则不同,重点关注识别文字的置信度。PaddleOCR和RapidOCR都会返回识别置信度,低于0.8的文本块要单独标记,后续要么人工复核,要么丢给多模态大模型重新识别。
4.3 chunk切分与元数据设计
解析完的文本进入分块阶段。这里很多人会犯一个错误:拿一个定长的滑动窗口从头切到尾,完全不管文档结构。这样切出来的chunk经常把标题和正文分离,或者在一个chunk里混进两个不相干的主题,检索时相关性和准确率都会打折扣。
我推荐“结构优先、定长兜底”的策略:
- 先用解析出的标题层级(H1/H2/H3,marker-pdf能直接给出)作为分块的锚点;
- 在锚点之间,根据token数做二次切分,
chunk_size=800、overlap=100左右起步; - 如果一个章节超长,再按段落边界切细。
分块的同时,一定给每个chunk带上元数据。我实际用的metadata至少包含这些字段:
{ "source_file": "合同编号xxx.pdf", "page_no": 12, "page_image_path": "s3://bucket/xxx第12页.png", "parser_used": "pdfplumber_v1.0, paddleocr_v3.0", "chunk_type": "paragraph", "title_chain": ["第三章", "合同价款"] }为什么要记parser_used?检索阶段如果发现某条结果质量差,你能顺着元数据回查这条chunk是哪条解析链路产出的,是文本通道还是OCR通道,出了问题可以精准修管线。没有这个信息,你只能对着整体效果干瞪眼,排查效率极低。
5. 常见问题与排错私货
5.1 高频问题速查表
| 问题现象 | 根因 | 解决方案 |
|---|---|---|
| 提取到一堆乱码/方框 | CID字体映射错误 | 把该页渲染成图片,改用OCR通道 |
| OCR结果文本顺序错乱 | 多栏排版或图文交错 | 用版面分析获取分栏坐标,按列优先排序 |
| 扫描件是倒的/转了个方向 | 原件方向不统一 | 启用方向分类器,PaddleOCR自带此功能 |
| 表格提取出来全是错位 | 表格线断裂或表格无边框 | 换flavor(lattice/stream),或改走多模态 |
| 文本被莫名截断 | 页面有文本层但缺失字符 | 对比文本层与渲染图,缺失页走OCR |
| 解析速度太慢 | 全量走OCR/大模型 | 先分类,文本型别过OCR,复杂页才走大模型 |
| 图片内容在知识库里“消失”了 | 只提取文本,没有为图片生成描述 | 图片存对象存储,路径+描述写进文本chunk |
5.2 几个花了很多时间才搞明白的细节
第一,文本型PDF的“文本顺序”不一定等于“阅读顺序”。PDF允许内容对象乱序存储,尤其从InDesign这类排版软件导出的文档,线性提取会把正文和边栏文字穿插在一起。解决思路是拿坐标聚类:先按x坐标把文字分到不同栏,再在每个栏内按y坐标排序,最后按从左到右、从上到下的顺序拼接。这套“坐标分层排序法”我写进管线后,多栏文档的解析混乱率直接下降了一截。
第二,OCR不是图像处理之后就能一劳永逸。图像预处理里,二值化阈值的选择对浅色文字和复杂背景影响极大,我建议保留三份预处理结果(原图、灰度图、二值化图),OCR识别时逐个尝试取最高置信度。这个“多版本投票”的做法虽然笨,但很管用。
第三,也是最隐蔽的一个坑:pdfplumber和PyMuPDF拿到的文本,有些字符在Unicode层面是合法的,但实际是字体里自定义映射的“私用区字符”,喂给LLM时会变成看不懂的符号。这种问题没法用正则解决,唯一靠谱的办法是拿标准词典比对一遍,剔除不在常用词表里的字符。我之前处理一批日文排版软件导出的PDF,就是被这种“合法乱码”卡了整整一天。
5.3 给工具选型的一句实在话
我个人的体会是,PDF解析工具没有“最好”,只有“适合”。适合的标准不是功能多寡,而是能不能融入你的现有管线、维护成本高不高、出问题时能不能快速排查。PyMuPDF加pdfplumber的组合能覆盖90%以上常规项目,PaddleOCR或RapidOCR解决中文扫描件,再留一个多模态大模型或者marker-pdf当精修后手,这套组合打到今天还没遇到真正过不去的坎。
最后分享一个小建议:任何PDF解析管线,都一定要把中间产物(提取的文本、坐标信息、页面渲染图)缓存下来。很多人把解析当作一次性任务,跑完就扔,等到Retrieval效果不理想时想回溯,才发现原始解析结果已经没了,只能重新翻工。把这些中间结果按文件路径和页面号存好,不管是排查问题、调优Embedding还是重新切分chunk,都能省下一大半时间。数据导入这件事,前期多花十分钟整理缓存,后期能省下十个小时的返工。