文档加载器:从PDF、网页、数据库加载数据
做RAG系统,第一步就是喂数据。
但数据在哪?PDF文件里、网页上、数据库里。格式五花八门,编码乱七八糟。我见过不少团队,模型选得挺好,向量库也搭得漂亮,结果卡在数据加载这一步——PDF乱码、网页被反爬、数据库连不上。
说白了,文档加载器就是RAG系统的「入口守卫」。它负责把原始数据变成结构化的文本块。今天咱们就聊聊,怎么用LangChain和LlamaIndex搞定这件事。
为什么文档加载这么重要?
我刚开始做RAG时,犯过一个低级错误。用PyPDF2直接读PDF,结果表格数据全丢了,模型回答时总说「找不到相关信息」。后来才发现,PDF里的表格需要专门的解析器。
文档加载器要解决三个问题:
- 格式兼容——PDF、Word、HTML、Markdown,每种格式都有自己的「脾气」
- 内容提取——文字、表格、图片、元数据,哪些该留哪些该扔
- 分块预处理——加载时就做好初步清洗,减少后续工作量
你想想看,如果加载阶段就丢了30%的内容,后面检索再强也白搭。
PDF加载:最头疼的格式
PDF是RAG系统里最常见的输入格式,也是最坑的。为什么?因为PDF本质上是「打印布局」,不是「文本结构」。同一个PDF,不同加载器读出来的结果可能天差地别。
| 加载器 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| PyPDFLoader | 纯文本PDF | 速度快,依赖少 | 表格/图片全丢 |
| PyMuPDFLoader | 带表格的PDF | 保留布局信息 | 中文有时乱码 |
| PDFPlumberLoader | 表格密集型PDF | 表格提取最准 | 速度慢 |
| UnstructuredPDFLoader | 复杂排版PDF | 自动识别段落 | 依赖太多 |
来看个实际例子。用LangChain加载PDF:
from langchain_community.document_loaders import PyMuPDFLoader
loader = PyMuPDFLoader("report.pdf")
documents = loader.load()
# 每个document包含page_content和metadata
for doc in documents[:2]:
print(f"页码: {doc.metadata['page']}")
print(f"内容预览: {doc.page_content[:100]}...")
嗯,这里要注意。metadata里通常有页码、来源文件名,有些加载器还会带上PDF的创建日期。这些信息在后续检索时很有用,可以用来做过滤。
网页加载:小心反爬和动态内容
网页加载比PDF更麻烦。静态页面还好说,遇到JavaScript渲染的SPA页面,直接请求HTML是拿不到内容的。
LangChain提供了几种网页加载器:
- WebBaseLoader——最简单的,直接请求URL,适合静态页面
- SeleniumURLLoader——启动浏览器渲染,适合动态页面
- PlaywrightURLLoader——类似Selenium,但更轻量
我曾经爬一个技术博客,用WebBaseLoader死活拿不到文章正文。后来发现页面是Vue写的,内容通过API异步加载。换成SeleniumURLLoader才搞定。
from langchain_community.document_loaders import WebBaseLoader
loader = WebBaseLoader(
web_paths=("https://example.com/blog",),
bs_kwargs=dict(
parse_only=bs4.SoupStrainer(class_=("post-content", "article-body"))
)
)
docs = loader.load()
数据库加载:结构化数据的入口
很多RAG系统的知识库其实存在数据库里。MySQL、PostgreSQL、MongoDB,这些都能作为文档来源。
数据库加载的核心思路是:把每一行记录变成一个Document。字段名变成metadata,内容字段变成page_content。
| 数据库类型 | LangChain加载器 | 典型用途 |
|---|---|---|
| SQL数据库 | SQLDatabaseLoader | 产品信息、用户手册 |
| MongoDB | MongoDBLoader | 日志、配置文档 |
| CSV文件 | CSVLoader | 表格数据、导出记录 |
from langchain_community.document_loaders import SQLDatabaseLoader
loader = SQLDatabaseLoader(
"sqlite:///knowledge.db",
query="SELECT title, content, category FROM articles WHERE status='published'"
)
docs = loader.load()
我个人习惯把数据库加载和增量更新结合起来。每天跑一次定时任务,只加载新增或修改的记录,避免重复处理。
LlamaIndex的文档加载器
LlamaIndex的加载器设计思路和LangChain不太一样。它更强调「文档解析」和「节点提取」的分离。
在LlamaIndex里,加载流程是:
- 用Reader读取原始数据
- 用Parser解析成结构化文档
- 用NodeParser切分成节点
这种分层设计的好处是灵活。你可以换不同的Parser来处理同一份数据,而不需要改Reader。
from llama_index.core import SimpleDirectoryReader
from llama_index.core.node_parser import SentenceSplitter
# 加载目录下所有PDF
reader = SimpleDirectoryReader(
input_dir="./docs",
recursive=True
)
documents = reader.load_data()
# 切分节点
parser = SentenceSplitter(chunk_size=512, chunk_overlap=50)
nodes = parser.get_nodes_from_documents(documents)
知识体系总览
下面这张图,把文档加载器的核心逻辑串起来了。你可以看到,无论数据源是什么,最终都要变成统一的Document格式,才能进入后续的分块和向量化流程。
实战建议:怎么选加载器?
选加载器没有银弹。我一般按这个顺序决策:
- 先看数据量——小规模用PyMuPDFLoader,大规模用Unstructured(虽然慢但准)
- 再看内容类型——纯文本用轻量加载器,表格多上PDFPlumber
- 最后看部署环境——服务器上没浏览器就别用Selenium加载器
文档加载器是RAG系统的第一道关卡。选对了,后面一路顺畅;选错了,后面所有环节都在给前面填坑。花时间把这一步做好,绝对值。