MinerU 4.0.12 实战笔记:从PDF到可用的Markdown
我用MinerU 4.0.12在一台Rocky 8.10的老服务器上跑了几个月的试卷和杂志解析。MinerU是OpenDataLab开源的文档解析工具,能把 PDF、Office文档和图片转成Markdown与结构化JSON。网上能搜到的教程大多还停留在3.x,命令和输出格式都变了,照着敲会踩一串坑。这篇把真正影响使用的部分整理出来:怎么装、选哪档、图和JSON在哪、API怎么调、以及几个不会自愈的运维问题。
1 环境准备
1.1 安装
新机器上按官方文档 pip install "mineru[full]"就行。麻烦出在老系统: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 installcpp>
如还遇到问题可以借助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. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
还可以继续通过help查看下一级参数的用法:
mineru search --help Usage: mineru search [OPTIONS] {query} Search parsed document content. ╭─ Arguments ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ * querySearch query [required] │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Options ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ --type 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 Exact search index tier: flash, basic, standard, advanced │ │ --min-tier 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. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
同样可以查看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
之后运行如下命令创建软链接,使其开机自动启动:
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="产物目录,默认 ./_ ") 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())
示例中包含三种调用模式,基本上覆盖了所有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
在步骤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)切片以及高保真渲染深度优化后的结构化文本,其核心特性与内容构成如下:
1. 结构化标题与逻辑阅读顺序 (Reading Order)
多级标题映射:MinerU会基于版面分析模型(LayoutLM / YOLO等)识别字号、粗细和段落层级,自动映射为标准的#、##、###等Markdown 标题。
双栏/多栏与浮动块重排:传统工具直接转文本常出现“左右两栏混读”的错乱,MinerU会在后台按照人类自然阅读逻辑重构段落流,输出到.md时已经是严格的上下自然流式顺序。
2. 数学公式的高精度LaTeX化
这是MinerU的一大核心优势:
行内公式:被识别为文本内嵌的公式格式。
独立块级公式:被转换为居中的公式代码块。
复杂矩阵与符号:通过专门训练的公式识别模型(MFR)直接反编译为精准的LaTeX 语法,完全保留积分、求和、矩阵和上下标,方便 RAG 系统直接向大模型提问公式细节。
3. 表格的高保真重构
HTML Table / Markdown Table格式:对于简单的表格,输出为标准的管道符表格(| col1 | col2 |);对于包含合并单元格(跨行跨列 colspan/rowspan)的复杂三线表或财报表格,通常会以标准的HTML
表头与表注关联:表格上方通常会附带捕获的表格标题(如Table 1: ...),方便做检索时建立完整的元数据上下文。
4. 图片、插图与图注的规范引用
本地相对路径引用:正文中的插图会保存到同级的 images目录下,在 .md 文件中使用标准的相对路径引用:

图注(Figure Caption)绑定:图片下方的图注(如 Figure 2: Workflow of ...)会被紧贴在图片下方作为独立段落输出,便于多模态模型或知识库切片时将“图片与其描述文字”保持在同一个 chunk 内。
5. 杂质剔除与正文净化
自动清洗页眉与页脚:每页重复出现的文档名、作者名、页码(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文件的全局骨架

1. metadata(元数据)
这部分记录了输入文档的基础属性和处理该文档的工具信息。
2. 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 纸的长宽尺寸。

3. 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后就不需要请求其他格式的文件输出。
参考
1 https://github.com/opendatalab/MinerU
2 https://wnjson.com/jsonudview
原文地址: https://www.cveoy.top/t/topic/qHVE 著作权归作者所有。请勿转载和采集!