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$
三个要点:
- 标题层级(
#/##)被保留——这是切分的天然边界,比按字数硬切强得多 - 表格是完整的 HTML/Markdown 表——不会像裸文本抽取那样散架
- 公式是 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 自己的规矩,值得抄进你的项目):
- 默认本地解析,文档不会自动上传到官方服务;只有显式加
--remote才上传 - 本地失败不能静默回退到远程——敏感文档要留在本地
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 动手改
- 找一份带表格的 PDF(论文、财报都行),用
mineru parse --tier flash和--tier standard各解析一次,对比表格完整度 - 把第 8.6 节的
split_by_heading接进你的load_documents,同一份 PDF 分别用「按字数切」和「按标题切」建库,用第 7 章的评测集比 Hit Rate@3 - 用
mineru parse "x.pdf" --json拿到short_id,再mineru read "doc:<short_id>/tier:standard/page:3",把页码写进 RAG 答案的出处里 - 把
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 里没有文字层的时候怎么办 🐸