文档解析与预处理:PDF/HTML/Word解析、文本清洗、段落分割策略
文档解析这步,说白了就是给RAG系统「喂饭」。饭没切好,后面的检索和生成全得跟着遭殃。我见过太多团队在模型调优上花了大把时间,结果召回率上不去,一查才发现——原始文档里的文字都是乱码或者被截断的。
今天咱们就把这顿饭怎么做透,聊清楚。
2.1 多格式文档解析实战
现实世界里的文档格式五花八门。PDF、Word、HTML是最常见的三种。每种格式的解析策略完全不同,我一个个说。
2.1.1 PDF解析:最头疼的格式
PDF这玩意儿,说白了就是一张「打印出来的纸」。它里面没有段落、没有标题层级,只有坐标和字体。所以解析PDF时,我习惯分场景选工具:
| 场景 | 推荐工具 | 优点 | 缺点 |
|---|---|---|---|
| 文字型PDF(可复制) | PyMuPDF (fitz) | 速度快,保留字体和位置信息 | 对复杂表格支持一般 |
| 扫描型PDF(图片) | OCR(PaddleOCR / Tesseract) | 能提取图片中的文字 | 速度慢,有识别错误 |
| 复杂排版PDF | pdfplumber | 表格提取能力强 | 大文件内存占用高 |
| 混合型PDF | Unstructured.io | 自动检测类型,一站式 | 依赖较多,部署稍重 |
下面是我常用的PDF解析代码片段:
import fitz # PyMuPDF
def extract_text_from_pdf(pdf_path):
doc = fitz.open(pdf_path)
full_text = []
for page_num, page in enumerate(doc):
# 提取文本块,保留位置信息
blocks = page.get_text("blocks")
page_text = []
for block in blocks:
# block结构: (x0, y0, x1, y1, text, block_type)
if block[5] == 0: # 0表示文本块
page_text.append(block[4])
full_text.append("\n".join(page_text))
doc.close()
return "\n".join(full_text)
2.1.2 Word文档解析
Word文档比PDF友好得多。它本身就有段落、标题、列表这些结构信息。我个人习惯用 python-docx 这个库。
这里有个坑:很多人直接遍历所有段落,忽略了表格里的内容。Word里的表格经常藏着关键数据。
from docx import Document
def extract_from_docx(docx_path):
doc = Document(docx_path)
content = []
# 提取段落
for para in doc.paragraphs:
if para.text.strip():
content.append({
"text": para.text,
"style": para.style.name # 保留样式信息
})
# 提取表格内容
for table in doc.tables:
for row in table.rows:
row_text = [cell.text for cell in row.cells]
content.append({
"text": " | ".join(row_text),
"style": "Table"
})
return content
2.1.3 HTML解析
HTML解析的核心是「去噪」。网页里充斥着导航栏、广告、侧边栏、版权声明……这些东西对RAG来说全是噪音。
我推荐用 BeautifulSoup 配合 readability-lxml 来做:
from bs4 import BeautifulSoup
import readability
def extract_from_html(html_content):
# 第一步:用readability提取正文
doc = readability.Document(html_content)
main_content = doc.summary()
# 第二步:用BeautifulSoup清理残留标签
soup = BeautifulSoup(main_content, 'html.parser')
# 移除脚本、样式、图片
for tag in soup(['script', 'style', 'img', 'iframe']):
tag.decompose()
# 提取标题层级
title = soup.find('h1')
title_text = title.get_text() if title else ""
# 提取所有文本
text = soup.get_text(separator='\n', strip=True)
return {
"title": title_text,
"text": text
}
2.2 文本清洗:别让脏数据毁了你的RAG
解析完文档,你会发现文本里全是「垃圾」——乱码字符、多余的空格、换行符、Unicode控制字符……这些必须清理干净。
我总结了一套清洗流程,按优先级排序:
- 去除控制字符:\x00-\x1f 这些不可见字符,直接干掉
- 统一换行符:\r\n 统一转成 \n
- 合并多余空白:连续空格、制表符,合并成一个空格
- 修复乱码:比如「’」这种UTF-8解码错误,转成正确的单引号
- 去除特殊符号:保留中文、英文、数字、常用标点,其他酌情处理
核心原则: 清洗要「保守」。宁可少洗,不要多洗。我见过有人把代码里的下划线全删了,结果检索「RAG_system」时永远匹配不到「RAGsystem」——因为下划线被洗掉了。
下面是我常用的清洗函数:
import re
import unicodedata
def clean_text(text):
# 1. 去除控制字符
text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', '', text)
# 2. 统一换行符
text = text.replace('\r\n', '\n').replace('\r', '\n')
# 3. 合并多余空白(保留单个换行)
text = re.sub(r'[ \t]+', ' ', text)
# 4. 修复常见乱码
text = text.replace('’', "'").replace('â€"', '—')
text = text.replace('“', '"').replace('â€', '"')
# 5. 去除零宽字符
text = text.replace('\u200b', '').replace('\ufeff', '')
# 6. 规范化Unicode(NFKC)
text = unicodedata.normalize('NFKC', text)
return text.strip()
2.3 段落分割策略:切得好,才能找得准
文档清洗干净了,接下来就是「切段」。这一步直接决定了检索的颗粒度。
切得太粗,一个段落包含多个主题,检索时语义模糊。切得太细,上下文丢失,检索结果支离破碎。
我常用的分割策略有三种,适用场景不同:
| 策略 | 原理 | 适用场景 | 推荐参数 |
|---|---|---|---|
| 固定长度分割 | 按字符数或数切分 | 通用场景,简单粗暴 | 512-1024 tokens |
| 语义分割 | 按段落、标题、换行符切 | 结构化文档(、报告) | 保留自然段落 |
| 递归分割 | 先按大块切,再递归细分 | 长文档、混合内容 | chunk_size=1000, overlap=200 |
我个人最常用的是「递归分割 + 重叠窗口」。为什么?因为固定长度分割经常把一句话从中间砍断,导致语义不完整。而递归分割会优先按段落边界切,实在超长了才按句子切。
from langchain.text_splitter import RecursiveCharacterTextSplitter
def split_document(text, chunk_size=1000, chunk_overlap=200):
splitter = RecursiveCharacterTextSplitter(
separators=["\n\n", "\n", "。", ".", " ", ""],
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=len,
)
chunks = splitter.split_text(text)
return chunks
["\n\n", "\n", "。", "!", "?", ".", " ", ""],效果好了很多。
2.4 整体流程与SVG框架图
把上面这些步骤串起来,就是一套完整的文档预处理流水线。我画了张图,方便你理解整体脉络:
你看,整个流程其实不复杂。但每个环节都有细节要注意。格式识别这步,我建议用文件扩展名 + MIME类型双重判断,别只信扩展名——我遇到过把PDF重命名为.docx的奇葩情况。
2.5 实战中的几个坑
最后分享几个我踩过的坑,希望能帮你省点时间:
- PDF里的表格:很多PDF解析库会把表格内容打乱。我建议先用
pdfplumber单独提取表格,再和正文合并。 - HTML里的动态内容:有些网页内容是通过JavaScript渲染的。直接用requests拿不到。这种情况得用
selenium或playwright先渲染再解析。 - 编码问题:Word文档可能是GBK编码,HTML可能是UTF-8。统一转成UTF-8再处理,否则清洗时容易出乱码。
- 分割后的重叠:重叠窗口不是越大越好。我试过overlap=500,结果相邻两个chunk几乎一模一样,检索时重复内容太多。一般overlap控制在chunk_size的10%-20%比较合理。
文档预处理这块,说白了就是「七分工具,三分经验」。工具选对了,流程搭好了,剩下的就是根据你的数据特点慢慢调参。别指望一次搞定,多试几次,你会找到最适合自己场景的那套方案。