为什么 PDF 元素提取比纯文本难
PDF 是一种视觉格式,不是逻辑格式。PDF 文件的本质是一组绘图指令------把文字放在哪、画多粗的线、用什么字体渲染------而非像 HTML 或 Markdown 那样告诉你"这是一个表格标题"或"这是一个三级公式"。当你用传统 PDF 解析工具提取文本时,得到的通常是一段按阅读顺序拼接的纯文本,表格结构、图片位置、公式内容全部丢失。
传统 OCR 的困境就在这里。它能把扫描件里的像素转成字符,但它不知道哪些字符属于同一列、哪些被包在表格单元格里、哪个公式的分数线跨越三行。这种结构性损失导致文本提取后,你需要人工重排数据、重新关联图注与图片、手动修复公式的 LaTeX 表示。这是目前 PDF解析、表格提取、公式识别、图片提取等任务在实际工程中反复踩坑的根本原因。
MinerU 的云端 SDK/API 把这一层逻辑封装进了解析管线。它不再只输出纯文本,而是返回一个 JSON 层级结构------告诉你每个元素的类型(表格、图片、公式、文本块)、坐标(bbox)、阅读顺序以及结构化正文(表格的 HTML、公式的 LaTeX)。你可以直接通过 result.content_list 或下载完整的 zip 包来获取这些结构化数据。
本地部署用户对应的输出文件为 content_list.json 和 middle.json,云端 SDK 在此基础上封装了更直接的方法调用。下面展开三种从 PDF 中提取结构化元素的方案,从零代码感知的 content_list 到支持像素级定位的 layout.json,再到一个完整的端到端实战。
方案一:result.content_list 快速提取(零代码感知的结构化数据)
MinerU SDK 的 ExtractResult 对象暴露了一个 content_list 字段------它对应本地部署的 content_list.json,但已经过 SDK 解析为可直接遍历的 Python 列表。每个元素通过 type 字段告诉你它是什么:table 附带完整的 HTML 表格正文,equation 附带 LaTeX 公式字符串,text 携带纯文本和层级信息。
基础用法
SDK 使用流程分为三步:初始化客户端、调用解析接口、遍历结果。
python
from mineru import MinerU
client = MinerU("your-api-token")
result = client.extract("https://cdn-mineru.openxlab.org.cn/demo/example.pdf")
for item in result.content_list:
print(f"[{item['type']}] page {item['page_idx']}")
result.content_list 是一个 Python 列表,每个元素是一个字典。你不需要手动解析 JSON 文件或处理文件路径------SDK 已经将云端返回的结果包解析成了可直接消费的数据结构。对于 Agent 轻量解析场景,使用 flash_extract() 甚至不需要 Token:
python
client = MinerU() # 不传 token,进入 flash-only mode
result = client.flash_extract("https://example.com/paper.pdf")
# content_list 同样可用,但输出仅为 Markdown 级别
按类型过滤
content_list 中每种类型携带不同的字段。以下代码分别提取表格、公式和文本块:
python
# 提取所有表格
tables = [item for item in result.content_list if item["type"] == "table"]
for tbl in tables:
print(f" Caption: {tbl.get('table_caption', [''])[0]}")
print(f" HTML body: {tbl['table_body'][:200]}...")
print(f" Image path: {tbl['img_path']}")
print(f" Bbox: {tbl['bbox']}")
# 提取所有公式
equations = [item for item in result.content_list if item["type"] == "equation"]
for eq in equations:
print(f" LaTeX: {eq['text'][:150]}...")
print(f" Format: {eq.get('text_format', 'unknown')}")
# 提取所有文本块(含标题层级)
texts = [item for item in result.content_list if item["type"] == "text"]
for txt in texts:
level = txt.get("text_level", 0)
prefix = " " * level + f"[H{level}]" if level else " [body]"
print(f"{prefix} {txt['text'][:100]}")
三种核心类型的字段差异
| 字段 | table |
equation |
text |
|---|---|---|---|
type |
"table" |
"equation" |
"text" |
text |
--- | LaTeX 公式字符串 | 纯文本/标题 |
text_level |
--- | --- | 0=正文, 1=一级标题, 2=二级标题... |
table_body |
HTML <table> 字符串 |
--- | --- |
table_caption |
字符串数组(可能为空) | --- | --- |
table_footnote |
字符串数组(可能为空) | --- | --- |
img_path |
图片文件名 | 图片文件名 | --- |
text_format |
--- | "latex" |
--- |
bbox |
[x0, y0, x1, y1] | [x0, y0, x1, y1] | [x0, y0, x1, y1] |
page_idx |
页码 | 页码 | 页码 |
实际返回中 table_body 是一个 HTML 表格字符串,可以直接渲染或存入数据库。equation 的 text 字段包含 LaTeX 表达式,如 $$\\frac{d}{dx}\\int_{a}^{x} f(t) dt = f(x)$$,配合 text_format: "latex" 标识。text 中的 text_level 字段让你区分正文和标题层级------这在构建文档树时很有用。
有个需要注意的细节:SDK 的 content_list 中没有独立的 image 类型 (本地部署的 content_list.json 则包含 image 类型及 image_caption 字段,SDK 层做了语义归并)。视觉元素(图、表、公式)要么以 table/equation 类型出现并附带 img_path,要么在 layout.json 层才暴露为独立的图片块。换句话说,SDK 层做了语义归并------纯装饰性图片或无法归类的插图会被过滤或合并到相邻文本块中。
实用场景举例
- 构建表格数据集 :过滤
type == "table",将table_body的 HTML 解析为结构化行/列数据,用于训练表格理解模型。 - 公式检索系统 :提取
type == "equation"的text字段(LaTeX),结合page_idx建立公式-文档索引。 - Markdown 文档还原 :将
text块按text_level组织目录树,配合bbox排序,还原出保留层级和阅读顺序的文档。
方案一适用于不需要像素级坐标的场景。如果你的流水线只需要"这个 PDF 里有哪些公式、它们的 LaTeX 是什么"或者"第 3 页的表格 HTML 是什么",content_list 是最直接的入口,没有之一。
方案二:save_all() 获取完整 zip,读取 layout.json 做精准定位
SDK 直接暴露的 content_list 做了大量简化------它按阅读顺序平铺了可读内容,但丢弃了不少底层信息:页面尺寸、旋转角度、被丢弃的页眉页脚、图表的细粒度子块(body/caption/footnote 分离)。这些信息在 content_list 层不可见。
MinerU SDK 不直接暴露 middle.json 层级的数据。你需要通过 result.save_all(dir) 下载完整的解析结果 zip 包,再从 zip 中读取 layout.json------它对应本地部署的 middle.json。API 返回结果中的 full_zip_url 字段提供了 zip 包的远程地址,save_all() 内部基于此 URL 下载。
获取并读取 layout.json
python
from mineru import MinerU
import zipfile
import json
client = MinerU("your-api-token")
result = client.extract("https://cdn-mineru.openxlab.org.cn/demo/example.pdf")
# 下载完整 zip 包到本地目录
result.save_all("./output_zip/")
# 从 zip 中读取 layout.json
zip_path = "./output_zip/result.zip" # save_all 实际生成的文件名
with zipfile.ZipFile(zip_path, "r") as zf:
with zf.open("layout.json") as f:
layout = json.load(f)
# 检查后端类型
print(f"Backend: {layout['_backend']}") # "pipeline" 或 "vlm"
# 遍历每页
for page in layout["pdf_info"]:
page_idx = page["page_idx"]
page_size = page["page_size"] # [width, height]
print(f"\n--- Page {page_idx} ({page_size[0]}x{page_size[1]}) ---")
# para_blocks 包含主要内容块
for block in page["para_blocks"]:
btype = block["type"]
bbox = block["bbox"] # [x0, y0, x1, y1]
angle = block.get("angle", 0)
print(f" [{btype}] bbox={bbox}, angle={angle}°")
# discarded_blocks 包含页眉/页脚/页码等
for dblock in page.get("discarded_blocks", []):
print(f" [discarded:{dblock['type']}] {dblock['bbox']}")
bbox 坐标系统
content_list(即 SDK 的 result.content_list)的 bbox 坐标采用 0-1000 归一化映射 :[x0, y0, x1, y1] 四个整数均在 0 到 1000 之间,分别对应页面左上角到右下角的相对位置。无论原始 PDF 页面是 A4 还是 A3,坐标都统一映射到这个范围,方便不同页面尺寸之间的坐标比较和渲染。
layout.json(即本地部署的 middle.json)的坐标系统因后端而异。在 pipeline 后端 下,bbox 使用原始像素值 ,需要配合 page_size 字段换算比例;在 VLM 后端 下,layout.json 仍为 0-1000 归一化,而同一后端的 model.json 切换为 0-1 浮点数百分比格式。
pipeline 与 VLM 后端的字段差异
layout.json 的顶层包含 _backend 字段,标识解析模式:
| 维度 | pipeline 后端 | VLM 后端 |
|---|---|---|
_backend 值 |
"pipeline" |
"vlm" |
para_blocks 块类型 |
text, title, table, image, interline_equation |
同上,额外支持 code, list, algorithm |
| discarded_blocks | 有限类型 | 完整输出 header, footer, page_number, aside_text, page_footnote |
| 旋转角度 | 无 angle 字段 |
每个 block 有 angle 字段 (0/90/180/270) |
| bbox 坐标 | content_list 0-1000 归一化;layout.json 使用原始像素值 |
layout.json 0-1000 归一化;model.json 0-1 百分比 |
如果你需要处理有旋转内容的页面(如扫描件中倾斜的表格),VLM 后端的 angle 字段提供了必要的校正信息。如果你只需要标准阅读顺序的结构化数据,pipeline 后端的输出更简洁。
从 zip 中读取 images
python
with zipfile.ZipFile(zip_path, "r") as zf:
# 列出所有图片文件
img_files = [f for f in zf.namelist() if f.startswith("images/")]
for img_name in img_files:
zf.extract(img_name, "./extracted_images/")
layout.json 的 para_blocks 中 image 类型的块会包含直接引用,而在 SDK content_list 层这些图片可能被合并到相邻表格或公式中。方案二适用于需要对图片、表格、公式做像素级精确对应的场景------比如将表格 HTML 渲染后与原 PDF 截图做视觉对比,或者在自定义 UI 中按原始位置覆盖渲染提取出的元素。
实战:从一篇学术论文中批量提取表格 + 公式 + 图片
以下代码展示了一个完整的端到端流程:输入一篇学术论文 PDF,通过 MinerU SDK 解析后,从 content_list 中遍历所有元素,将表格保存为独立 HTML 文件,公式保存为 LaTeX 文件,图片保存为本地文件。
python
from mineru import MinerU
import json
import os
def extract_elements(pdf_url: str, output_dir: str, token: str):
os.makedirs(output_dir, exist_ok=True)
client = MinerU(token)
result = client.extract(pdf_url, model="vlm")
tables, equations, text_blocks = [], [], []
for item in result.content_list:
t = item["type"]
page = item["page_idx"]
if t == "table":
html = item.get("table_body", "")
caption = "".join(item.get("table_caption", []))
path = os.path.join(output_dir, f"table_p{page}_{len(tables)}.html")
with open(path, "w", encoding="utf-8") as f:
f.write(f"<!-- {caption} -->\n{html}")
tables.append({"page": page, "html_path": path, "caption": caption})
elif t == "equation":
latex = item.get("text", "")
path = os.path.join(output_dir, f"eq_p{page}_{len(equations)}.tex")
with open(path, "w", encoding="utf-8") as f:
f.write(latex)
equations.append({"page": page, "latex_path": path})
elif t == "text" and item.get("text_level", 0) > 0:
text_blocks.append({
"page": page,
"level": item["text_level"],
"text": item["text"]
})
# 保存图片(从 zip 中提取)
result.save_all(output_dir)
# 输出汇总
report = {
"total_tables": len(tables),
"total_equations": len(equations),
"total_headings": len(text_blocks),
"tables": tables,
"equations": equations,
"headings": text_blocks
}
with open(os.path.join(output_dir, "extract_report.json"), "w") as f:
json.dump(report, f, indent=2, ensure_ascii=False)
print(f"提取完成:{len(tables)} 个表格,{len(equations)} 个公式,{len(text_blocks)} 个标题")
return report
report = extract_elements(
pdf_url="https://cdn-mineru.openxlab.org.cn/demo/example.pdf",
output_dir="./paper_extract",
token="your-api-token"
)
这段代码覆盖了典型的数据工程场景:输入一篇学术论文 PDF,输出保存为结构化文件。content_list 的 page_idx 字段使你可以按页码组织提取结果,text_level 让标题树的重建变得可直接用。
处理结果的文件结构大致如下:
bash
paper_extract/
├── extract_report.json # 提取结果索引
├── table_p0_0.html # 第 0 页第一个表格
├── table_p3_1.html # 第 3 页第二个表格
├── eq_p1_0.tex # 第 1 页第一个公式
├── eq_p2_0.tex # 第 2 页第二个公式
├── result.zip # save_all() 下载的完整包
├── images/ # 从 zip 中解压的图片
│ ├── a8ecda1c69b27e4f.jpg
│ └── 181ea56ef185060d.jpg
└── full.md # Markdown 全文输出
提取出的表格 HTML 可以直接在浏览器中渲染查看,公式 LaTeX 可以用 MathJax 或 LaTeX 编译器编译,图片则保存在 images/ 目录下。extract_report.json 提供了完整的索引,方便下游流水线按需加载。
需要注意的是,content_list 中的 img_path 指向的是 zip 包 images/ 目录内的文件名,而非完整 URL 或绝对路径。如果你需要通过 save_all() 以外的途径独立获取图片资源,可以从 zip 包的 images/ 路径直接读取。
输入输出对应关系
| 原始 PDF 内容 | content_list 类型 |
输出文件 |
|---|---|---|
| 第 2 页的统计表格 | type: "table" → table_body (HTML) |
table_p2_0.html |
| 第 5 页的多行公式 | type: "equation" → text (LaTeX) |
eq_p5_0.tex |
| 第 1 页的示意图 | type 无独立 image → 通过 save_all() 的 images/ 目录获取 |
images/*.jpg |
| 正文段落 | type: "text" + text_level: 0 |
汇总到 extract_report.json |
表格和公式在 content_list 层就已经是结构化状态,无需额外解析。图片则需要通过 zip 包获取------这是 content_list 层做了语义归并的代价。
三种方案选型对比
| 维度 | 方案一:content_list |
方案二:save_all() + layout.json |
实战组合方案 |
|---|---|---|---|
| 适用场景 | 快速原型、自动化流水线、仅需结构化数据 | 像素级定位、自定义渲染、需访问 discarded_blocks | 生产级全量提取 |
| 是否需要 Token | 需申请 | 需申请 | 需申请 |
| 坐标精度 | 0-1000 归一化 bbox | bbox + page_size + angle | 同方案一 |
| 代码复杂度 | 低(1 行属性访问) | 中(下载 zip + 解析 JSON) | 中 |
| 二次开发灵活度 | 中等(字段预定义) | 高(访问完整 middle 结构) | 高 |
| 后端差异暴露 | 无 | _backend 字段明示 |
可根据需要切换 |
| 图片提取方式 | 通过 img_path 间接获取 |
从 zip 的 images/ 目录提取 |
通过 save_all() 获取 |
如果你的场景只需要表格 HTML 和公式 LaTeX,方案一够用。如果需要像素级坐标或页眉页脚等辅助信息,方案二更适合。实战方案面向完整数据管线------从 PDF 到结构化文件系统。
选型决策流程
- 结构化数据即可 → 方案一。一行
result.content_list,bbox用于排序。 - 需要像素级坐标 → 方案二。
page_size配合bbox精确还原位置。 - 需要旋转元素或页眉页脚 → 方案二 + VLM 后端。
- 生产级全量提取 → 实战方案,组合
content_list和save_all()。
方案一和方案二可以配合使用:先用 content_list 过滤 table 获取 HTML,再通过 save_all() 下载 zip 读取 layout.json 获取精确坐标。
结尾与关键词收口
MinerU 覆盖了从 PDF 元素提取到结构化输出的链路。无论是表格提取、公式提取还是图片提取,其云端 SDK/API 都提供了分层的访问接口------从零代码感知的 content_list 到完整像素级控制的 layout.json。根据你的工程需求选择合适的层级,可以直接在数据流水线中消费这些结构化输出。
在技术实现上,MinerU 2.5-Pro 在元素级解析上达到了文本 Edit Distance 0.019、公式 CDM 97.29、表格 TEDS 91.10 的表现------这些数据来自其技术报告的独立评估。如果你需要在自己的项目中进行 PDF 解析、表格识别、公式提取或版面还原,可以结合自身对 Token 控制、坐标精度和代码复杂度的需求,从上述三种方案中选择最匹配的一种。
- 开源仓库:github.com/opendatalab...
- 官方网站:mineru.net
从 PDF 中提取结构化数据不再意味着只能拿到一段破碎的纯文本。通过版面分析还原文档的逻辑结构,再逐元素提取表格、公式和图片------这种基于结构化元素抽取的路径,使 PDF 的结构化数据可用于下游流水线和自动化处理。