LangChain文档加载与处理-DocumentLoader全解
构建 RAG(检索增强生成)知识库的第一步是把各种格式的文档加载成统一的数据结构。这一步往往被低估——PDF 里的表格解析乱码、Word 文档的页眉页脚混入正文、网页 HTML 标签残留……这些问题如果不在加载阶段处理好,后续的分块、检索都会受到影响。
LangChain 文档加载与处理:DocumentLoader 全解
构建 RAG(检索增强生成)知识库的第一步是把各种格式的文档加载成统一的数据结构。这一步往往被低估——PDF 里的表格解析乱码、Word 文档的页眉页脚混入正文、网页 HTML 标签残留……这些问题如果不在加载阶段处理好,后续的分块、检索都会受到影响。
LangChain 的 DocumentLoader 系列提供了统一接口,屏蔽了不同文档格式的解析细节。
1.1 DocumentLoader 的统一接口
文档处理流水线——各类文档源经DocumentLoader加载为Document对象,再经TextSplitter切分为Chunks
所有 DocumentLoader 都实现同一个接口:
from langchain_core.document_loaders import BaseLoader
from langchain_core.documents import Document
from typing import List, Iterator
class BaseLoader:
def load(self) -> List[Document]:
"""加载所有文档,返回列表。适合小文件。"""
...
def lazy_load(self) -> Iterator[Document]:
"""懒加载,逐个返回 Document。适合大文件,内存友好。"""
...
def load_and_split(self, text_splitter=None) -> List[Document]:
"""加载并立即分块,方便一步到位。"""
...
返回的 Document 对象包含两个字段:
doc = Document(
page_content="这是文档的正文内容",
metadata={
"source": "/path/to/file.pdf", # 来源文件路径
"page": 3, # 页码(PDF)
"author": "张三", # 自定义元数据
}
)
metadata(元数据:关于数据的数据,例如文件来源、页码、创建时间等附加信息)是整个 RAG 系统的重要基础设施——检索结果需要告诉用户"这段话来自哪里",过滤检索需要按来源、日期、分类过滤,这些都依赖 metadata。
1.2 常用 Loader 详解
1.2.1 TextLoader:纯文本
最简单的 Loader,适合 .txt、.md、.log 等纯文本文件:
from langchain_community.document_loaders import TextLoader
loader = TextLoader(
file_path="docs/readme.md",
encoding="utf-8", # 显式指定编码,避免中文乱码
)
docs = loader.load()
print(f"加载了 {len(docs)} 个文档")
print(f"内容前100字:{docs[0].page_content[:100]}")
print(f"元数据:{docs[0].metadata}")
# 元数据示例:{'source': 'docs/readme.md'}
1.2.2 PyPDFLoader:PDF 文档
PDF 是知识库最常见的格式,每一页返回一个 Document:
from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader("docs/technical_spec.pdf")
pages = loader.load()
print(f"共 {len(pages)} 页")
for i, page in enumerate(pages[:3]):
print(f"\n第 {i+1} 页(元数据:{page.metadata}):")
print(page.page_content[:200])
# 元数据示例:{'source': 'docs/technical_spec.pdf', 'page': 0}
PyPDF 解析质量适中,对于扫描件(图片型 PDF)需要 OCR(光学字符识别:从图片中识别并提取文字的技术),可以用 PyMuPDFLoader(速度更快)或 PDFMinerLoader(对复杂布局解析更好):
# PyMuPDFLoader:速度最快,元数据最丰富
from langchain_community.document_loaders import PyMuPDFLoader
loader = PyMuPDFLoader("docs/report.pdf")
docs = loader.load()
# 元数据包含:author, creator, producer, subject, title, total_pages, format 等
print(docs[0].metadata)
1.2.3 UnstructuredWordDocumentLoader:Word 文档
需要安装 unstructured 和 python-docx:
from langchain_community.document_loaders import UnstructuredWordDocumentLoader
loader = UnstructuredWordDocumentLoader(
file_path="docs/product_manual.docx",
# mode="single":整个文档合并为一个 Document
# mode="elements":按段落、标题、表格等元素分别返回(结构更精细)
mode="elements",
)
elements = loader.load()
# elements 模式下可以过滤元素类型
from unstructured.documents.elements import Title, NarrativeText, Table
titles = [e for e in elements if e.metadata.get("category") == "Title"]
tables = [e for e in elements if e.metadata.get("category") == "Table"]
print(f"标题数量:{len(titles)},表格数量:{len(tables)}")
1.2.4 WebBaseLoader:网页内容
从 URL 加载网页,自动去除 HTML 标签:
from langchain_community.document_loaders import WebBaseLoader
import bs4
# 单个 URL
loader = WebBaseLoader(
web_paths=["https://python.langchain.com/docs/introduction/"],
# bs_kwargs:传给 BeautifulSoup 的参数,用于精准提取页面内容区域
# 避免把导航栏、页脚等噪音内容也加载进来
bs_kwargs={
"parse_only": bs4.SoupStrainer(
class_=("post-content", "post-title", "post-header", "markdown")
)
},
)
docs = loader.load()
# 批量 URL
loader_batch = WebBaseLoader(
web_paths=[
"https://python.langchain.com/docs/introduction/",
"https://python.langchain.com/docs/concepts/",
],
# 并发数,加速批量加载
requests_per_second=2,
)
docs = loader_batch.load()
print(f"加载了 {len(docs)} 个页面")
1.2.5 CSVLoader:CSV 数据
每行数据返回一个 Document,元数据包含行号:
from langchain_community.document_loaders import CSVLoader
loader = CSVLoader(
file_path="data/products.csv",
# source_column 指定哪列作为 metadata['source'],方便溯源
source_column="product_id",
# csv_args 传给 csv.reader
csv_args={"delimiter": ",", "quotechar": '"'},
encoding="utf-8",
)
docs = loader.load()
print(f"加载了 {len(docs)} 条记录")
print(docs[0].page_content)
# 输出示例:
# product_id: P001
# name: iPhone 15
# price: 5999
# category: 手机
1.3 文档元数据的作用
加载时给文档附加丰富的元数据,是后续精准检索的基础:
from langchain_community.document_loaders import PyPDFLoader
from pathlib import Path
import datetime
def load_pdf_with_rich_metadata(pdf_path: str, category: str, author: str) -> list:
"""加载 PDF 并附加业务元数据"""
loader = PyPDFLoader(pdf_path)
docs = loader.load()
path = Path(pdf_path)
file_stat = path.stat()
for doc in docs:
# 保留原始元数据,追加业务级别的元数据
doc.metadata.update({
"category": category, # 文档分类,用于过滤检索
"author": author, # 作者
"filename": path.name, # 文件名
"file_size_kb": round(file_stat.st_size / 1024, 1),
"loaded_at": datetime.date.today().isoformat(),
# 把页码归一化为从 1 开始(PyPDF 默认从 0 开始)
"page_number": doc.metadata.get("page", 0) + 1,
})
return docs
docs = load_pdf_with_rich_metadata(
"docs/java_best_practices.pdf",
category="technical",
author="engineering_team"
)
1.4 文档清洗:去除页眉页脚
PDF 和 Word 文档里的页眉页脚会污染文本内容,影响 embedding 质量:
import re
from langchain_core.documents import Document
from typing import List
def clean_pdf_document(docs: List[Document]) -> List[Document]:
"""
清洗 PDF 文档:
1. 去除页眉页脚(通常是页码、公司名、文件标题的重复)
2. 合并被分行断开的段落
3. 去除多余空白
"""
cleaned = []
for doc in docs:
text = doc.page_content
# 1. 去除纯数字行(页码)
text = re.sub(r"^\s*\d+\s*$", "", text, flags=re.MULTILINE)
# 2. 去除常见页眉页脚模式(需要根据实际文档调整)
# 示例:去除"机密 - 请勿外传"这类固定文字
text = re.sub(r"机密.{0,10}请勿外传", "", text)
# 3. 合并被错误换行的段落
# 如果一行末尾没有标点(不是段落结尾),把下一行合并
text = re.sub(r"([^\n。!?;\.\!\?])\n([^\n])", r"\1\2", text)
# 4. 压缩多个连续空白行为最多一个
text = re.sub(r"\n{3,}", "\n\n", text)
text = text.strip()
if text: # 过滤掉清洗后变为空的文档
cleaned.append(Document(
page_content=text,
metadata=doc.metadata,
))
return cleaned
1.5 批量加载:DirectoryLoader 遍历目录
真实项目里通常需要批量加载一个目录下的所有文档:
from langchain_community.document_loaders import DirectoryLoader, PyPDFLoader, TextLoader
# 加载目录下所有 PDF
pdf_loader = DirectoryLoader(
path="./knowledge_base/",
glob="**/*.pdf", # 递归匹配所有 PDF
loader_cls=PyPDFLoader,
show_progress=True, # 显示进度条
use_multithreading=True, # 多线程并发加载,加速大量文件的加载
max_concurrency=4,
)
pdf_docs = pdf_loader.load()
# 加载目录下所有 Markdown
md_loader = DirectoryLoader(
path="./docs/",
glob="**/*.md",
loader_cls=TextLoader,
loader_kwargs={"encoding": "utf-8"},
)
md_docs = md_loader.load()
print(f"PDF 文档:{len(pdf_docs)} 个")
print(f"Markdown 文档:{len(md_docs)} 个")
1.6 完整实战:加载混合知识库
以下代码展示如何构建一个包含 PDF、Word、网页三种来源的混合知识库:
import os
from pathlib import Path
from typing import List
from langchain_core.documents import Document
from langchain_community.document_loaders import (
PyMuPDFLoader,
UnstructuredWordDocumentLoader,
WebBaseLoader,
DirectoryLoader,
)
class KnowledgeBaseLoader:
"""混合知识库加载器,统一处理多种文档格式"""
def __init__(self, base_dir: str):
self.base_dir = Path(base_dir)
self.all_docs: List[Document] = []
def load_pdfs(self, subdir: str = "pdfs") -> int:
"""加载 PDF 文档"""
pdf_dir = self.base_dir / subdir
if not pdf_dir.exists():
return 0
loader = DirectoryLoader(
path=str(pdf_dir),
glob="**/*.pdf",
loader_cls=PyMuPDFLoader,
show_progress=True,
use_multithreading=True,
)
docs = loader.load()
# 附加分类元数据
for doc in docs:
doc.metadata["doc_type"] = "pdf"
self.all_docs.extend(docs)
return len(docs)
def load_word_docs(self, subdir: str = "word") -> int:
"""加载 Word 文档"""
word_dir = self.base_dir / subdir
if not word_dir.exists():
return 0
loader = DirectoryLoader(
path=str(word_dir),
glob="**/*.docx",
loader_cls=UnstructuredWordDocumentLoader,
loader_kwargs={"mode": "single"}, # 每个文件合并为一个 Document
)
docs = loader.load()
for doc in docs:
doc.metadata["doc_type"] = "word"
self.all_docs.extend(docs)
return len(docs)
def load_web_pages(self, urls: List[str]) -> int:
"""加载网页"""
if not urls:
return 0
loader = WebBaseLoader(
web_paths=urls,
requests_per_second=1, # 限速,避免被目标网站封禁
)
docs = loader.load()
for doc in docs:
doc.metadata["doc_type"] = "webpage"
self.all_docs.extend(docs)
return len(docs)
def get_stats(self) -> dict:
"""统计各类型文档数量"""
from collections import Counter
type_counts = Counter(doc.metadata.get("doc_type", "unknown") for doc in self.all_docs)
return {
"total": len(self.all_docs),
"by_type": dict(type_counts),
}
# 使用示例
loader = KnowledgeBaseLoader(base_dir="./knowledge_base")
pdf_count = loader.load_pdfs()
word_count = loader.load_word_docs()
web_count = loader.load_web_pages([
"https://docs.python.org/3/library/asyncio.html",
])
print(f"加载统计:{loader.get_stats()}")
# 输出:加载统计:{'total': 45, 'by_type': {'pdf': 32, 'word': 8, 'webpage': 5}}
1.7 DocumentLoader 处理流程
1.8 各 Loader 特性对比
| Loader | 适用格式 | 解析质量 | 速度 | 依赖包 |
|---|---|---|---|---|
| TextLoader | TXT、MD、代码 | 原样加载 | 最快 | 无 |
| PyPDFLoader | 中等 | 快 | pypdf | |
| PyMuPDFLoader | 好,元数据丰富 | 很快 | pymupdf | |
| UnstructuredWordDocumentLoader | DOCX | 好,支持表格 | 中等 | unstructured, python-docx |
| WebBaseLoader | HTML | 依赖选择器质量 | 受网络影响 | bs4, requests |
| CSVLoader | CSV | 行级精确 | 快 | 无 |
| DirectoryLoader | 任意(配合其他Loader) | 取决于子Loader | 支持多线程 | 无 |
1.9 小结
DocumentLoader 的统一接口让上层代码不需要关心文档格式的差异。实践中的几个关键点:
- 加载时就附加充分的 metadata,后续过滤检索会用到
- PDF 优先用 PyMuPDF,速度和质量的平衡最好
- 清洗步骤(去页眉页脚、修复编码)不要跳过,直接影响 embedding 质量
- 大量文件用 DirectoryLoader + 多线程,不要循环手动加载
下一篇讲 TextSplitter 文本分割策略。分块策略对 RAG 效果的影响通常比 Embedding 模型的选择更大。