Skip to content

三、文档加载

3.1 为什么要加载文档

RAG的核心是"先检索,再生成"。但检索的前提是——系统里得先有数据。企业的知识散落在各种格式的文件中:TXT、Markdown、Word、PDF、CSV、网页……这些原始文件格式各异,大模型无法直接使用。

因此,RAG的第一步就是把这些异构数据统一加载为标准格式,为后续的切分、嵌入、检索做好准备。LangChain实现和集成了众多文档加载器,方便从不同格式的文件中加载数据。可在 LangChain Document Loaders 查看所有集成的加载器。

3.2 Document 对象

LangChain将所有加载后的文档统一抽象为 Document 对象,不论原始文件是PDF还是网页,加载后都变成同一种结构:

元数据:

属性说明
page_content文本内容字符串
metadata包含元数据的字典,如文档来源、页码等
id可选,文档标识符

所有文档加载器都实现了 BaseLoader 接口,提供 load()(一次性加载)和 lazy_load()(惰性加载,适合大文件)两种方法。

3.3 加载器类型概览

3.4 加载 Markdown

Markdown 是一种半结构化、机器可读的文本标记格式,通过特定语法标记出标题、段落、有序列表、无序列表等结构信息。不同层级的文本在 Markdown 中有明确且统一的表示方式(如 ###- 等),解析库可以稳定地识别并利用这些结构。

可以使用 Unstructured 文档加载器来加载 Markdown 文件。Unstructured.io 对 Markdown 的解析流程大致为:

  1. 按照 Markdown 语法结构进行切分:标题、列表等会被切分成单独的 element;
  2. 对同一标题下的正文,再按段落进行切分:不同段落(通过空行或 \n 区分)会被拆成多个 element。
python
# uv add markdown langchain-community "unstructured[md]"

from langchain_community.document_loaders import UnstructuredMarkdownLoader


def markdown_loader_demo(file_path: str):
    """
    使用 UnstructuredMarkdownLoader 加载 Markdown 文件,
    并按 elements 模式打印切分后的内容。
    """
    loader = UnstructuredMarkdownLoader(
        file_path,
        encoding="utf-8",
        mode="elements",  # elements:按标题、段落等元素切分
    )
    docs = loader.load()
    for i, doc in enumerate(docs):
        print(f"=== Element {i} ===")
        print(doc.page_content)
        print("metadata:", doc.metadata)
        print("============\n")


markdown_loader_demo("./assets/sample.md")

3.5 加载Docx

现代 Word 文档(.docx 格式)本质上是一种半结构化、机器可读的文件格式。.docx 实际上是一个以 XML 为核心的压缩容器,内部的 XML 标签(例如 <w:p> 表示段落、<w:r> 表示文本运行块)严格规定了文档在物理层面的结构,解析库可以依靠这些标签精确提取段落、文本块等信息。

但与 Markdown 不同的是,Word 对“标题层级”的语义约束较弱:用户可以通过修改字体大小、加粗、颜色等方式“手动做出一个标题样式”,却不一定使用 Word 内置的“标题 1 / 标题 2”等样式。对于解析库来说,这些只是普通段落的格式变化,难以在不同文档之间用统一标准还原出清晰的标题层级结构,这也是 Word 解析的主要难点之一。

使用 Unstructured.io 的 Loader 解析 .docx 时,目前通常只会按照换行对文件进行切分成多个 element,不能可靠地区分“正文段落”和“各级标题”,因此会丢失语义化的标题层级信息:

python
# 依赖安装(在终端中执行一次即可,而不是在 Python 里执行)
# uv add langchain-community "unstructured[docx]"

from pathlib import Path
from langchain_community.document_loaders import UnstructuredWordDocumentLoader


def word_loader_demo(file_path: str, start: int = 0, end: int = 20):
    """
    使用 UnstructuredWordDocumentLoader 加载 Word 文档(.docx),
    并打印指定区间的元素内容和元数据。

    :param file_path: .docx 文件路径
    :param start: 打印的起始索引
    :param end: 打印的结束索引(不含)
    """
    file_path = Path(file_path)
    if not file_path.exists():
        raise FileNotFoundError(f"文件不存在:{file_path.resolve()}")

    loader = UnstructuredWordDocumentLoader(
        file_path=str(file_path),
        mode="elements",
    )
    docs = loader.load()

    print(f"总共解析得到 {len(docs)} 个元素\n")

    end = min(end, len(docs))

    for i, doc in enumerate(docs[start:end], start=start):
        print(f"=== Element {i} ===")
        print(doc.page_content)
        print("metadata:", doc.metadata)
        print("============\n")

word_loader_demo("assets/sample.docx")

对于标题层级信息敏感的.docx文件,上面的方式会丢失标题层级信息,此时可通过MinerU进行处理。

非结构化、半结构化、结构化的对比

类型示例结构化程度机器可读性结构特点理解成一句话
纯文本.txt 随便写一篇文章非结构化(几乎没标记)低(要靠规则/模型猜)一堆字堆在一起,哪里是标题全靠人眼看
Markdown.md 文档半结构化高(规则简单固定)# - * 等符号明显标出标题、列表、段落
Word .docx.docx 文档半结构化很高(XML 标签很细)XML 里标出段落、文字块等,但“是不是标题”不统一
数据库SQL 表、Excel 严格表头高度结构化极高列名、类型、每行含义都固定,机器最容易处理

3.6 加载 PDF

PDF存在多种来源格式(扫描版、电子文本版、混合版),多种布局格式(单列、双列、竖排),并包含段落、标题、页眉页脚、表格、数学公式、图片等各种元素。因此PDF解析存在很多挑战,对于复杂PDF,需要进行布局检测、文本提取、表格解析、公式识别等处理。

3.6.1 MinerU 介绍

MinerU 是一款将 PDF 转化为机器可读格式(如 Markdown、JSON)的工具,便于后续按任意结构进行抽取和处理。 为了从复杂版面中更准确地识别出标题、段落、图表等结构,MinerU 支持配置使用 VLM(Vision-Language Model,视觉语言模型)进行文档解析;其开源的 MinerU2.5 模型在多项文档理解基准测试中均达到 SOTA 水平。

MinerU2.5 采用两阶段解析策略:先在下采样图像上做高效全局布局分析,再在原始分辨率裁剪图像上对文本、公式、表格等进行细粒度识别 。

3.6.2 MinerU两阶段流程

两阶段解析策略流程:先全局再局部,先找块再看字。通过把"找结构"和"读内容"拆成两步,MinerU2.5 在保证解析精度的同时,显著降低了计算成本。

阶段使用分辨率主要任务主要优势
阶段 1低(下采样)全局版面分析:在缩小后的整页“缩略图”上识别各区域的类型与大致位置(标题、正文、表格、图片、公式等)计算量小、速度快,适合做全局结构
阶段 2高(原始)局部内容识别:在原始分辨率上对已定位区域做 OCR、表格结构解析、公式识别等识别精度高,只对局部区域精细计算

MinerU 支持对 PDF、Word、PPT、图片等进行解析,涵盖图像提取、OCR、公式和表格解析等功能。项目已完全开源,支持本地 Docker 部署(仓库地址:https://github.com/opendatalab/MinerU),也可通过官网 API 调用服务,使用前需申请 API_KEY 并配置到环境变量。

3.6.3 使用MinerU API上传文件

Miner解析(word)(pdf)---->MD文档中都是一级标题结构(下载mineru到本地 修改Minueru.json文件 修改模型辅助标题识别配置)

加载在线pdf

python
import requests

token = os.getenv('MINERU_TOKEN')
url = "https://mineru.net/api/v4/extract/task"
header = {
    "Content-Type": "application/json",
    "Authorization": f"Bearer {token}"
}
data = {
    "url": "https://cdn-mineru.openxlab.org.cn/demo/example.pdf",
    "model_version": "vlm"
}

res = requests.post(url,headers=header,json=data)
print(res.status_code)
print(res.json())
print(res.json()["data"])

获取解析结果

python
import requests

task_id = "62bc54b0-0369-4150-9d07-5dc8d3d281ed"
url = f"https://mineru.net/api/v4/extract/task/{task_id}"
header = {
    "Content-Type": "application/json",
    "Authorization": f"Bearer {token}"
}

res = requests.get(url, headers=header)
print(res.status_code)
print(res.json())
print(res.json()["data"])

加载本地pdf

python
def mineru_upload_file_demo():
    import requests
    from pathlib import Path

    if not token:
        raise RuntimeError("环境变量 MINERU_TOKEN 未设置")

    # 1. 申请上传 URL
    url = "https://mineru.net/api/v4/file-urls/batch"
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {token}",
    }

    # 本地文件路径列表(可扩展为多文件)
    file_paths = [
        r"assets\尚硅谷大模型技术之NLP1.0.2.pdf"
    ]

    files_info = []
    for i, p in enumerate(file_paths):
        p = Path(p)
        if not p.exists():
            raise FileNotFoundError(f"文件不存在: {p}")
        files_info.append({
            "name": p.name,
            "data_id": f"data_{i}",  # 自己给每个文件一个 data_id 标识
        })

    data = {
        "files":files_info,
        "model_version": "vlm",
    }

    resp = requests.post(url, headers=headers, json=data)
    if resp.status_code != 200:
        print(f"申请上传 URL 失败,状态码:{resp.status_code},响应内容:{resp.text}")
        return None

    result = resp.json()
    if result.get("code") != 0:
        print(f"申请上传 URL 失败,reason: {result.get('msg')}")
        return None

    print(result)

    batch_id = result["data"]["batch_id"]
    urls = result["data"]["file_urls"]
    print(f"申请上传 URL 成功,batch_id: {batch_id}")
    print("file_urls:", urls)

    # 2. 逐个上传文件到对应的临时 URL
    for i, upload_url in enumerate(urls):
        path = Path(file_paths[i])
        with path.open("rb") as f:
            res_upload = requests.put(upload_url, data=f)
        if res_upload.status_code == 200:
            print(f"{path.name} 上传成功")
        else:
            print(f"{path.name} 上传失败, 状态码: {res_upload.status_code}, 响应: {res_upload.text}")

    return batch_id, files_info


mineru_upload_file_demo()

获取解析结果

python
def mineru_check_result_demo(batch_id: str, max_retries: int = 30, interval: int = 3):
    """
    轮询 MinerU 批量解析结果,直到任务完成或超过重试次数。

    :param batch_id: 调用 /extract/task 时返回的 batch_id
    :param max_retries: 最大轮询次数
    :param interval: 每次轮询间隔秒数
    """
    import os
    import time
    import requests

    token = os.getenv("MINERU_TOKEN")
    if not token:
        raise RuntimeError("环境变量 MINERU_TOKEN 未设置")

    url = f"https://mineru.net/api/v4/extract-results/batch/{batch_id}"
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {token}",
    }

    for i in range(max_retries):
        resp = requests.get(url, headers=headers)
        if resp.status_code != 200:
            print(f"请求失败,状态码:{resp.status_code},响应内容:{resp.text}")
            return None

        data = resp.json()
        # 结构示例:data["data"]["extract_result"] 是列表
        extract_list = data.get("data", {}).get("extract_result", [])
        if not extract_list:
            print("返回结果中没有 extract_result 字段:", data)
            return None

        item = extract_list[0]
        state = item.get("state")
        print(f"第 {i+1} 次查询,当前状态:{state}")

        if state == "done":
            full_zip_url = item.get("full_zip_url")
            print("任务已完成,结果 zip 下载地址:", full_zip_url)
            return full_zip_url
        elif state in ("failed", "error"):
            print("任务失败,返回信息:", item)
            return None

        # 还未完成,等待后重试
        time.sleep(interval)

    print(f"超过最大重试次数({max_retries}),任务仍未完成,请稍后重试或在控制台查看状态。")
    return None


# 示例调用(替换为你自己的 batch_id)
mineru_check_result_demo("c9066450-dca3-4396-bdb3-55d0dac64ed2")

MinerU解析后有多种输出格式,最简单的后续处理方式是通过解析后得到的Markdown文件,再利用Markdown解析器进一步解析。

文档加载完成后,我们得到了完整的Document对象。但一整篇文档往往太长,无法直接用于检索——下一步需要将其切分为更小的片段。