1. 为什么图文与 PDF 解析是 RAG 系统最容易被低估的环节
做过 RAG 项目的人大多有个共同体会:向量库选型、检索策略、重排序模型这些"上层建筑"讨论得热火朝天,但真正让系统效果拉胯的,往往是数据导入这一层。尤其是当你的知识库里混进了扫描件、产品手册、财报 PDF、带表格的技术文档、甚至带公式的学术论文时,纯文本抽取那一套就直接歇菜了。
我前后搭过三套不同规模的 RAG 知识库,从个人文档助手到企业内部制度问答,踩得最狠的坑几乎都集中在 PDF 解析这一段。一份 200 页的产品手册,用普通工具抽出来是一堆乱码加错位的表格;一份扫描版合同,文字层根本不存在,不做 OCR 就是空白;一份带流程图的运维手册,图里的关键信息全丢了,检索时用户问"那个流程图里第三步是什么"直接答不上来。这些问题不是靠换个 embedding 模型能解决的,根子就在解析环节。
这篇是"RAG 数据导入与解析"系列的第二篇,专门聊图文和 PDF 这两块硬骨头。核心要解决三件事:第一,扫描件和图片里的文字怎么可靠地提取出来(OCR 路线);第二,图表、公式、版面结构这些非纯文本信息怎么保留(多模态大模型路线);第三,市面上九种主流 PDF 解析工具到底该怎么选,各自的适用边界在哪。适合正在搭 RAG 知识库、被 PDF 折磨过的工程师,也适合刚入门想少走弯路的朋友。我会把每种方案的原理、实操代码、参数选择、踩坑经验都摊开讲,尽量让你看完就能直接抄作业。
先说一个贯穿全文的判断:没有万能工具,只有匹配场景的组合拳。你不可能指望一个工具同时搞定高清扫描件、复杂表格、多栏排版和公式识别,正确的做法是先对文档做分类,再针对每一类走不同的解析管线。这个思路后面会反复出现。
2. 图文解析的两条技术路线:传统 OCR 与多模态大模型
2.1 传统 OCR 路线的能力边界与适用场景
传统 OCR 的本质是"检测文字区域 + 识别字符",它输出的是纯文本流,对版面结构的理解非常有限。这条路线成熟、便宜、速度快,但有几个硬伤你得心里有数。
第一个硬伤是版面丢失。OCR 引擎通常按行或按块返回文字,多栏排版的 PDF 被 OCR 之后,左右栏的文字可能被交错拼接,读起来前言不搭后语。表格更惨,行列关系直接塌缩成一串文字,原本"某产品 2024 年 Q3 营收 1.2 亿"这种结构化信息,变成"产品 2024 Q3 营收 1.2 亿"散落在不同行,检索时语义完全对不上。
第二个硬伤是对图像质量极度敏感。倾斜、阴影、低分辨率、手写体、艺术字,任何一个因素都能让识别率断崖式下跌。我实测过一批手机拍的问卷照片,正面光照均匀的识别率能到 95% 以上,稍微有点反光或者角度偏一点,直接掉到 70% 以下,而且错的地方往往是关键字段(比如金额、日期)。
第三个硬伤是语言和字体覆盖。热词里有人提到"OCR 代码识别不了韩文",这就是典型的语言包问题。PaddleOCR 默认加载的是中英文模型,要识别韩文得显式指定韩文识别模型,否则它会把韩文字符当成乱码或者直接跳过。类似的问题在日文、阿拉伯文、以及各种特殊字体上都会出现。
那传统 OCR 还有没有价值?当然有。纯文字扫描件、票据、身份证、验证码这类场景,传统 OCR 依然是最优解,因为快、准、成本低。关键是你要清楚它的边界,别拿它去啃复杂版面。
2.2 多模态大模型路线:让模型"看懂"而不只是"认出"
多模态大模型(比如能同时处理图像和文本的模型)带来的最大变化,是它不再把图片当成"待识别的字符集合",而是当成"需要理解的内容"。你给它一张财报截图,它不只是把字读出来,还能告诉你"这是一张利润表,第一列是项目名称,第二列是本期金额,第三列是上期金额,其中营业收入同比增长了 15%"。
这条路线的优势非常明显:版面理解、表格结构还原、图表语义描述、公式识别,它都能干。对于 RAG 来说,这意味着你可以把一张复杂的图表转成一段结构清晰的文字描述,再去做 embedding,检索命中率会高出一大截。
但代价也很实在。第一是成本,多模态模型的调用费用远高于传统 OCR,一张图几毛钱到几块钱不等,文档量大起来账单很吓人。第二是速度,单张图的推理时间通常是秒级,批量处理几千页文档得有耐心。第三是稳定性,大模型会有幻觉,偶尔会"脑补"出图里根本没有的内容,这在财务、法律这类严谨场景里是致命的。
所以我的建议是:多模态大模型用在"高价值、低数量"的文档上,比如核心制度文件、关键合同、重要图表;海量的普通文档还是走传统 OCR 或者规则解析,把成本压下来。
2.3 两条路线怎么选:一张决策表说清楚
| 维度 | 传统 OCR | 多模态大模型 |
|---|---|---|
| 输入类型 | 纯文字扫描件、票据、验证码 | 复杂版面、表格、图表、公式 |
| 输出形式 | 纯文本流 | 结构化文本 + 语义描述 |
| 单页成本 | 极低(本地部署近乎为零) | 较高(按调用量计费) |
| 处理速度 | 快(毫秒到秒级) | 慢(秒级到十秒级) |
| 版面还原 | 差 | 好 |
| 幻觉风险 | 无 | 有,需校验 |
| 适合场景 | 大批量简单文档 | 小批量高价值文档 |
实操中的常见做法是混合管线:先用轻量工具判断文档类型(有没有文字层、是不是扫描件、版面复杂度如何),再路由到不同的解析器。这个思路在后面的工具选型里会具体展开。
3. 九种 PDF 解析工具选型实战
3.1 工具选型的三个核心判断维度
在列具体工具之前,先明确选型的判断标准,不然容易陷入"哪个火用哪个"的误区。我一般看三个维度。
第一是文档类型适配度。你的 PDF 是原生电子版(有文字层)还是扫描版(纯图片)?是单栏还是多栏?有没有复杂表格和公式?原生电子版用规则解析工具就够了,扫描版必须上 OCR,复杂版面得上版面分析或者多模态。
第二是输出结构质量。好的解析工具应该输出带层级结构的 Markdown 或 JSON,保留标题、段落、列表、表格的语义。如果只输出一坨纯文本,后面还得自己写规则去切分,工作量翻倍。
第三是部署与成本。本地部署的工具(比如开源的)没有调用费用,但要考虑硬件和维护成本;云服务省心但有按量计费,数据敏感的场景还得考虑合规。
3.2 九种工具逐一拆解与适用边界
下面这九种是我实际用过或者深度评估过的,覆盖了从轻量到重量、从开源到商用的主要类型。我不按排名列,按"适用场景"来分组说。
第一类:原生电子版 PDF 的快速抽取
PyMuPDF(也叫 fitz)是我处理原生 PDF 的首选。它速度快、依赖少、能拿到文字、图片、坐标、字体信息。对于有文字层的 PDF,几行代码就能把全文抽出来。缺点是它不做版面理解,多栏文档抽出来顺序会乱,表格也还原不了。适合做预处理和快速验证。
pdfplumber 在表格抽取上比 PyMuPDF 强不少,它能识别表格的线条和单元格边界,输出结构化的表格数据。我处理财报里的数据表基本都用它。缺点是速度慢,大文档处理起来有点磨人。
PyPDF2(现在叫 pypdf)是最基础的库,功能有限,主要用来做页面操作和简单文本抽取。现在新项目我基本不用了,除非只是想把 PDF 拆页或者合并。
第二类:扫描件 OCR 解析
PaddleOCR 是国内用得最多的开源 OCR 方案,中文识别效果好,支持多语言(记得显式指定语言模型),有版面分析能力。热词里那个"识别不了韩文"的问题,就是因为没指定韩文模型。它的 PP-Structure 模块能做版面分析和表格识别,对 RAG 很友好。
Tesseract 是老牌开源 OCR,语言包丰富,但中文识别效果不如 PaddleOCR,版面分析能力也弱。适合英文文档或者对精度要求不高的场景。
第三类:复杂版面与结构化解析
MinerU(原 PDF-Extract-Kit)是这两年很火的开源方案,专门针对学术论文和复杂版面,能识别公式、表格、图片,输出 Markdown。对科研类 RAG 知识库非常合适。
Marker 是另一个开源选手,基于深度学习模型做版面分析和文字识别,输出质量高,但显存占用大,需要 GPU。
Unstructured 是一个文档处理框架,支持 PDF、Word、HTML 等多种格式,能输出带元素类型(标题、正文、表格)的结构化数据。它的定位是"通用文档 ETL",适合做统一的数据导入管线。
第四类:多模态大模型解析
这一类没有固定工具名,指的是调用多模态大模型 API 来做解析。适合高价值文档,能输出语义化的描述。成本高,需要做结果校验。
第五类:商用 PDF 编辑器/解析服务
像福昕、搜狗 PDF 编辑器这类工具,主要面向人工编辑场景,OCR 语言包需要单独下载(热词里提到的 ocr-zh-cn.fzip 就是福昕的中文语言包)。它们不太适合做自动化批量解析,但在人工校对环节有用。
3.3 工具组合的推荐配置
基于上面的分析,我给两套推荐配置。
轻量方案(个人/小团队):PyMuPDF 做原生 PDF 抽取 + PaddleOCR 做扫描件 OCR + pdfplumber 处理表格。全本地部署,零调用成本,适合文档量不大、预算有限的场景。
进阶方案(企业/大批量):Unstructured 做统一入口和类型路由 + MinerU 处理复杂版面 + 多模态大模型处理高价值文档。这套组合覆盖全面,但需要一定的工程投入和 GPU 资源。
4. 从零搭建图文与 PDF 解析管线
4.1 环境准备与依赖安装
先把基础环境搭起来。我习惯用 conda 建独立环境,避免依赖冲突。
conda create -n rag-parse python=3.10 -y conda activate rag-parse # 基础 PDF 处理 pip install pymupdf pdfplumber pypdf # OCR 方案 pip install paddlepaddle paddleocr # 如果有 GPU,装 GPU 版本 # pip install paddlepaddle-gpu paddleocr # 复杂版面解析 pip install magic-pdf[full] # MinerU 的包名 # 通用文档处理 pip install unstructuredPaddleOCR 第一次运行会自动下载模型,国内网络可能慢,可以提前配置模型下载源。MinerU 对显存有要求,建议至少 8G 显存,纯 CPU 也能跑但很慢。
4.2 文档类型自动识别与路由
这是整个管线的第一道关卡,判断文档该走哪条解析路线。核心逻辑是:先看有没有文字层,再看版面复杂度。
import fitz # PyMuPDF def analyze_pdf(pdf_path): """分析 PDF 类型,返回路由建议""" doc = fitz.open(pdf_path) total_pages = len(doc) text_pages = 0 total_chars = 0 for page in doc: text = page.get_text().strip() if len(text) > 50: # 有实质文字内容 text_pages += 1 total_chars += len(text) doc.close() text_ratio = text_pages / total_pages if total_pages > 0 else 0 avg_chars = total_chars / text_pages if text_pages > 0 else 0 if text_ratio > 0.8 and avg_chars > 200: return "native" # 原生电子版,走规则解析 elif text_ratio < 0.2: return "scanned" # 扫描件,走 OCR else: return "mixed" # 混合型,逐页判断这个判断逻辑的关键参数是text_ratio和avg_chars。阈值不是死的,我一般把文字层比例 0.8 作为原生文档的门槛,低于 0.2 判定为扫描件。中间地带就是混合型,需要逐页处理。实测下来这套判断对绝大多数文档都准,偶尔有误判的(比如文字层是乱码的伪原生 PDF),可以在后续环节加校验。
4.3 原生 PDF 的高质量文本抽取
原生 PDF 用 PyMuPDF 抽取,但要注意保留结构。直接get_text()拿到的是纯文本,标题和正文混在一起。更好的做法是用get_text("dict")拿到带字体、字号、坐标的块信息,再根据字号大小判断标题层级。
def extract_native_pdf(pdf_path): doc = fitz.open(pdf_path) result = [] for page_num, page in enumerate(doc): blocks = page.get_text("dict")["blocks"] for block in blocks: if block.get("type") != 0: # 跳过图片块 continue for line in block.get("lines", []): for span in line.get("spans", []): text = span["text"].strip() if not text: continue font_size = span["size"] # 根据字号判断层级,字号大的当标题 if font_size > 16: level = "#" elif font_size > 13: level = "##" else: level = "" result.append({ "page": page_num + 1, "text": text, "size": font_size, "level": level }) doc.close() return result字号阈值(16 和 13)需要根据具体文档调整,不同模板的标题字号不一样。我的经验是先抽几页看看字号分布,再定阈值。这个方法的局限是遇到用加粗而非字号区分标题的文档会失效,那种情况得结合字体名(比如是否包含 Bold)来判断。
4.4 扫描件的 OCR 解析与多语言处理
扫描件走 PaddleOCR。针对热词里提到的韩文识别问题,关键是初始化时指定语言。
from paddleocr import PaddleOCR # 中文文档 ocr_zh = PaddleOCR(use_angle_cls=True, lang='ch') # 韩文文档 ocr_ko = PaddleOCR(use_angle_cls=True, lang='korean') # 英文文档 ocr_en = PaddleOCR(use_angle_cls=True, lang='en') def ocr_image(img_path, lang='ch'): ocr = PaddleOCR(use_angle_cls=True, lang=lang) result = ocr.ocr(img_path, cls=True) texts = [] for line in result[0]: text = line[1][0] confidence = line[1][1] if confidence > 0.7: # 过滤低置信度结果 texts.append(text) return "\n".join(texts)use_angle_cls=True会启用方向分类,能处理旋转的文本,对扫描件很有用。置信度阈值 0.7 是我常用的过滤线,低于这个值的识别结果错误率明显偏高,宁可丢掉也别污染知识库。
对于 PDF 扫描件,得先把每页转成图片再 OCR。PyMuPDF 可以直接渲染页面为图片:
def pdf_to_images(pdf_path, dpi=200): doc = fitz.open(pdf_path) images = [] for page in doc: # dpi 越高越清晰,但处理越慢 pix = page.get_pixmap(dpi=dpi) img_bytes = pix.tobytes("png") images.append(img_bytes) doc.close() return imagesDPI 的选择是个权衡。200 是常用值,清晰度和速度平衡得不错;扫描质量差的文档可以提到 300,但处理时间会明显增加。低于 150 的话小字容易糊,识别率下降。
4.5 复杂版面与表格的结构化还原
表格是 RAG 解析里最头疼的部分。pdfplumber 的表格抽取能力不错,但需要调参。
import pdfplumber def extract_tables(pdf_path): tables = [] with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: page_tables = page.extract_tables({ "vertical_strategy": "lines", "horizontal_strategy": "lines", "snap_tolerance": 3, "join_tolerance": 3, }) for table in page_tables: # 转成 Markdown 表格,方便后续 embedding md_table = to_markdown(table) tables.append(md_table) return tables def to_markdown(table): if not table or not table[0]: return "" header = "| " + " | ".join(str(c or "") for c in table[0]) + " |" separator = "| " + " | ".join(["---"] * len(table[0])) + " |" rows = [] for row in table[1:]: rows.append("| " + " | ".join(str(c or "") for c in row) + " |") return "\n".join([header, separator] + rows)vertical_strategy和horizontal_strategy设为lines表示按表格线识别,适合有边框的表格。没有边框的表格得改成text策略,靠文字对齐来推断,准确率会低一些。snap_tolerance和join_tolerance是容差参数,表格线不规整时可以适当调大。
把表格转成 Markdown 是个很实用的技巧,因为 Markdown 表格保留了行列结构,embedding 之后检索"某产品某季度的营收"这类问题命中率会高很多。相比之下,纯文本流里的表格数据基本检索不出来。
4.6 多模态大模型处理高价值文档
对于图表、公式、复杂版面这类传统工具搞不定的内容,上多模态大模型。核心思路是把页面渲染成图片,连同提示词一起发给模型,让它输出结构化的 Markdown。
import base64 def multimodal_parse(image_bytes, client): img_b64 = base64.b64encode(image_bytes).decode() prompt = """请将这张文档图片转换为结构化的 Markdown 格式。 要求: 1. 保留标题层级,用 # ## ### 表示 2. 表格用 Markdown 表格还原,保持行列对应 3. 图表用文字描述其内容和关键数据 4. 公式用 LaTeX 表示 5. 不要添加图片中不存在的内容""" response = client.chat.completions.create( model="your-multimodal-model", messages=[{ "role": "user", "content": [ {"type": "text", "text": prompt}, {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img_b64}"}} ] }] ) return response.choices[0].message.content提示词里那句"不要添加图片中不存在的内容"很重要,能一定程度上抑制幻觉。但即便如此,关键数据还是得做校验,尤其是数字。我的做法是把多模态输出的数字和传统 OCR 的结果做交叉比对,不一致的标记出来人工复核。
4.7 解析结果的清洗与分块
解析完不等于能直接入库,还得清洗和分块。清洗主要处理 OCR 噪声(乱码、多余空格、断行)、页眉页脚、页码这些干扰内容。分块则决定了后续检索的粒度。
import re def clean_text(text): # 去掉连续空白 text = re.sub(r'\s+', ' ', text) # 去掉常见页眉页脚模式 text = re.sub(r'第\s*\d+\s*页', '', text) text = re.sub(r'Page\s*\d+', '', text) # 修复被 OCR 拆断的英文单词 text = re.sub(r'(\w)-\s+(\w)', r'\1\2', text) return text.strip() def chunk_by_structure(elements, max_chunk_size=500): """按结构分块,标题作为分块边界""" chunks = [] current = {"title": "", "content": []} for elem in elements: if elem["level"]: # 是标题 if current["content"]: chunks.append(current) current = {"title": elem["text"], "content": []} else: current["content"].append(elem["text"]) # 内容过长时切分 if sum(len(c) for c in current["content"]) > max_chunk_size: chunks.append(current) current = {"title": current["title"], "content": []} if current["content"]: chunks.append(current) return chunks按结构分块比固定长度分块效果好很多,因为标题天然是语义边界。每个 chunk 带上所属标题作为上下文,检索时语义更完整。max_chunk_size设 500 字左右是个经验值,太大检索不精准,太小语义不完整。
5. 常见问题与排查技巧实录
5.1 OCR 识别率低的排查思路
OCR 识别率低是最常见的问题,排查要按顺序来。先看图像质量,把页面渲染的 DPI 提上去试试,很多时候就是分辨率不够。再看语言设置,中文文档用了英文模型、韩文文档没指定韩文模型,都会导致识别失败。然后看预处理,倾斜的图片先做纠偏,有阴影的先做二值化,PaddleOCR 内置了部分预处理但效果有限,必要时用 OpenCV 自己处理。
还有一个容易被忽略的点是字体。某些 PDF 用了特殊嵌入字体,渲染出来是正常的,但 OCR 引擎没见过这种字形,识别率就低。这种情况只能换引擎或者上多模态。
5.2 表格解析错位的典型原因
表格解析错位通常有三个原因。一是表格没有边框线,靠文字对齐推断行列时容易串行,解决办法是调大容差参数或者改用多模态。二是单元格内有换行,pdfplumber 会把一个单元格拆成多行,需要在后处理时合并。三是跨页表格,表格被分到两页,需要识别并拼接。跨页表格的处理比较麻烦,我的做法是检测页面顶部和底部是否有表格线,有的话尝试和相邻页拼接。
5.3 多模态大模型幻觉的识别与抑制
多模态幻觉的表现是输出里出现了图里没有的内容,或者数字被"改"了。识别方法是交叉验证,用传统 OCR 的结果做对照,重点核对数字、专有名词。抑制方法除了提示词约束,还可以要求模型对不确定的内容标注"不确定",或者分两次调用取一致结果。但最靠谱的还是关键字段人工复核,别全指望模型。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| OCR 输出乱码 | 语言模型不匹配 | 检查 lang 参数 |
| 识别率突然下降 | 图像 DPI 过低 | 提高渲染 DPI 到 300 |
| 表格行列错位 | 无边框或单元格换行 | 调容差或换多模态 |
| 多栏文字交错 | 版面分析缺失 | 用带版面分析的工具 |
| 多模态输出幻觉 | 模型脑补 | 交叉验证 + 人工复核 |
| 处理速度极慢 | 纯 CPU 跑深度学习模型 | 上 GPU 或换轻量方案 |
| 公式识别成乱码 | 工具不支持公式 | 用 MinerU 或多模态 |
5.5 几个我踩过的坑
第一个坑是盲目追求高 DPI。有次为了提升识别率把 DPI 拉到 600,结果处理时间翻了四倍,识别率只提升了不到 2 个百分点。后来发现 200 到 300 是性价比最高的区间,再高收益递减严重。
第二个坑是忽略 PDF 的加密和权限。有些 PDF 设了复制限制,PyMuPDF 直接抽会报错或者抽出来是空的。这种情况得先检查文档权限,必要时用有权限的账号重新导出。
第三个坑是分块时把表格切碎。早期我用固定长度分块,一个表格被切成好几段,检索时只能命中半张表。后来改成表格整体作为一个 chunk,问题就解决了。表格、代码块、公式这类结构化内容,分块时一定要保持完整。
第四个坑是没做解析结果的抽样校验。批量处理几千页文档,总有一些页面解析失败或者质量很差,如果不抽样检查,这些脏数据会悄悄污染知识库,等到检索效果差的时候再回头查,成本就高了。我的做法是每批随机抽 5% 人工看一眼,发现问题及时调整管线。
6. 管线性能优化与批量处理建议
6.1 并行化与缓存策略
文档量大起来,串行处理会慢到无法接受。我的做法是按文档类型分组,原生 PDF 用多进程并行(CPU 密集),OCR 和深度学习模型用 GPU 批处理。PyMuPDF 的抽取是 CPU 密集型的,用multiprocessing能线性提速;PaddleOCR 和 MinerU 走 GPU,靠批处理提升吞吐。
缓存也很关键。解析结果按文件哈希缓存,同一个文件重复处理时直接读缓存。开发调试阶段这个能省大量时间,因为调分块策略时不用反复跑解析。
6.2 质量监控与增量更新
生产环境的解析管线得有质量监控。我一般记录几个指标:解析成功率、平均每页字符数、OCR 平均置信度、表格识别数量。这些指标突然异常就说明有问题。增量更新则是只处理新增或修改的文档,靠文件哈希和修改时间判断,避免全量重跑。
6.3 不同规模场景的配置建议
个人用几十到几百份文档,轻量方案足够,本地跑跑就行。团队用几千到几万份,建议上 GPU 服务器,把 OCR 和复杂版面解析集中处理。企业级几十万份以上,得考虑分布式处理和任务队列,把解析、清洗、分块、入库拆成独立服务,各自扩容。
我个人在实际操作中的体会是,解析管线的投入产出比在 RAG 项目里被严重低估了。大家愿意花时间调检索和重排,却不愿意在数据导入上多花两天,结果就是垃圾进垃圾出。把解析这一层做扎实,后面很多"检索不准"的问题会自然消失。最后分享一个小技巧:建一个"解析质量样本集",挑几十份有代表性的文档(各种类型、各种质量),每次调整管线都拿它跑一遍对比效果,比盲目调参靠谱得多。