第3章:文档加载器——LangChain文档加载器概述、PDF加载器实战、HTML加载器实战、CSV加载器实战

好,咱们进入第三个章节。说实话,文档加载器是整个RAG系统的「入口关」。你想想看,如果连数据都喂不进去,后面的检索、生成全是白搭。我在做第一个RAG项目时,就栽在这个环节上——PDF里的表格全乱了,HTML里的正文被广告淹没,CSV的编码直接报错。嗯,今天咱们就把这些坑一个个填平。

3.1 LangChain文档加载器概述

LangChain的文档加载器,说白了就是一套「数据适配器」。它的核心作用是把各种格式的文件,统一转换成LangChain能理解的Document对象。每个Document包含两个部分:page_content(文本内容)和metadata(元数据,比如文件名、页码、来源URL)。

我个人习惯把加载器分成三类:

  • 纯文本类:TXT、Markdown、CSV——结构简单,直接读就行
  • 富文本类:PDF、HTML、Word——需要解析布局和样式
  • 二进制类:图片、音频——需要OCR或语音识别

你可能会问:「为什么不直接用Python的open()读文件?」嗯,问得好。但现实是,PDF里有表格、HTML里有嵌套标签、CSV里有特殊分隔符。自己写解析代码,少说也得几百行。LangChain把这些脏活累活都封装好了,你只需要一行代码就能搞定。

核心概念:所有加载器都继承自BaseLoader基类,提供两个核心方法——load()一次性加载所有文档,lazy_load()按需加载(适合大文件)。

3.2 PDF加载器实战

PDF是RAG系统里最常见的输入格式。但PDF这玩意儿,表面看着是文本,底层其实是绘图指令。我在项目中遇到过一份扫描件PDF,用普通加载器读出来全是乱码。后来才发现,得用OCR方案。

LangChain提供了多种PDF加载器,我挑三个最常用的讲讲:

加载器 适用场景 依赖库
PyPDFLoader 标准文本PDF pypdf
PDFMinerLoader 需要保留布局的PDF pdfminer.six
PyMuPDFLoader 需要提取图片/表格的PDF pymupdf

先看最基础的PyPDFLoader

from langchain_community.document_loaders import PyPDFLoader

loader = PyPDFLoader("report.pdf")
documents = loader.load()

# 每个document对应一页
for doc in documents:
    print(f"第{doc.metadata['page']}页: {doc.page_content[:100]}...")

这段代码会把PDF按页拆分,每页变成一个Document。元数据里会自动带上页码,方便后续检索时定位来源。

我的经验:如果PDF里有表格,建议用PyMuPDFLoader。它能把表格转成Markdown格式,比纯文本好读得多。安装命令:pip install pymupdf

再来看一个更复杂的场景——处理扫描件PDF:

from langchain_community.document_loaders import PDFPlumberLoader

loader = PDFPlumberLoader("scanned_report.pdf")
documents = loader.load()

# PDFPlumber能提取表格数据
for doc in documents:
    if 'table' in doc.metadata:
        print("发现表格:", doc.metadata['table'])

我曾经用PDFPlumberLoader处理过一份300页的财务报表,里面的表格数据提取准确率能达到95%以上。当然,如果是纯图片扫描件,还是得上OCR方案(比如UnstructuredPDFLoader配合Tesseract)。

3.3 HTML加载器实战

HTML加载器的难点在于「去噪」。一个网页里,真正有用的内容可能只占30%,剩下的全是导航栏、广告、侧边栏。我刚开始做的时候,直接把整个HTML喂给模型,结果检索出来的全是「点击这里」「免费领取」之类的垃圾信息。

LangChain的HTMLLoader可以指定CSS选择器来提取特定区域:

from langchain_community.document_loaders import HTMLLoader

loader = HTMLLoader(
    "article.html",
    bs_kwargs=dict(
        parse_only=bs4.SoupStrainer(class_=("article-content", "main-body"))
    )
)
documents = loader.load()

这里用SoupStrainer只保留article-contentmain-body这两个class的内容。说白了,就是告诉加载器:「我只想要正文,其他的一概不要。」

注意:如果HTML文件很大(比如整个网站导出),建议先用lazy_load()按需加载,避免内存溢出。我曾经处理过一个50MB的HTML文件,直接load()把服务器搞崩了。

还有一个更智能的方案——UnstructuredHTMLLoader

from langchain_community.document_loaders import UnstructuredHTMLLoader

loader = UnstructuredHTMLLoader("webpage.html")
documents = loader.load()

# 自动识别标题、段落、列表
for doc in documents:
    print(f"类型: {doc.metadata['category']}")
    print(f"内容: {doc.page_content[:200]}")

这个加载器会自动识别HTML的语义结构,把标题、段落、列表分别标记出来。嗯,这里要注意:它依赖unstructured库,安装时可能会有点慢,但值得等待。

3.4 CSV加载器实战

CSV看起来简单,但坑也不少。最常见的问题有三个:编码错误、分隔符不一致、表头缺失。我在项目中遇到过一份CSV,明明是逗号分隔,但内容里也包含逗号,结果解析出来全乱了。

LangChain的CSVLoader提供了灵活的配置:

from langchain_community.document_loaders import CSVLoader

loader = CSVLoader(
    "data.csv",
    csv_args={
        "delimiter": ",",       # 分隔符
        "quotechar": '"',       # 引号字符
        "fieldnames": ["id", "name", "content"]  # 自定义表头
    },
    encoding="utf-8"           # 编码
)
documents = loader.load()

这段代码里,我显式指定了分隔符和引号字符。如果CSV文件是GBK编码,把encoding改成"gbk"就行。

每个CSV行会变成一个Documentpage_content是整行的文本,metadata里包含行号:

# 查看加载结果
for doc in documents[:3]:
    print(f"行号: {doc.metadata['row']}")
    print(f"内容: {doc.page_content}")
    print("---")
避坑指南:我曾经处理过一个CSV,里面有一列是「备注」,内容包含换行符。普通加载器会把换行当成新行,导致数据错位。解决方案是用pandas先清洗数据,再传给DataFrameLoader

如果你需要更精细的控制,可以用DataFrameLoader

import pandas as pd
from langchain_community.document_loaders import DataFrameLoader

df = pd.read_csv("data.csv")
loader = DataFrameLoader(df, page_content_column="content")
documents = loader.load()

这样你可以指定哪一列作为page_content,其他列自动变成metadata。比如,把「标题」列作为内容,「作者」「日期」作为元数据,检索时就能按作者筛选了。

3.5 实战:多格式文档统一加载

最后,咱们来个综合案例。假设你有一个文件夹,里面混着PDF、HTML、CSV三种文件,怎么统一加载?

import os
from langchain_community.document_loaders import (
    PyPDFLoader,
    UnstructuredHTMLLoader,
    CSVLoader
)

def load_document(file_path):
    ext = os.path.splitext(file_path)[1].lower()
    
    if ext == '.pdf':
        loader = PyPDFLoader(file_path)
    elif ext in ['.html', '.htm']:
        loader = UnstructuredHTMLLoader(file_path)
    elif ext == '.csv':
        loader = CSVLoader(file_path)
    else:
        raise ValueError(f"不支持的文件格式: {ext}")
    
    return loader.load()

# 遍历文件夹
all_docs = []
for root, dirs, files in os.walk("data_folder"):
    for file in files:
        file_path = os.path.join(root, file)
        try:
            docs = load_document(file_path)
            all_docs.extend(docs)
            print(f"成功加载: {file_path} ({len(docs)}页)")
        except Exception as e:
            print(f"加载失败: {file_path} - {e}")

print(f"总共加载了 {len(all_docs)} 个文档")

这段代码会自动识别文件后缀,调用对应的加载器。我在实际项目中用这个方案处理过上千份混合文档,效果很稳定。当然,你还可以加上异常处理、日志记录,让它更健壮。

好了,这一章的内容就到这里。文档加载器看似简单,但选对了工具,能省下你80%的数据预处理时间。下一章咱们聊聊「文本分割器」——把长文档切成合适的片段,让检索更精准。