RAG中PDF 文档处理全流程指南——从加载、清洗、分块到入库

发布时间:2026/11/6 3:34:07

RAG中PDF 文档处理全流程指南——从加载、清洗、分块到入库 文章目录开篇为什么 PDF 是 RAG 的第一道坎整体流水线一览第一步PDF 加载与文本提取代码示例用 PyMuPDF 提取带位置信息的文本第二步文档清洗——去掉噪音怎么去页眉页脚多栏排版怎么处理第三步分块策略——RAG 的灵魂四种主流分块策略对比代码示例LangChain 递归分块实战chunk_size 和 overlap 怎么选第四步特殊内容处理表格RAG 中最容易丢失的信息图片和扫描件OCR 是必选项第五步元数据增强不同类型 PDF 的解决方案1. 正常文档 PDF直接跑通用流水线2. 带章节的 PDF按标题层级切分3. 扫描件 PDF先 OCR 再分块4. 带图片的 PDF提取图片并生成描述5. 带表格的 PDF转 Markdown 独立存储6. 左右分栏的 PDF按坐标重排常见坑与最佳实践开篇为什么 PDF 是 RAG 的第一道坎很多人刚接触 RAG 时觉得不就是读文档 → 切块 → 向量化三步走嘛。但真正上手做企业级项目就会发现PDF 文档的处理质量直接决定了 RAG 最终回答的准确率。为什么这么说因为 PDF 本质上是为人眼阅读设计的排版格式而不是为机器读取设计的。一个看起来排版整齐的 PDF背后可能是多栏排版导致文字提取后左右栏混在一起读起来驴唇不对马嘴页眉页脚、水印文字混进正文污染了向量检索的结果表格里的关键数据被拆成零散的行失去了行列对应关系扫描件里全是图片直接提取出来全是空白或乱码如果这些问题不解决后面再强的大模型也救不回来——因为垃圾进垃圾出。本文就把 RAG 中 PDF 处理的完整流水线拆给你看每一步都讲清楚为什么做、怎么做、代码怎么写。整体流水线一览先上一张全景图建立整体认知整个流程分为 5 个核心阶段下面我们一步步拆。第一步PDF 加载与文本提取这是最基础的一步但工具选不对后面全白搭。目前主流的 Python PDF 解析库有好几个各有侧重工具特点擅长场景注意事项PyMuPDF (fitz)速度最快能拿到每个文本块的精确坐标、字体大小需要按位置做排版分析、按标题层级切分的场景对扫描件无效需配合 OCRpdfplumber表格识别能力强能保留表格结构财务报表、数据表格类 PDF速度比 PyMuPDF 慢一些PyPDF2老牌库API 简单但提取效果粗糙纯文字、排版简单的 PDF多栏、表格基本废掉不推荐生产用unstructured开箱即用的文档分块框架自动识别标题、列表、表格快速搭建原型不想自己写解析逻辑依赖较重定制化程度低**选型建议**生产环境推荐PyMuPDF pdfplumber 组合——前者负责正文提取和位置分析后者负责表格识别。快速原型用unstructured一把梭。代码示例用 PyMuPDF 提取带位置信息的文本importfitz# PyMuPDFdocfitz.open(example.pdf)forpage_num,pageinenumerate(doc):# get_text(dict) 返回每个文本块的坐标、字体、大小等信息blockspage.get_text(dict)[blocks]forblockinblocks:ifblock[type]0:# 0 文本块, 1 图片块forlineinblock[lines]:forspaninline[spans]:textspan[text].strip()iftext:# span 里有 bbox(坐标)、size(字号)、font(字体) 等信息print(f[第{page_num1}页] 字号{span[size]:.1f}|{text})重点在于不要只用page.get_text()拿到一坨纯文本就完事。用dict模式提取你能拿到每个文字块的坐标、字号、字体——这些信息后面做按标题分块去页眉页脚时全靠它。第二步文档清洗——去掉噪音提取出来的原始文本往往很脏。常见的噪音有三类页眉页脚每一页都重复出现的章节名、页码、公司 logo 文字多栏错乱左栏文字读完一半突然跳到右栏逻辑断裂多余空白和特殊字符大量换行、换行符穿插在句子中间怎么去页眉页脚最靠谱的方法是基于位置过滤页眉页脚通常在页面顶部或底部固定区域字号也比正文小。利用第一步拿到的坐标和字号信息可以精准过滤。defis_header_footer(span,page_height):textspan[text].strip()y_posspan[bbox][1]# 文本块顶部 y 坐标font_sizespan[size]# 页面上方 8% 区域 字号偏小 → 页眉ify_pospage_height*0.08andfont_size10:returnTrue# 页面下方 6% 区域 → 页脚ify_pospage_height*0.94:returnTrue# 纯数字且在底部 → 页码iftext.isdigit()andy_pospage_height*0.9:returnTruereturnFalse# 使用时过滤掉页眉页脚文本clean_lines[]forspaninall_spans:ifnotis_header_footer(span,page.rect.height):clean_lines.append(span[text])多栏排版怎么处理学术论文、杂志类 PDF 经常是左右双栏。直接按顺序提取会把左栏末尾和右栏开头拼在一起语义全乱。解决思路是根据文本块的 x 坐标判断属于哪一栏先拼完左栏再拼右栏。defsort_blocks_by_column(blocks,page_width):midpage_width/2left_col,right_col[],[]forbinblocks:x_center(b[bbox][0]b[bbox][2])/2ifx_centermid:left_col.append(b)else:right_col.append(b)# 每栏内部按 y 坐标从上到下排序left_col.sort(keylambdab:b[bbox][1])right_col.sort(keylambdab:b[bbox][1])returnleft_colright_col# 先左后右拼接第三步分块策略——RAG 的灵魂分块Chunking是 RAG 中最关键的环节没有之一。为什么要分块因为向量检索是按相似度匹配的——你不能把一整本 200 页的书当成一个向量去搜那样检索精度会惨不忍睹。但块切多大怎么切这直接决定了检索质量。四种主流分块策略对比策略原理优点缺点固定长度分块按固定字符数硬切比如每 500 字一块简单粗暴实现快容易切断句子和语义效果差递归分块优先按段落→句子→词递归切分尽量保持完整语义LangChain 默认方案效果不错不感知文档结构章节边界可能被切断按标题分块识别 PDF 中的标题层级一/二/三级标题按章节切分语义边界清晰检索精准依赖标题识别准确度需要位置/字号信息语义分块用 embedding 计算相邻句子相似度在语义突变处切分最贴近人类理解的分块方式计算成本高速度慢代码示例LangChain 递归分块实战fromlangchain.text_splitterimportRecursiveCharacterTextSplitter# 假设你已经提取并清洗好了全文文本clean_text这里是清洗后的 PDF 全文文本……text_splitterRecursiveCharacterTextSplitter(chunk_size500,# 每块约 500 字符chunk_overlap50,# 相邻块重叠 50 字符避免切断上下文separators[# 优先按这些分隔符递归切分\n\n,# 先按段落\n,# 再按换行。,# 再按句号,,# 再按感叹句/疑问句,# 最后按逗号 # 实在不行按空格])chunkstext_splitter.split_text(clean_text)print(f共切出{len(chunks)}个块)print(f第 1 块预览{chunks[0][:100]}...)chunk_size 和 overlap 怎么选这是新手最常问的问题给一个经验值chunk_size300800 字符中文。太小→上下文不足大模型答不全太大→检索不精准噪声多。chunk_overlapchunk_size 的 10%15%。作用是让相邻块有重叠避免一句话被切在两块中间导致检索时上下文断裂。企业级文档推荐先试 500 / 50再根据实际检索效果调优。第四步特殊内容处理正文文本好处理难的是 PDF 里的非纯文本内容。这也是很多 RAG 效果差的隐藏原因。表格RAG 中最容易丢失的信息表格里的数据往往是用户最常问的内容“第三章的合格率是多少”。如果表格直接被当成纯文本提取行列关系会完全丢失。正确做法用 pdfplumber 识别表格 → 转成 Markdown 表格格式 → 作为一个独立 chunk 存入。importpdfplumberdefextract_table_as_markdown(page,table_index0):tablespage.extract_tables()ifnottables:returntabletables[table_index]# 转成 Markdown 格式lines[]fori,rowinenumerate(table):cells[str(cell).replace(\n, )ifcellelseforcellinrow]lines.append(| | .join(cells) |)ifi0:# 表头下面加分隔线lines.append(| | .join([---]*len(row)) |)return\n.join(lines)# 使用withpdfplumber.open(report.pdf)aspdf:pagepdf.pages[2]md_tableextract_table_as_markdown(page)print(md_table)提取出来的 Markdown 表格可以直接作为一个 chunk 存入向量库。大模型读 Markdown 表格的理解能力远好于一堆散乱的数字行。图片和扫描件OCR 是必选项如果你的 PDF 是扫描件比如盖章的合同、扫描的纸质文档前面所有基于文本提取的方法都会拿到空内容。这时候必须走 OCR 路线importfitzfromPILimportImageimportpytesseractimportiodefocr_pdf_page(page):# 把 PDF 页面渲染成高分辨率图片pixpage.get_pixmap(dpi300)imgImage.open(io.BytesIO(pix.tobytes(png)))# OCR 识别中文需指定 langchi_simtextpytesseract.image_to_string(img,langchi_sim)returntext# 判断是否为扫描件先试试文本提取拿不到文字就走 OCRdocfitz.open(scanned.pdf)forpageindoc:raw_textpage.get_text().strip()iflen(raw_text)20:# 文字太少 → 大概率是扫描件textocr_pdf_page(page)print(f[OCR] 第{page.number1}页{text[:100]}...)else:textraw_text**注意**OCR 准确率取决于扫描质量和语言包。正式项目建议用商用 OCR百度、阿里、腾讯云的 OCR APITesseract 适合简单场景和原型验证。第五步元数据增强很多人做完分块就直接向量化了但加上元数据能让检索效果上一个台阶。什么是元数据就是每个 chunk 的标签它来自哪个文档、哪一页、哪个章节、是正文还是表格。有了这些标签你就可以按文档名过滤检索范围“只在操作手册里找答案”按章节做加权前面的章节更重要知道答案出自第几页方便溯源引用chunks_with_meta[]fori,chunk_textinenumerate(chunks):chunks_with_meta.append({content:chunk_text,metadata:{source_file:国家电网安规.pdf,page:page_num1,# 来自第几页section:第三章 安全操作规程,# 所属章节chunk_index:i,# 块序号content_type:text# text / table / image_caption}})# 存入向量数据库时metadata 字段一起存# 检索时可以用 metadata 做过滤条件不同类型 PDF 的解决方案上面讲的是通用流水线但真实世界的 PDF 千差万别。拿到一份 PDF先判断它属于下面哪一类或哪几类叠加再针对性选方案能省掉大量无效解析。先给一张识别特征 推荐方案速查表方便对号入座PDF 类型识别特征推荐方案关键点正常文档 PDF纯文字、排版规整、无多栏无表格PyMuPDF 提取 递归分块最省心直接跑通用流水线带章节的 PDF有明显标题层级、目录、章节号按标题层级分块 section 元数据用位置/字号识别标题扫描件 PDF每页是图片文本提取为空OCR渲染页面 → 文字识别先判断再走 OCR避免白干带图片的 PDF页面含插图、照片、示意图图片单独提取 描述/OCR图片转 caption 作独立 chunk带表格的 PDF含数据表、报表、矩阵pdfplumber 提取转 Markdown表格独立 chunk标 content_typetable左右分栏的 PDF论文/杂志式双栏排版按 x 坐标分栏重排先拼完左栏再拼右栏重要一个真实 PDF 往往是多类型叠加比如带章节 带表格 左右分栏的学术论文。方案不是单选而是按优先级组合——先分栏、再识别标题、再单独抽表格最后才走通用分块。1. 正常文档 PDF直接跑通用流水线这类 PDF 是教科书级的简单纯文字、单栏、排版规整。直接用第一步的 PyMuPDF 提取 第三步的递归分块即可不需要任何特殊处理。唯一要做的是去页眉页脚避免污染。importfitzfromlangchain.text_splitterimportRecursiveCharacterTextSplitter docfitz.open(normal.pdf)full_text\n.join(page.get_text()forpageindoc)splitterRecursiveCharacterTextSplitter(chunk_size500,chunk_overlap50,separators[\n\n,\n,。,,,, ])chunkssplitter.split_text(full_text)# 完事2. 带章节的 PDF按标题层级切分教材、规范、操作手册这类 PDF 有清晰的章节结构。如果按固定长度硬切章节边界会被切断检索第三章的内容时容易串到别处。正确做法是识别标题层级以章节为天然分块边界。识别标题靠的是第一步拿到的字号和位置标题通常字号比正文大、有章节号且独占一行。defdetect_headings(spans,body_size):headings[]forspaninspans:textspan[text].strip()# 字号明显大于正文 开头是章节号(如 3.2 或 第三章) → 判定为标题ifspan[size]body_size*1.2and(text[:2].replace(.,).isdigit()ortext.startswith(第)andtext.endswith(章)):headings.append({text:text,y:span[bbox][1]})returnheadings# 得到所有标题后按标题把正文切成标题 该节内容的 chunk# 每个 chunk 的 metadata[section] 记为所属章节便于按章节过滤检索3. 扫描件 PDF先 OCR 再分块盖章合同、纸质扫描件这类 PDF每一页其实都是一张图片。直接get_text()提取出来基本是空串。判断方法很简单提取的文字长度过短就判定为扫描件转走 OCR。importfitz,iofromPILimportImageimportpytesseractdefextract_page_text(page):rawpage.get_text().strip()iflen(raw)20:# 文字太少 → 扫描件pixpage.get_pixmap(dpi300)# 高分辨率渲染成图imgImage.open(io.BytesIO(pix.tobytes(png)))rawpytesseract.image_to_string(img,langchi_sim)returnraw docfitz.open(scanned.pdf)forpageindoc:print(extract_page_text(page))OCR 之后得到的是纯文本再接第三步的递归分块即可。生产环境建议用商用 OCR百度/阿里/腾讯识别率远高于 Tesseract。4. 带图片的 PDF提取图片并生成描述产品手册、课件里常有插图——流程图、示意图、架构图。这些图里的信息用纯文本提取不到会直接漏掉用户可能想问的内容。方案是把每张图片单独提取出来用 OCR 或多模态模型生成一段文字描述caption作为独立 chunk 入库。importfitzdefextract_images(page):images[]forimginpage.get_images(fullTrue):xrefimg[0]basedoc.extract_image(xref)images.append({bytes:base[image],ext:base[ext],bbox:page.get_image_bbox(xref)# 图片位置})returnimages# 对每张图片# 方案AOCR 提取图内文字适合含文字的架构图/截图# 方案B用多模态模型(VLM)生成一句话描述如# 图系统架构图包含数据层、模型层、应用层三层结构# 把 caption 作为独立 chunk 入库metadata[content_type]image_caption5. 带表格的 PDF转 Markdown 独立存储财务表、参数表、配置表里的数据用户常问“合格率多少”“税率几档”。如果被当成纯文本行列关系全丢。方案在第四步已讲过用 pdfplumber 识别表格 → 转成 Markdown → 作为独立 chunk。importpdfplumberdeftable_to_markdown(table):lines[]fori,rowinenumerate(table):cells[str(c).replace(\n, )ifcelseforcinrow]lines.append(| | .join(cells) |)ifi0:lines.append(| | .join([---]*len(row)) |)return\n.join(lines)withpdfplumber.open(report.pdf)aspdf:forpnum,pageinenumerate(pdf.pages):fortidx,tableinenumerate(page.extract_tables()):chunk{content:table_to_markdown(table),metadata:{source:report.pdf,page:pnum1,content_type:table}}大模型读 Markdown 表格的理解能力远好于读一堆散乱的数字行——这是表格类检索质量提升最明显的一步。6. 左右分栏的 PDF按坐标重排学术论文、杂志常见的双栏排版是 RAG 的隐形杀手直接按顺序提取会得到左栏结尾 右栏开头的乱序文本语义断裂。方案是按文本块的 x 坐标判断归属栏先拼完整左栏、再拼右栏。defreorder_columns(blocks,page_width):midpage_width/2left,right[],[]forbinblocks:x_center(b[bbox][0]b[bbox][2])/2(leftifx_centermidelseright).append(b)# 每栏内部按 y 坐标从上到下排序left.sort(keylambdab:b[bbox][1])right.sort(keylambdab:b[bbox][1])# 输出先左后右读起来才符合原文逻辑returnleftright重排后再进入清洗和分块多栏的语义断裂问题就解决了。常见坑与最佳实践最后总结几个踩过的坑帮你少走弯路坑表现解决方案坑 1分块切断句子检索出来的 chunk 开头是半句话大模型读不懂上下文用递归分块器 overlap 重叠区优先按句号/段落切分坑 2表格信息丢失用户问报表数据大模型回答文档中未找到相关信息单独提取表格转 Markdown作为独立 chunk 存储坑 3扫描件提取为空向量化后检索完全搜不到内容先判断是否为扫描件是的话走 OCR 路线坑 4chunk 太大检索出来的块包含大量无关内容大模型被干扰控制在 300-800 字用 MMR 或重排序过滤坑 5忽略元数据多文档场景下检索结果来源混乱无法溯源每个 chunk 都带 source、page、section 标签PDF 处理是 RAG 系统中脏活累活最多的一环但也是投入产出比最高的一环。把这五步做扎实你的 RAG 回答质量至少能提升一个档次。建议从最简单的版本开始跑通流程再逐步针对自己的文档类型多栏表格多扫描件做针对性优化。不要一上来就堆最复杂的方案适合自己文档的才是最好的。
延伸阅读

更多相关文章

2026/11/6 3:29:07

生成全局唯一的 `long` 类型 ID

在分布式集群(多台服务器)环境下,生成 全局唯一 的长整型ID,当作订单ID、优惠券ID、业务主键,替代数据库自增主键。基于 Redis 实现分布式 ID 生成器,类似雪花算法思路:时间戳(高位) 序列号(低…

2026/11/6 3:29:07

mybatis-plus - 乐观锁的使用

mybatis-plus - 乐观锁的使用 添加版本号更新插件 Bean public MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());return inte…

2026/11/6 4:49:21

MyBatis(随笔2 : Sql映射文件 )

Sql文件映射 这里只是笔记,属性详情,关键还是要多练 ——实例代码 MyBatisDemo MyBatis 真正的强大在于映射语句,专注于SQL,功能强大,SQL映射的配置却是相当简单 SQL映射文件的几个顶级元素 name详情mappernamespace: 命名空间cache配置给…

2026/11/6 4:49:21

MyBatis(随笔: 动态SQL映射文件)

动态SQL 我们可以用mybatis执行sql的形式来对数据的表进行增删改查操作,不过遇到比较复杂的业务需要写复杂的sql时(比如说sql的条件不确定,可能有一个条件或者多个), 我们就需要动态sql来提高sql的灵活性。 基于OGNL…

2026/11/6 4:49:21

Spring配置补充 (随笔详情扩展:)

Spring配置补充&#xff1a; 实现Spring 和 MyBatis 集成过程中学习了,Spring 的配置数据源的方法; 实际开发过程中还有很多灵活的配置文件; 灵活配置DataSource: <!-- 远古方法&#xff08;最初方法&#xff09;,数据库连接池,不过我最喜欢用的; --><bean id"…

2026/11/6 4:49:21

初始MyBatis(随笔1)

初始MyBatis(随笔1) 框架技术 为什么要学习框架技术&#xff1a; PTT模板举例&#xff1a;做ppt前, 总是会需要找一些模板, 优点; 1. 不用考虑布局, 排版等问题, 提高了效率; 2. 可以专心于PTT的内容, 使演讲 “质量” 更有保障; 3. 新手也可以实现出很好的 PPT; 框架&#x…

2026/11/5 15:39:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代&#xff0c;基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而&#xff0c;与配合度高的人类不同&#xff0c;宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态&#xff0c;更无法控制光照环境。实际应用中&#xff0c;我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/6 1:03:39

MCU晶振电路设计全解析:从负载电容计算到PCB布局调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 15:39:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代&#xff0c;基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而&#xff0c;与配合度高的人类不同&#xff0c;宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态&#xff0c;更无法控制光照环境。实际应用中&#xff0c;我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑