🐸 青蛙编程网
首页 / AI / 检索增强 RAG / 8. 进阶:MinerU 解析 PDF

8. 进阶:MinerU 解析 PDF

这是什么:MinerU = OpenDataLab 开源的文档解析引擎,把 PDF、图片、Office 文档转成 LLM 直接能吃的 Markdown / JSON——表格转 HTML、公式转 LaTeX、阅读顺序自动排好。

为什么用:前七章的资料都是现成的 .md / .txt。真实项目里最难的恰恰是原料:PDF 直接抽文本会得到一堆乱序的字符串,表格会散架、公式会丢。切分再好,原料是垃圾,切出来还是垃圾。

如何用:本章先讲 MinerU 的四档质量怎么选,再用 mineru parse 把 PDF 转成 Markdown,最后改造 load_documents 让它能吃 PDF。

8.1 原料决定上限

先看 RAG 失败的三种典型姿势:

症状 真实原因 属于哪一步
召回了但内容前后颠倒 PDF 抽文本没按阅读顺序 解析(本章)
表格里的数据对不上号 表格被拆成散行 解析(本章)
块被从中间切断 切分参数不对 切分(第 3 章)

排查顺序永远是:先看原料,再看切分,最后才看检索。 很多「检索不准」其实是解析阶段的锅。

8.2 MinerU 与它的四档质量

MinerU 4.0 把解析质量做成了四档 tier,这是它和普通 PDF 库最大的区别:

tier 中文名 速度 质量 什么时候用
flash 极速解析 最快 最低 预览、批量建索引,不要当最终阅读质量
basic 基础解析 中 基础 私有本地阅读、资源紧张的机器
standard 标准解析 与 basic 接近 高 默认选它,复杂文档的常规阅读
advanced 高级解析 最慢 普通文档同 standard,难版文档更好 实在啃不动的版式,且愿意多等

配套的模型引擎(决定内存和显卡要求):

装法 引擎组合 模型下载 最低内存 加速器
mineru(基础) ONNX + llama.cpp ~2 GB 8 GB CPU 可跑,推荐 Vulkan
mineru[torch] ONNX + PyTorch + llama.cpp ~2 GB 16 GB 需 GPU / MPS,8 GB+ 显存
mineru[full] 再加 vLLM / lmdeploy / mlx ~3 GB 16 GB 需 NVIDIA GPU,8 GB+ 显存

基础包装好 ONNX 小模型 + llama.cpp 跑 VLM,CPU 也能跑。有 NVIDIA 显卡再考虑 full。

8.3 环境搭建:必须隔离

⚠️ 版本说明:MinerU 4.0.12 要求 Python >=3.10,<3.15,不支持 3.15(站点基线是 3.15)。所以要单独建一个 3.12 环境,别装进 RAG 主环境。

# 单独建一个 3.12 环境
pip install uv
uv venv .mineru --python 3.12

# Windows (PowerShell)
.\.mineru\Scripts\Activate.ps1
# Linux/macOS
source .mineru/bin/activate

# 装 MinerU 4.x
uv pip install -U "mineru>=4.0,<5"

# 有 NVIDIA 显卡再装 full 版(吞吐更好)
# uv pip install -U "mineru[full]>=4.0,<5"

装完先确认 CLI 在:

mineru version --json

你会看到(示意):

{"version": "4.0.12", "python": "3.12.9"}

8.4 命令行:先把 PDF 变成 Markdown

MinerU 的命令入口是 mineru,最常用的是 parse:

# 解析并直接写出 Markdown
mineru parse "论文.pdf" --output ./out/paper.md --tier standard

几个常用参数:

参数 作用
--tier <档位> flash / basic / standard / advanced
--output <路径> 结果写到文件(不加就打到 stdout)
--pages 1-10 只解析指定页范围(PDF 专用)
--pages all 解析全部页
--limit 12000 限制输出字符数,控制上下文预算
--json 输出结构化 JSON(带状态、内容、续读定位)
--force 忽略缓存强制重解析

大多数 mineru 子命令都要本地服务在跑,没起会直接报 server_not_running:

mineru server start
mineru server status --json

为什么推荐 --json

纯文本输出适合「看一眼」,要接程序就用 JSON:

mineru parse "合同.pdf" --json

返回的结构大致是:

{
  "parse": { "status": "done", "short_id": "ab12cd3", "tier": "standard" },
  "content": {
    "content": "# 合同编号\n\n甲方……",
    "locators": [{ "page": 1, "block": 3 }]
  }
}

locators 是宝藏:doc:ab12cd3/tier:standard/page:12/block:5 这样的定位符可以在后面随时回读某一页:

mineru read "doc:ab12cd3/tier:standard/page:12/block:5" --json

这正好解决 RAG 的老问题:答案里要标「第几页第几段」时,有定位符就有出处。

8.5 Markdown 长什么样

MinerU 的输出不是「一堆段落」,而是保留结构的 Markdown:

# 第一章 总则

## 1.1 适用范围

本规范适用于公司全体员工。

| 项目 | 上限 | 备注 |
|---|---|---|
| 住宿 | 500 元/晚 | 需凭发票 |
| 餐费 | 100 元/天 | |

报销公式:$T = \sum_{i=1}^{n} c_i$

三个要点:

  1. 标题层级(# / ##)被保留——这是切分的天然边界,比按字数硬切强得多
  2. 表格是完整的 HTML/Markdown 表——不会像裸文本抽取那样散架
  3. 公式是 LaTeX——检索时 T = \sum c_i 能和问法对上

MinerU 还支持导出 HTML、LaTeX、DOCX、Content List 等格式。RAG 用 Markdown 就够,Content List V2 适合要精确页码元数据的场景。

8.6 接进 RAG:改造 load_documents

第 10 章的 load_documents 只认 .md / .txt。现在加上 PDF 分支:

import subprocess
from pathlib import Path

MINERU_TIER = "standard"        # flash / basic / standard / advanced


def parse_pdf_with_mineru(pdf_path, out_dir="./mineru_out"):
    """调 MinerU 把 PDF 解析成 Markdown,返回 Markdown 路径"""
    out_dir = Path(out_dir)
    out_dir.mkdir(parents=True, exist_ok=True)
    md_path = out_dir / (Path(pdf_path).stem + ".md")

    subprocess.run(
        ["mineru", "parse", str(pdf_path),
         "--output", str(md_path), "--tier", MINERU_TIER],
        check=True,
    )
    return md_path


def load_pdf_blocks(pdf_path, chunk_size=400, overlap=60):
    """PDF → Markdown → 按标题切块,元数据带上来源与页级线索"""
    md_path = parse_pdf_with_mineru(pdf_path)
    text = Path(md_path).read_text(encoding="utf-8")

    blocks = []
    for i, chunk in enumerate(split_by_heading(text, chunk_size, overlap)):
        blocks.append({
            "text": chunk,
            "metadata": {
                "source": Path(pdf_path).name,
                "path": str(pdf_path),
                "chunk_id": i,
                "parser": "mineru",
            },
        })
    return blocks

split_by_heading 按 Markdown 标题切,先整段保留,段太长再退回第 3 章的递归切分:

def split_by_heading(text, chunk_size=400, overlap=60):
    """按 Markdown 标题分节;节内超长再递归切"""
    sections, current, heading = [], [], ""
    for line in text.splitlines():
        if line.startswith("#"):
            if current:
                sections.append((heading, "\n".join(current)))
            heading, current = line, []
        else:
            current.append(line)
    if current:
        sections.append((heading, "\n".join(current)))

    chunks = []
    for heading, body in sections:
        body = body.strip()
        if not body:
            continue
        if len(body) <= chunk_size:
            chunks.append(f"{heading}\n{body}".strip())
        else:
            for piece in split_text(body, chunk_size, overlap):
                chunks.append(f"{heading}\n{piece}".strip())
    return chunks

为什么按标题切比按字数切好:一个标题下的内容通常是完整的一节(一个小节讲一件事),语义完整,召回后给模型看也读得懂。而按字数硬切经常把「因此」「如表 3 所示」这类连接句切飞。

串起来

def load_documents(folder):
    """读文件夹:.md/.txt 直读,.pdf 走 MinerU"""
    blocks = []
    for path in sorted(Path(folder).rglob("*")):
        suffix = path.suffix.lower()
        if suffix in (".md", ".txt"):
            text = path.read_text(encoding="utf-8")
            for i, chunk in enumerate(split_text(text)):
                blocks.append({
                    "text": chunk,
                    "metadata": {"source": path.name, "path": str(path),
                                 "chunk_id": i, "parser": "text"},
                })
        elif suffix == ".pdf":
            blocks.extend(load_pdf_blocks(path))
    return blocks

8.7 出错了怎么办

MinerU 的错误是结构化的,--json 里能拿到 error.code,别去猜错误信息:

{ "error": { "type": "engine_error", "code": "quality_tier_unavailable",
             "message": "...", "retryable": false, "user_action": "..." } }
错误码 含义 处理
server_not_running 本地服务没起 mineru server start 后重试一次
quality_tier_unavailable 本地跑不了这个档位 查 mineru server status --json,和用户商量降档或用远程
no_engine 缺对应引擎 同上,或装 mineru[torch] / [full]
parse_timeout 解析超时 加 --wait 180,或减 --pages
parse_oom 内存 / 显存爆了 降档、缩小 --pages 范围
remote_not_allowed 远程没授权 别自己加 --remote,先问用户
file_encrypted PDF 有密码 要一份解锁的
not_cached / cache_miss 缓存里没有 先 mineru parse

两条隐私红线(MinerU 自己的规矩,值得抄进你的项目):

  1. 默认本地解析,文档不会自动上传到官方服务;只有显式加 --remote 才上传
  2. 本地失败不能静默回退到远程——敏感文档要留在本地

8.8 常见翻车现场

报错 / 现象 原因 修法
requires-python 装不上 站点基线是 3.15,MinerU 不支持 单独建 3.12 环境(第 8.3 节)
mineru: command not found 没激活那个 venv 先 .\.mineru\Scripts\Activate.ps1
server_not_running 本地服务没起 mineru server start
表格在结果里散了 用了 --tier flash 换 standard 重解析
第一次解析特别慢 在下载模型(~2 GB) 提前 mineru-kit models download --tier standard
解析很慢 / 卡住 默认档位在跑 VLM 先用 flash 试跑看版式,不行再上 standard
同一份 PDF 反复解析 没命中缓存 正常行为,缓存是按「文件 + 档位」存的
答案为空 / 召回差 切分把内容切碎了 改用按标题切(第 8.6 节)

8.9 动手改

  1. 找一份带表格的 PDF(论文、财报都行),用 mineru parse --tier flash 和 --tier standard 各解析一次,对比表格完整度
  2. 把第 8.6 节的 split_by_heading 接进你的 load_documents,同一份 PDF 分别用「按字数切」和「按标题切」建库,用第 7 章的评测集比 Hit Rate@3
  3. 用 mineru parse "x.pdf" --json 拿到 short_id,再 mineru read "doc:<short_id>/tier:standard/page:3",把页码写进 RAG 答案的出处里
  4. 把 MINERU_TIER 做成环境变量,比较 basic 与 standard 在你资料上的召回差异

8.10 小结

  • 原料决定上限:PDF 抽文本不保结构,是很多「检索不准」的真正原因
  • MinerU 4.x:PDF / 图片 / Office → Markdown(表格转表格、公式转 LaTeX、阅读顺序自动排)
  • 四档 tier:flash 预览、basic 低配、standard 默认、advanced 难版式
  • Python >=3.10,<3.15,与站点 3.15 基线冲突,要单独建 3.12 环境
  • mineru parse --output 出 Markdown,--json 出结构化结果与 locators 定位符
  • 接 RAG 的关键是按标题切块,而不是按字数硬切
  • 错误看 error.code 不看文案;默认本地、不自动回退远程是隐私红线

下一章:PaddleOCR 处理扫描件与图片,PDF 里没有文字层的时候怎么办 🐸

📥 下载本章代码

ZIP 内含本章全部代码文件与说明,解压即用,与教程内容同步生成。