第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。元数据里会自动带上页码,方便后续检索时定位来源。
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-content和main-body这两个class的内容。说白了,就是告诉加载器:「我只想要正文,其他的一概不要。」
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行会变成一个Document,page_content是整行的文本,metadata里包含行号:
# 查看加载结果
for doc in documents[:3]:
print(f"行号: {doc.metadata['row']}")
print(f"内容: {doc.page_content}")
print("---")
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%的数据预处理时间。下一章咱们聊聊「文本分割器」——把长文档切成合适的片段,让检索更精准。