3、文档加载器:PDF、Word、TXT、HTML等常见文档格式的加载与解析
好,咱们今天聊聊文档加载器。说白了,这就是RAG系统的“嘴巴”——它负责把各种格式的文档吃进去,消化成我们能用的文本。
我刚开始做RAG项目时,以为这步很简单。不就是读文件嘛,谁不会?结果第一次上线就翻车了——PDF里的表格全乱码,Word文档的页眉页脚混进了正文,HTML里的广告标签也跑进来了。嗯,从那以后,我再也不敢小看文档加载器了。
核心观点:文档加载器是RAG系统的第一道关卡。加载质量直接决定了后续检索和生成的效果。垃圾进,垃圾出,这句话在RAG里尤其适用。
3.1 为什么需要专门的文档加载器?
你可能会问:Python不是有open()函数吗?直接读文件不就行了?
问题在于,现实世界的文档格式五花八门。PDF可能是扫描件,Word里可能有嵌入式表格,HTML里混着CSS和JavaScript。直接读文本,你得到的是满屏乱码和噪音。
我个人习惯把文档加载器想象成一个“翻译官”。它的工作就是:
- 格式识别:判断文件是PDF、Word还是别的什么
- 内容提取:把结构化内容转成纯文本
- 元数据保留:记录文件名、页码、创建时间等信息
- 噪音过滤:去掉页眉页脚、广告、注释等无关内容
我在项目中遇到过最坑的一次,是加载一个300页的PDF合同。直接用PyPDF2读出来,结果表格数据全挤在一起,根本没法用。后来换了pdfplumber,才把表格结构完整保留下来。
3.2 常见文档格式的加载方案
咱们一个一个来看。每种格式都有它的脾气,也有对应的“驯服”方法。
3.2.1 PDF格式
PDF是RAG系统里最常见的格式,也是最难处理的。为什么?因为PDF本质上是一堆绘图指令,不是结构化文本。
| 库名称 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| PyPDF2 / pypdf | 简单文本PDF | 轻量、速度快 | 表格、图片支持差 |
| pdfplumber | 含表格的PDF | 表格提取能力强 | 速度稍慢 |
| PyMuPDF (fitz) | 复杂排版PDF | 支持注释、图片提取 | 依赖较多 |
| pdfminer.six | 需要精细控制 | 可自定义解析规则 | 配置复杂 |
我个人最常用的是pdfplumber。它处理表格的能力确实强。来看个例子:
import pdfplumber
def load_pdf(file_path):
"""加载PDF文件,保留表格结构"""
text_content = []
with pdfplumber.open(file_path) as pdf:
for page_num, page in enumerate(pdf.pages):
# 提取文本
text = page.extract_text()
if text:
text_content.append(f"--- 第{page_num + 1}页 ---\n{text}")
# 提取表格
tables = page.extract_tables()
for table in tables:
table_text = "\n".join(
[" | ".join([cell if cell else "" for cell in row])
for row in table]
)
text_content.append(f"--- 表格 ---\n{table_text}")
return "\n".join(text_content)
# 使用示例
content = load_pdf("合同文件.pdf")
print(content[:500]) # 只打印前500个字符看看效果
小技巧:处理扫描版PDF时,记得先做OCR。我一般用pytesseract配合pdf2image,效果还不错。但要注意,OCR会消耗大量时间,建议批量处理时用异步方式。
3.2.2 Word格式(.docx)
Word文档比PDF友好一些,因为它是基于XML的。但坑也不少——页眉页脚、批注、修订记录,这些都可能混进正文。
我推荐用python-docx库。它可以直接操作Word的XML结构,精确控制提取内容。
from docx import Document
def load_docx(file_path):
"""加载Word文档,过滤页眉页脚"""
doc = Document(file_path)
# 提取正文段落
paragraphs = []
for para in doc.paragraphs:
# 跳过空段落和页眉页脚样式
if para.text.strip() and para.style.name not in ['Header', 'Footer']:
paragraphs.append(para.text)
# 提取表格内容
tables_text = []
for table in doc.tables:
for row in table.rows:
row_text = [cell.text for cell in row.cells]
tables_text.append(" | ".join(row_text))
return "\n".join(paragraphs + tables_text)
# 使用示例
content = load_docx("报告.docx")
print(len(content)) # 看看提取了多少字符
注意:Word文档里的图片和图表,python-docx只能获取到占位符,无法提取图片内容。如果需要处理图片,建议配合OCR工具。
3.2.3 TXT格式
TXT是最简单的格式,但简单不代表没坑。编码问题就是个大麻烦。
我曾经接手过一个项目,客户给的TXT文件是GBK编码的。直接用UTF-8读,全是乱码。排查了半天才发现问题。
import chardet
def load_txt(file_path):
"""智能检测编码并加载TXT文件"""
# 先检测编码
with open(file_path, 'rb') as f:
raw_data = f.read(10000) # 读前1万个字节就够了
result = chardet.detect(raw_data)
encoding = result['encoding']
# 用检测到的编码读取
with open(file_path, 'r', encoding=encoding) as f:
content = f.read()
return content
# 使用示例
content = load_txt("笔记.txt")
print(content[:200])
建议:处理TXT文件时,永远不要假设编码是UTF-8。用chardet检测一下,花不了多少时间,但能避免很多麻烦。
3.2.4 HTML格式
HTML文档的难点在于噪音太多。CSS样式、JavaScript脚本、广告标签、导航栏……这些都不是我们想要的。
我推荐用BeautifulSoup配合lxml解析器。它俩配合,基本能搞定99%的HTML文档。
from bs4 import BeautifulSoup
def load_html(file_path):
"""加载HTML文件,提取正文内容"""
with open(file_path, 'r', encoding='utf-8') as f:
soup = BeautifulSoup(f.read(), 'lxml')
# 移除不需要的标签
for tag in soup(['script', 'style', 'nav', 'footer', 'header', 'aside']):
tag.decompose()
# 提取正文
# 优先找article或main标签
main_content = soup.find('article') or soup.find('main') or soup.find('body')
if main_content:
text = main_content.get_text(separator='\n', strip=True)
else:
text = soup.get_text(separator='\n', strip=True)
return text
# 使用示例
content = load_html("网页.html")
print(content[:300])
注意:HTML里可能包含相对路径的图片和链接。如果你需要保留这些资源,记得用urljoin补全路径。我踩过这个坑——提取的文本里全是"images/logo.png",根本没法用。
3.3 统一加载接口设计
在实际项目中,你不会只处理一种格式。我建议设计一个统一的加载接口,根据文件扩展名自动选择加载器。
import os
class DocumentLoader:
"""统一的文档加载器"""
def __init__(self):
self.loaders = {
'.pdf': self._load_pdf,
'.docx': self._load_docx,
'.txt': self._load_txt,
'.html': self._load_html,
'.htm': self._load_html,
}
def load(self, file_path):
"""根据文件扩展名自动选择加载器"""
ext = os.path.splitext(file_path)[1].lower()
if ext not in self.loaders:
raise ValueError(f"不支持的格式: {ext}")
return self.loaders[ext](file_path)
def _load_pdf(self, file_path):
# 调用pdfplumber实现
pass
def _load_docx(self, file_path):
# 调用python-docx实现
pass
def _load_txt(self, file_path):
# 调用chardet实现
pass
def _load_html(self, file_path):
# 调用BeautifulSoup实现
pass
# 使用示例
loader = DocumentLoader()
content = loader.load("文档.pdf") # 自动识别格式
核心思路:统一接口 + 策略模式。这样新增格式时,只需要添加一个加载方法,不需要修改调用代码。
3.4 知识体系结构图
下面这张图,是我梳理的文档加载器知识体系。你可以把它当作一个“导航图”,看看自己掌握了哪些,还有哪些需要补。
3.5 避坑指南
最后,分享几个我踩过的坑,希望能帮你少走弯路。
- 编码问题:永远不要假设文件编码。用chardet检测一下,花不了多少时间。
- 大文件处理:PDF和Word文件可能很大。我建议用流式处理,不要一次性加载到内存。
- 特殊字符:有些PDF里会有不可见字符。提取后记得用正则清理一下。
- 表格识别:PDF里的表格,不同库识别效果差异很大。建议多试几个库,选效果最好的。
- 日志记录:加载失败时,记录详细的错误信息。方便排查问题。
我的习惯:每次加载文档后,我都会打印前200个字符看看效果。如果发现乱码或格式错乱,马上调整加载参数。别等到最后才发现问题,那时候改起来就麻烦了。
好了,文档加载器就聊到这儿。记住一句话:加载质量决定RAG系统的上限。花时间把这一步做好,后面的工作会轻松很多。