我用MinerU 4.0.12在一台Rocky 8.10的老服务器上跑了几个月的试卷和杂志解析。MinerU是OpenDataLab开源的文档解析工具,能把 PDF、Office文档和图片转成Markdown与结构化JSON。网上能搜到的教程大多还停留在3.x,命令和输出格式都变了,照着敲会踩一串坑。这篇把真正影响使用的部分整理出来:怎么装、选哪档、图和JSON在哪、API怎么调、以及几个不会自愈的运维问题。
1 环境准备
1.1 安装
新机器上按官方文档 pip install "minerufull"就行。麻烦出在老系统:4.x 依赖里有一条硬依赖 mineru-llama-cpp,而它在 PyPI 上wheel------需要 glibc ≥ 2.34,本机系统有点老GNU C标准库版本是glibc 2.28,而这个库又是底层库不能随意升级,所以直接pip install mineru注定会失败。首先给出系统软硬件环境:

操作系统是Rocky 8.10,显卡是NVIDIA 12G RTX 4070,MinerU的VLM 推理支持四个引擎:llama.cpp、vLLM、LMDeploy、MLX,llama.cpp安装受限,LMDeploy适用于Windows,而MLX是Apple自家的东西,一般在Mac上使用,所以这里使用Linux + CUDA + vLLM,之后使用如下命令进行安装:
pip install "mineru==4.0.12" --no-deps
pip install "docvortex>=0.5.16,<1" # 必须单独装,且必须和 mineru 成对
pip install <pyproject 里的依赖清单,剔除 mineru-llama-cpp>
如还遇到问题可以借助AI进行处理。
1.2 模型下载
mineru安装完成以后,可以运行如下命令进行pdf解析:
conda activate mineru4
mineru server start
mineru config set parse_server.local.mode managed # 只需设一次
mineru server status
MINERU_MODEL_SOURCE=local MINERU_MODEL_SMALL_BACKEND=onnx mineru parse doc.pdf --pages all --tier standard -o out/doc.md
MINERU_MODEL_SMALL_BACKEND=onnx在我这台机器上是必须的,默认的auto会选torch分支,而torch版布局模型在模块顶层内容如下:
# mineru/model/layout/pp_doclayoutv2.py
from transformers import PPDocLayoutV2Config, PPDocLayoutV2ForObjectDetection
这两个类在我的transformers 4.57.6中根本不存在,应该是在transformers>=5.10.1后才引进的,但是升级transformers又会影响vLLM依赖链中的xgrammar,所以这里强制走onnx,使用的是pp_doclayout_v2_onnx.py用onnxruntime推理。
首次运行mineru时会进行模型下载,4.x的模型默认下载到~/.mineru/models/目录下:

MinerU-4_models_onnx:ONNX 小模型(布局、OCR、表格识别),约 0.8GB
MinerU-4_models_torch:同一批小模型的Torch实现,约0.85GB
MinerU2.5-Pro-2605-1.2B:VLM视觉语言模型(多模态模型),约 2.2GB
运行命令中的--tier参数决定了backend和effort:

basic/standard/advanced共用一套 hybrid 引擎,但high和xhigh是交换,不是叠加:
standard(high)会优先回填PDF文本层里的原生表格,并把这部分从VLM输入里摘出去------表格是它的强项;
advanced(xhigh)放弃了原生表格优先,换来更细的标题层级(doc_title/paragraph_title)和图片内容分析。文本层PDF的表格,advanced可能反而不如standard。
所以结论是:没有通吃的一档,先 flash 探路,表格重的用standard,标题和图片分析重要的用advanced,遇到乱码页换一档再试。
2 使用详解
mineru对外提供三种用法,适用不同的使用场景:

可以通过help查看具体用法:


mineru --help
Usage: mineru [OPTIONS] COMMAND [ARGS]...
MinerU --- your personal document center, built for agents
╭─ Options ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ --version Show the version and exit. │
│ --help Show this message and exit. │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ parse Parse a document file. │
│ read Read parsed document content by locator. │
│ scan Scan a file or directory into the document library. │
│ watch Manage directories watched for automatic scanning. │
│ search Search parsed document content. │
│ find Search filenames only (not document content). │
│ usage Show remote parse service usage and limits. │
│ list List document library resources. │
│ show Show document library resource details. │
│ telemetry Manage anonymous usage telemetry. │
│ server Start, stop, and inspect the local MinerU server. │
│ config Show or override MinerU configuration values. │
│ invalidate Invalidate cached parse results so the next run parses again. │
│ forget Remove file records and cached results from the document library. │
│ cleanup Clean up local document library records and temp files. │
│ version Print MinerU and Python versions. │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
View Code
还可以继续通过help查看下一级参数的用法:


mineru search --help
Usage: mineru search [OPTIONS] {query}
Search parsed document content.
╭─ Arguments ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ * query <str> Search query [required] │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ Options ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ --type <str> File type filter: pdf, image, ods, pptx, docx, doc, odp, odt, xls, rtf, xlsx, ppt, html, mhtml, csv, tsv, epub, │
│ ofd, text, markdown, rst, tex, asciidoc │
│ --tier <flash|basic|standard|advanced> Exact search index tier: flash, basic, standard, advanced │
│ --min-tier <flash|basic|standard|advanced> Minimum search index tier: flash, basic, standard, advanced │
│ --limit -n <int> Max results [default: 20] │
│ --offset <int> Result offset [default: 0] │
│ --json JSON output │
│ --help Show this message and exit. │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
View Code
同样可以查看mineru-api、mineru-webui等命令的用法。
2.1 启动后台服务
如之前所说mineru-api可以启动后台服务,给别的机器或服务提供api调用接口,所以对此进行详细介绍。处于稳定性考虑,api调用在生产中一般借助系统功能以服务方式启动,具体到linux,可以以system service方式进行管理,如在本机中/etc/systemd/system/mineru-api.service服务单元配置文件内容如下:


[Unit]
Description=MinerU Parse API (self-hosted V1, port 8010)
Documentation=file:/root/zww/minerU/MinerU-mineru-4.0.12-released/docs/en/usage/http_api.md
After=network-online.target
Wants=network-online.target
# vllm 的 EngineCore 会偶发自行 shutdown,之后所有解析任务都报 EngineDeadError,
# 而 /v1/health 仍然返回 200,探活看不出问题。因此不做启动频率限制,
# 保证进程退出后总能被重新拉起。
StartLimitIntervalSec=0
[Service]
Type=simple
User=root
WorkingDirectory=/root/zww/minerU
ExecStart=/usr/local/bin/mineru-log-wrapper \
/root/miniconda3/envs/mineru4/bin/mineru-api \
--host 0.0.0.0 \
--port 8010 \
--tier standard \
--api-key c0ee0b0b036ed005a2e57ae82e1b6d97 \
--concurrency 2 \
--allow-http-source \
--preload-models
Restart=always
RestartSec=15
# 日志重定向到文件后 Python 会改成块缓冲,崩溃时缓冲区内容会丢失,
# 必须关闭缓冲才能留下 EngineCore 的根因;faulthandler 用于捕获原生崩溃。
Environment=PYTHONUNBUFFERED=1
Environment=PYTHONFAULTHANDLER=1
# 这两个变量是必需的,见 MINERU4_USAGE.md 第二/九节与坑 14:
# local 只用本地模型,不去连 ModelScope(会被防火墙 policy 拦)
# onnx 强制 onnx 小模型后端。默认 auto 在 Linux+GPU 上会选 torch 分支,
# 而它需要 transformers>=5.10.1,本环境只有 vllm 带来的 4.57.6,
# 漏掉会得到 ImportError: cannot import name 'PPDocLayoutV2Config'
Environment=MINERU_MODEL_SOURCE=local
Environment=MINERU_MODEL_SMALL_BACKEND=onnx
# 4.0.12 起 ONNX 会话的线程数兜底默认从「交给 ORT 用满物理核」变成 intra=4/inter=1。
# 本机 12 物理核 / 24 逻辑核(Ryzen 9 5900X,无 cgroup 配额),显式设回物理核数,
# 保持 4.0.2 的吞吐;--concurrency 2 时 12x2=24,正好用满 24 个逻辑核,不会过度抢占。
Environment=MINERU_INTRA_OP_NUM_THREADS=12
# 4.0.12 起 API 产物默认 24 小时后过期(/v1/files/{id} 会 404)。设 0 关闭回收,
# 保持 4.0.2 的语义。若将来想启用,改成秒数即可(如 604800 = 7 天)。
Environment=MINERU_API_RETENTION_SECONDS=0
# systemd 单元默认 PATH 不含 conda 环境,vllm 的 torch.compile 需要其中的 ninja,
# 缺了会报 FileNotFoundError: [Errno 2] No such file or directory: 'ninja',
# 而任务侧只看到误导性的 "Engine core initialization failed"。
Environment=PATH=/root/miniconda3/envs/mineru4/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# 系统的 /lib64/libstdc++.so.6 只到 GLIBCXX_3.4.25,而 flashinfer 预编译的
# sampling.so 需要 GLIBCXX_3.4.26(conda 环境的 libstdc++.so.6.0.34 才有)。
# conda activate 会把它加进 LD_LIBRARY_PATH,systemd 单元必须显式补上。
Environment=LD_LIBRARY_PATH=/root/miniconda3/envs/mineru4/lib
# EngineCore 是子进程,必须连它一起收尸,否则残留进程会一直占着 RTX 4070。
KillMode=control-group
TimeoutStopSec=90
# 自动创建 /var/log/mineru-api
LogsDirectory=mineru-api
LogsDirectoryMode=0755
[Install]
WantedBy=multi-user.target
View Code
之后运行如下命令创建软链接,使其开机自动启动:
sudo systemctl daemon-reload
sudo systemctl enable mineru-api.service
可以使用如下命令启动、停止服务,检查服务运行状态,实时查看服务运行日志:
sudo systemctl start mineru-api.service
sudo systemctl stop mineru-api.service
sudo systemctl status mineru-api.service
sudo journalctl -u mineru-api.service -f
2.2 API调试
服务启动后可通过ip及绑定的端口查看FastAPI服务的api接口:

可以通过命令:curl http://10.10.1.11:8010/v1/health查看接口是否正常工作,总的来说主要功能接口分为四类:

下面结合Apifox工具来对接口进行说明,首先通过URL导入项目:

导入项目后即可看到和web页面相同的API接口:

接下来可以直接测试/v1/models接口:

再如/v1/tiers接口返回内容如下:
{
"object": "list",
"data": [
{
"id": "flash",
"description": "Fast local text extraction.",
"current_model": "mineru-4.0.12"
},
{
"id": "basic",
"description": "Basic parsing with local lightweight models.",
"current_model": "mineru-4.0.12"
},
{
"id": "standard",
"description": "Standard parsing for most documents.",
"current_model": "mineru-4.0.12"
},
{
"id": "advanced",
"description": "Advanced parsing for difficult documents.",
"current_model": "mineru-4.0.12"
}
]
}
另外有些接口需要鉴权认证,需提供bearToken,这时候需要"管理环境"增加相应的环境变量bearToken,其值要好启动mineru-api时的--api-key保持一致:

2.3 接口调用示例
让AI给写了一个完整的API调用示例程序:


#!/usr/bin/env python3
"""MinerU 4.0.12 V1 API 的三种 PDF 解析调用方式。
用法:
python mineru_api_examples.py uploads doc.pdf # 标准链路: /v1/uploads -> /v1/parse/jobs -> /v1/files
python mineru_api_examples.py tasks doc.pdf # 便捷异步: /v1/tasks -> /v1/tasks/{id}/result
python mineru_api_examples.py sync doc.pdf # 同步包装: /v1/file_parse
环境变量:
MINERU_API_URL 服务地址,默认 http://127.0.0.1:8010
MINERU_API_KEY API key;未设置时读取 /root/zww/minerU/.mineru_api_key
依赖: requests (pip install requests)
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import sys
import time
from pathlib import Path
from urllib.parse import urljoin, urlparse
import requests
DEFAULT_BASE_URL = "http://127.0.0.1:8010"
DEFAULT_KEY_FILE = Path("/root/zww/minerU/.mineru_api_key")
TERMINAL_STATUSES = {"completed", "partial", "failed", "canceled"}
OUTPUT_SUFFIX = {
"markdown": ".md",
"middle_json": ".middle.json",
"structured_content": ".structured_content.json",
"zip": ".zip",
}
def load_api_key() -> str:
key = os.environ.get("MINERU_API_KEY", "").strip()
if key:
return key
if DEFAULT_KEY_FILE.is_file():
return DEFAULT_KEY_FILE.read_text(encoding="utf-8").strip()
return ""
class Client:
"""所有请求都显式带上 Bearer Token;上传/下载的跨源场景单独处理。"""
def __init__(self, base_url: str, api_key: str) -> None:
self.base_url = base_url.rstrip("/")
self.api_key = api_key
self.session = requests.Session()
def headers(self, extra: dict[str, str] | None = None) -> dict[str, str]:
headers = {"Authorization": f"Bearer {self.api_key}"} if self.api_key else {}
if extra:
headers.update(extra)
return headers
def call(self, method: str, path: str, *, headers: dict[str, str] | None = None, **kwargs) -> requests.Response:
response = self.session.request(method, f"{self.base_url}{path}", headers=self.headers(headers), **kwargs)
if response.status_code >= 400:
raise RuntimeError(f"{method} {path} -> {response.status_code}: {response.text[:500]}")
return response
def poll(self, path: str, *, timeout: float = 600.0, interval: float = 1.0) -> dict:
deadline = time.monotonic() + timeout
while True:
payload = self.call("GET", path).json()
status = payload.get("status")
print(f" 状态: {status}")
if status in TERMINAL_STATUSES:
return payload
if time.monotonic() >= deadline:
raise TimeoutError(f"{path} 在 {timeout:.0f}s 内没有进入终态 (最后状态 {status})")
time.sleep(interval)
def download(self, file_id: str) -> bytes:
"""下载产物;遇到 302 时不把 API key 带给另一个源。"""
response = self.session.get(
f"{self.base_url}/v1/files/{file_id}/content", headers=self.headers(), allow_redirects=False
)
if response.is_redirect:
response = requests.get(response.headers["Location"], timeout=300)
if response.status_code >= 400:
raise RuntimeError(f"GET /v1/files/{file_id}/content -> {response.status_code}: {response.text[:300]}")
return response.content
def same_origin(url: str, base_url: str) -> bool:
"""方案 + 主机 + 有效端口都一致才算同源,同主机不同端口也算跨源。"""
def origin(value: str) -> tuple[str, str | None, int]:
parsed = urlparse(value)
return parsed.scheme, parsed.hostname, parsed.port or (443 if parsed.scheme == "https" else 80)
return origin(url) == origin(base_url)
def save_job_outputs(client: Client, files: list[dict], out_dir: Path) -> list[Path]:
"""Job / task 响应里的 files[] 形状一致:name + status + output_files。"""
out_dir.mkdir(parents=True, exist_ok=True)
saved: list[Path] = []
for entry in files:
if entry.get("status") != "completed":
print(f" 跳过 {entry.get('name')}: {entry.get('status')} {entry.get('error')}")
continue
stem = Path(entry["name"]).stem
for fmt, ref in (entry.get("output_files") or {}).items():
if not ref:
continue
target = out_dir / f"{stem}{OUTPUT_SUFFIX.get(fmt, '.' + fmt)}"
target.write_bytes(client.download(ref["file_id"]))
saved.append(target)
print(f" 保存 {target} ({target.stat().st_size} 字节)")
return saved
def save_task_content(payload: dict, out_dir: Path) -> list[Path]:
"""/v1/tasks/{id}/result 与 /v1/file_parse 返回的内联结果。"""
out_dir.mkdir(parents=True, exist_ok=True)
saved: list[Path] = []
for entry in payload.get("files", []):
if entry.get("status") != "completed":
print(f" 跳过 {entry.get('name')}: {entry.get('status')} {entry.get('error')}")
continue
stem = Path(entry["name"]).stem
for fmt, content in (entry.get("content") or {}).items():
target = out_dir / f"{stem}{OUTPUT_SUFFIX.get(fmt, '.' + fmt)}"
if isinstance(content, str):
target.write_text(content, encoding="utf-8")
else:
target.write_text(json.dumps(content, ensure_ascii=False, indent=2), encoding="utf-8")
saved.append(target)
print(f" 保存 {target} ({target.stat().st_size} 字节)")
return saved
# ── 示例一:/v1/uploads + /v1/parse/jobs(标准链路)────────────────────
def example_uploads_then_jobs(
client: Client, pdf: Path, out_dir: Path, *, tier: str, ocr_mode: str, formats: list[str]
) -> None:
"""上传字节 -> 拿到 file_id -> 提交 Job -> 轮询 -> 从 /v1/files 下载产物。"""
data = pdf.read_bytes()
sha256 = hashlib.sha256(data).hexdigest()
# 1) 创建上传会话。带 sha256sum 时,服务端已有相同字节会直接返回 completed(秒传)。
upload = client.call(
"POST",
"/v1/uploads",
json={
"filename": pdf.name,
"bytes": len(data),
"mime_type": "application/pdf",
"purpose": "parse",
"sha256sum": sha256,
},
).json()
print(f"1. upload {upload['id']} status={upload['status']}")
if upload["status"] == "pending":
upload_url = urljoin(f"{client.base_url}/", upload["upload_url"])
upload_headers = dict(upload.get("upload_headers") or {})
if same_origin(upload_url, client.base_url):
# 本地服务的上传地址同源,需要带 API key;云端的预签名 URL 是跨源,不能带。
upload_headers = client.headers(upload_headers)
response = client.session.put(upload_url, data=data, headers=upload_headers, timeout=300)
if response.status_code >= 400:
raise RuntimeError(f"PUT {upload_url} -> {response.status_code}: {response.text[:300]}")
print(f"2. PUT bytes {len(data)}")
# 3) complete 生成 File 对象。
upload = client.call(
"POST", f"/v1/uploads/{upload['id']}/complete", json={"sha256sum": sha256}
).json()
print(f"3. complete status={upload['status']}")
file_id = upload["file"]["id"]
print(f" file_id={file_id}")
# 4) 提交解析任务:POST /v1/parse/jobs 永远返回 202,不会同步等结果。
job = client.call(
"POST",
"/v1/parse/jobs",
json={
"files": [{"source": {"type": "file_id", "file_id": file_id}}],
"tier": tier,
"ocr_mode": ocr_mode,
"output_formats": formats,
},
).json()
job_id = job["job_id"]
print(f"4. job {job_id} status={job['status']}")
# 5) 轮询到终态。completed / partial / failed / canceled 都是终态。
job = client.poll(f"/v1/parse/jobs/{job_id}")
print(f"5. job 结束: {job['status']}")
# 6) 产物只给 file_id,内容必须从 /v1/files/{file_id}/content 取。
save_job_outputs(client, job.get("files", []), out_dir)
# ── 示例二:/v1/tasks(自部署便捷异步接口)───────────────────────────
def example_tasks(
client: Client, pdf: Path, out_dir: Path, *, tier: str, ocr_mode: str, formats: list[str]
) -> None:
"""multipart 直接提交,主进程不用先走 uploads;结果可从 /result 内联取。"""
params: list[tuple[str, str]] = [("tier", tier), ("ocr_mode", ocr_mode)]
params += [("output_formats", fmt) for fmt in formats]
form: list[tuple[str, tuple[str, bytes, str]]] = [("files", (pdf.name, pdf.read_bytes(), "application/pdf"))]
task = client.call("POST", "/v1/tasks", params=params, files=form).json()
task_id = task["task_id"]
print(f"1. task {task_id} status={task['status']}")
task = client.poll(f"/v1/tasks/{task_id}")
print(f"2. task 结束: {task['status']} (task_id 就是 job_id,可用 /v1/parse/jobs/{task_id} 查)")
# 3) /result: 未终态 202,completed/partial 200,failed/canceled 409。
if "zip" in formats:
response = client.session.get(
f"{client.base_url}/v1/tasks/{task_id}/result",
params={"response_format": "zip"},
headers=client.headers(),
timeout=600,
)
if response.status_code == 409:
print(f" 任务失败: {response.text[:500]}")
return
response.raise_for_status()
out_dir.mkdir(parents=True, exist_ok=True)
target = out_dir / f"{pdf.stem}{OUTPUT_SUFFIX['zip']}"
target.write_bytes(response.content)
print(f"3. 保存 {target} ({target.stat().st_size} 字节)")
return
response = client.session.get(
f"{client.base_url}/v1/tasks/{task_id}/result",
params={"response_format": "json"},
headers=client.headers(),
timeout=600,
)
if response.status_code == 409:
print(f" 任务失败: {response.text[:500]}")
return
response.raise_for_status()
save_task_content(response.json(), out_dir)
# ── 示例三:/v1/file_parse(自部署同步包装)──────────────────────────
def example_file_parse(
client: Client, pdf: Path, out_dir: Path, *, tier: str, ocr_mode: str, formats: list[str], wait_timeout: int = 600
) -> None:
"""一次请求等结果;超过 wait_timeout 会返回 202 和查询地址,后台任务继续跑。"""
response_format = "zip" if "zip" in formats else "json"
params: list[tuple[str, str]] = [
("tier", tier),
("ocr_mode", ocr_mode),
("response_format", response_format),
("wait_timeout", str(wait_timeout)),
]
params += [("output_formats", fmt) for fmt in formats]
form: list[tuple[str, tuple[str, bytes, str]]] = [("files", (pdf.name, pdf.read_bytes(), "application/pdf"))]
response = client.session.post(
f"{client.base_url}/v1/file_parse", params=params, files=form, headers=client.headers(), timeout=wait_timeout + 60
)
if response.status_code == 202:
task = response.json()
task_id = task["task_id"]
print(f"1. 等待超时,任务仍在跑: {task_id},转为轮询")
client.poll(f"/v1/tasks/{task_id}")
response = client.session.get(
f"{client.base_url}/v1/tasks/{task_id}/result",
params={"response_format": response_format},
headers=client.headers(),
timeout=600,
)
if response.status_code == 409:
print(f"任务失败: {response.text[:500]}")
return
if response.status_code >= 400:
raise RuntimeError(f"POST /v1/file_parse -> {response.status_code}: {response.text[:500]}")
print(f"1. 同步返回: {response.status_code}")
if response_format == "zip":
out_dir.mkdir(parents=True, exist_ok=True)
target = out_dir / f"{pdf.stem}{OUTPUT_SUFFIX['zip']}"
target.write_bytes(response.content)
print(f"2. 保存 {target} ({target.stat().st_size} 字节)")
return
save_task_content(response.json(), out_dir)
def main() -> int:
parser = argparse.ArgumentParser(description="MinerU 4.0.12 V1 API PDF 解析示例")
parser.add_argument("mode", choices=["uploads", "tasks", "sync"], help="要演示的接口组合")
parser.add_argument("pdf", type=Path, help="待解析的 PDF")
parser.add_argument("--out", type=Path, default=None, help="产物目录,默认 ./<pdf名>_<mode>")
parser.add_argument("--tier", default="flash", choices=["flash", "basic", "standard", "advanced"])
parser.add_argument("--ocr-mode", default="auto", choices=["auto", "txt", "ocr"])
parser.add_argument("--formats", default="markdown", help="逗号分隔: markdown,middle_json,structured_content,zip")
parser.add_argument("--wait-timeout", type=int, default=600, help="sync 模式等待秒数 (1-3600)")
args = parser.parse_args()
if not args.pdf.is_file():
print(f"找不到文件: {args.pdf}", file=sys.stderr)
return 1
if args.pdf.suffix.lower() != ".pdf":
print(f"这个示例只处理 PDF: {args.pdf}", file=sys.stderr)
return 1
formats = [item.strip() for item in args.formats.split(",") if item.strip()]
out_dir = args.out or Path.cwd() / f"{args.pdf.stem}_{args.mode}"
client = Client(os.environ.get("MINERU_API_URL", DEFAULT_BASE_URL), load_api_key())
if not client.api_key:
print("提示: 未找到 API key,将按匿名请求发送", file=sys.stderr)
print(f"服务 {client.base_url} | 文件 {args.pdf} | tier={args.tier} ocr_mode={args.ocr_mode} formats={formats}")
if args.mode == "uploads":
example_uploads_then_jobs(client, args.pdf, out_dir, tier=args.tier, ocr_mode=args.ocr_mode, formats=formats)
elif args.mode == "tasks":
example_tasks(client, args.pdf, out_dir, tier=args.tier, ocr_mode=args.ocr_mode, formats=formats)
else:
example_file_parse(
client,
args.pdf,
out_dir,
tier=args.tier,
ocr_mode=args.ocr_mode,
formats=formats,
wait_timeout=args.wait_timeout,
)
print(f"完成,产物目录: {out_dir}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
View Code
示例中包含三种调用模式,基本上覆盖了所有API接口:
# A. sync:同步包装,一次调用等结果(最省事)
python3 /root/zww/minerU/mineru_api_examples.py sync doc.pdf --out out/sync --tier flash
# B. tasks:异步便捷接口,multipart 直传,结果内联
python3 /root/zww/minerU/mineru_api_examples.py tasks doc.pdf --out out/tasks --tier standard --formats markdown,middle_json
# C. uploads:标准链路(uploads → parse/jobs → files),支持 sha256 秒传
python3 /root/zww/minerU/mineru_api_examples.py uploads doc.pdf --out out/uploads --tier advanced --formats markdown,middle_json,zip
方案A调用的是/v1/file_parse接口,同步包装共享异步任务,请求中包含超时时间,等待超时不取消后台工作,需要根据接口返回的task_id继续通过接口/v1/tasks/{task_id}异步轮询执行结果,轮询完成后可以通过/v1/tasks/{task_id}/result获取结果,根据status_code来判断是否解析成功,如果成功则下载相应的解析结果。
方案B调用/v1/tasks接口post上传待解析的pdf文件后,没有同步等待过程,直接进入到异步轮询状态,之后过程和方案A类似。
方案C是标准链路,先通过/v1/uploads接口POST上传会话(包括文件名、文件长度等信息),之后通过PUT + complete真正完成文件上传,接下来通过接口/v1/parse/jobs请求对上传的文件进行解析,并通过返回的job_id轮询任务状态,结束后通过/v1/files/{file_id}/content获取并保存解析结果文件。
以方案C为例,通过命令:
python3 /root/zww/minerU/mineru_api_examples.py uploads 2025_yanqing_gaoyi_wuli.pdf --out out/uploads --tier advanced --formats markdown,middle_json,structured_content,zip
对pdf文件进行解析,结果保存到out/uploads目录,解析级别是advanced,解析结果要有单独markdown,middle_json和structured_content文件,以及完整的解析结果压缩包,程序执行流程如下:


服务 http://127.0.0.1:8010 | 文件 2025_yanqing_gaoyi_wuli.pdf | tier=advanced ocr_mode=auto formats=['markdown', 'middle_json', 'structured_content', 'zip']
1. upload upload_8fe79e7acb250246200a87eb status=pending
{'id': 'upload_8fe79e7acb250246200a87eb', 'object': 'upload', 'bytes': 775923, 'created_at': 1793608607, 'expires_at': 1793612207, 'filename': '2025_yanqing_gaoyi_wuli.pdf', 'purpose': 'parse', 'mime_type': 'application/pdf', 'sha256sum': 'b32012b7d28a69f63e7ce6301bff8c5bb5dbcdd229cd50f1447fab43cf5836ae', 'status': 'pending', 'upload_url': 'http://127.0.0.1:8010/v1/uploads/upload_8fe79e7acb250246200a87eb/content', 'upload_method': 'PUT', 'upload_headers': {'Content-Type': 'application/pdf'}, 'file': None}
PUT http://127.0.0.1:8010/v1/uploads/upload_8fe79e7acb250246200a87eb/content
2. PUT bytes 775923
3. complete status=completed
file_id=file-034d4dc44d9ae04613f04817
4. job job_4e82a4d4b3e41a86d0cd0261 status=queued
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: running
状态: completed
5. job 结束: completed
保存 out/uploads/2025_yanqing_gaoyi_wuli.md (533589 字节)
保存 out/uploads/2025_yanqing_gaoyi_wuli.middle.json (61833 字节)
保存 out/uploads/2025_yanqing_gaoyi_wuli.structured_content.json (854525 字节)
保存 out/uploads/2025_yanqing_gaoyi_wuli.zip (1114170 字节)
完成,产物目录: out/uploads
View Code
在步骤1中,接口/v1/uploads给出upload'id'为upload_8fe79e7acb250246200a87eb对上传解析的文件进行追踪,http://127.0.0.1:8010/v1/uploads/upload_8fe79e7acb250246200a87eb/content是服务给出的完整上传路径;步骤2直接使用put向该路径上传文件二进制数据;之后步骤3获取上传状态是已完成;步骤4用POST提交解析请求后,服务端会返回job_id;步骤5通过job_id轮询任务执行状态;步骤6通过file_id依次将请求文件下载到本地保存。
2.4 解析结果详解
(1)md文件
markdown格式文件,该文件并非简单的文本堆砌,而是专门针对大语言模型(LLM)训练/微调、知识库(RAG)切片以及高保真渲染深度优化后的结构化文本,其核心特性与内容构成如下:
- 结构化标题与逻辑阅读顺序 (Reading Order)
多级标题映射:MinerU会基于版面分析模型(LayoutLM / YOLO等)识别字号、粗细和段落层级,自动映射为标准的#、##、###等Markdown 标题。
双栏/多栏与浮动块重排:传统工具直接转文本常出现"左右两栏混读"的错乱,MinerU会在后台按照人类自然阅读逻辑重构段落流,输出到.md时已经是严格的上下自然流式顺序。
- 数学公式的高精度LaTeX化
这是MinerU的一大核心优势:
行内公式:被识别为文本内嵌的公式格式。
独立块级公式:被转换为居中的公式代码块。
复杂矩阵与符号:通过专门训练的公式识别模型(MFR)直接反编译为精准的LaTeX 语法,完全保留积分、求和、矩阵和上下标,方便 RAG 系统直接向大模型提问公式细节。
- 表格的高保真重构
HTML Table / Markdown Table格式:对于简单的表格,输出为标准的管道符表格(| col1 | col2 |);对于包含合并单元格(跨行跨列 colspan/rowspan)的复杂三线表或财报表格,通常会以标准的HTML <table>标签输出,避免出现内容串列或格式崩塌。
表头与表注关联:表格上方通常会附带捕获的表格标题(如Table 1: ...),方便做检索时建立完整的元数据上下文。
- 图片、插图与图注的规范引用
本地相对路径引用:正文中的插图会保存到同级的 images目录下,在 .md 文件中使用标准的相对路径引用:
!\[\](images/xxxx_hash.jpg)
图注(Figure Caption)绑定:图片下方的图注(如 Figure 2: Workflow of ...)会被紧贴在图片下方作为独立段落输出,便于多模态模型或知识库切片时将"图片与其描述文字"保持在同一个 chunk 内。
- 杂质剔除与正文净化
自动清洗页眉与页脚:每页重复出现的文档名、作者名、页码(Header & Footer)会在版面分析阶段被标注为边缘噪点并直接丢弃,不会像原生 pdftotext 那样每隔几十行就插进一段"Page 12"、"Journal of AI..."。
断词重连(De-hyphenation):英文排版中行末经常出现的断字连字符(如differ- 换行 ent),在输出的 .md 中会被自动还原为完整的单词 different,防止切词分词时破坏语义。
(2)middle.json文件
middle.json是MinerU解析管线中的核心骨架文件。如果说.md是最终给人或大模型阅读的"成品",那_middle.json就是模型推理结束后、格式化拼接成Markdown之前保留的全量版面结构底稿。
它详细记录了每一页的几何坐标、版面分析划分的区块、OCR 识别的原始文本行以及模型对公式和表格的中间解析状态, 下图是结构化中间数据JSON文件的全局骨架

- metadata(元数据)
这部分记录了输入文档的基础属性和处理该文档的工具信息。
- extensions(模型扩展推断)
记录了本次提取使用的MinerU引擎配置和深度学习版面模型(DocVortex)的具体输出,如果展开pages节点将会看到原始pdf包含的页数及PDF规范中的标准绝对物理长度单位width_pt和height_pt等内容,在本次解析实例中width_pt=595.276,height_pt=841.89,1pt=1/72 inch = 1/72*25.4 mm ≈ 0.3527mm,所以width_pt≈210 mm,height_pt ≈ 297 mm,对应标准 A4 纸的长宽尺寸。

- pages(物理页面解析详情)
逐页的细粒度内容与结构树(核心数据),这是用于最终渲染Markdown的最关键数组,数组中的每一个元素代表PDF中的一页,每个页面内部展开后包含blocks,对应该页中包含的完整内容块列表,展开后里面就是具体划分出的title(标题)、text(自然段)、table(表格)、image(图片)等。

每个 block 内部会包含其对应的绝对坐标 bbox: x0, y0, x1, y1、以及之后的content信息,构成整篇文档自然阅读顺序的基本单元,其中bbox中的x,y都是根据宽高归一化到0~1内的坐标值。
(3)structured_content.json文件
如果说middle.json是记录所有原始边界坐标、底层版面分割的"生数据工程图",那么stuctured_context.json文件是经过阅读顺序重排、文本净化、公式反编译和表格规整后的高纯度语义结构树,即后者在前者的基础上进行了智能化处理。
(4)zip文件
包含以上所有3种文件,并且压缩包中还包含images目录,里面是所有的切片插图文件,所以理论上要求zip后就不需要请求其他格式的文件输出。