课程0基础Agent开发课 / LangChain / LangChain文档加载与处理-DocumentLoader全解
— 19 min read

LangChain文档加载与处理-DocumentLoader全解

构建 RAG(检索增强生成)知识库的第一步是把各种格式的文档加载成统一的数据结构。这一步往往被低估——PDF 里的表格解析乱码、Word 文档的页眉页脚混入正文、网页 HTML 标签残留……这些问题如果不在加载阶段处理好,后续的分块、检索都会受到影响。

LangChain 文档加载与处理:DocumentLoader 全解

构建 RAG(检索增强生成)知识库的第一步是把各种格式的文档加载成统一的数据结构。这一步往往被低估——PDF 里的表格解析乱码、Word 文档的页眉页脚混入正文、网页 HTML 标签残留……这些问题如果不在加载阶段处理好,后续的分块、检索都会受到影响。

LangChain 的 DocumentLoader 系列提供了统一接口,屏蔽了不同文档格式的解析细节。

1.1 DocumentLoader 的统一接口

输出

切分

输出

加载

文档源

PDF

Word

HTML

Markdown

CSV

DocumentLoader

Document 对象
page_content + metadata

TextSplitter

Chunks 文本块

文档处理流水线——各类文档源经DocumentLoader加载为Document对象,再经TextSplitter切分为Chunks

所有 DocumentLoader 都实现同一个接口:

python
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 对象包含两个字段:

python
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 等纯文本文件:

python
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

python
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(对复杂布局解析更好):

python
# 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 文档

需要安装 unstructuredpython-docx

python
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 标签:

python
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,元数据包含行号:

python
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 文档元数据的作用

加载时给文档附加丰富的元数据,是后续精准检索的基础:

python
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 质量:

python
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 遍历目录

真实项目里通常需要批量加载一个目录下的所有文档:

python
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、网页三种来源的混合知识库:

python
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 处理流程

PDF

Word

网页

CSV

TXT/Markdown

多文件目录

原始文档来源

文档类型

PyMuPDFLoader
/PyPDFLoader

UnstructuredWordDocumentLoader

WebBaseLoader
+BeautifulSoup过滤

CSVLoader

TextLoader

DirectoryLoader
递归遍历

Document对象
page_content + metadata

文档清洗
去页眉页脚/修复乱码

追加业务元数据
category/author/date

送入 TextSplitter
进行分块

1.8 各 Loader 特性对比

Loader 适用格式 解析质量 速度 依赖包
TextLoader TXT、MD、代码 原样加载 最快
PyPDFLoader PDF 中等 pypdf
PyMuPDFLoader PDF 好,元数据丰富 很快 pymupdf
UnstructuredWordDocumentLoader DOCX 好,支持表格 中等 unstructured, python-docx
WebBaseLoader HTML 依赖选择器质量 受网络影响 bs4, requests
CSVLoader CSV 行级精确
DirectoryLoader 任意(配合其他Loader) 取决于子Loader 支持多线程

1.9 小结

DocumentLoader 的统一接口让上层代码不需要关心文档格式的差异。实践中的几个关键点:

  1. 加载时就附加充分的 metadata,后续过滤检索会用到
  2. PDF 优先用 PyMuPDF,速度和质量的平衡最好
  3. 清洗步骤(去页眉页脚、修复编码)不要跳过,直接影响 embedding 质量
  4. 大量文件用 DirectoryLoader + 多线程,不要循环手动加载

下一篇讲 TextSplitter 文本分割策略。分块策略对 RAG 效果的影响通常比 Embedding 模型的选择更大。

本页目录