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 知识体系结构图

下面这张图,是我梳理的文档加载器知识体系。你可以把它当作一个“导航图”,看看自己掌握了哪些,还有哪些需要补。

文档加载器知识体系 文档加载器 PDF格式 PyPDF2 pdfplumber Word格式 python-docx TXT格式 chardet编码检测 open()读取 HTML格式 BeautifulSoup lxml解析器 统一接口设计:策略模式 + 工厂方法

3.5 避坑指南

最后,分享几个我踩过的坑,希望能帮你少走弯路。

  • 编码问题:永远不要假设文件编码。用chardet检测一下,花不了多少时间。
  • 大文件处理:PDF和Word文件可能很大。我建议用流式处理,不要一次性加载到内存。
  • 特殊字符:有些PDF里会有不可见字符。提取后记得用正则清理一下。
  • 表格识别:PDF里的表格,不同库识别效果差异很大。建议多试几个库,选效果最好的。
  • 日志记录:加载失败时,记录详细的错误信息。方便排查问题。

我的习惯:每次加载文档后,我都会打印前200个字符看看效果。如果发现乱码或格式错乱,马上调整加载参数。别等到最后才发现问题,那时候改起来就麻烦了。

好了,文档加载器就聊到这儿。记住一句话:加载质量决定RAG系统的上限。花时间把这一步做好,后面的工作会轻松很多。


专注资料整理