从 PDF 中精准提取表格、图片与公式:MinerU 结构化元素抽取的 3 种方案

为什么 PDF 元素提取比纯文本难

PDF 是一种视觉格式,不是逻辑格式。PDF 文件的本质是一组绘图指令------把文字放在哪、画多粗的线、用什么字体渲染------而非像 HTML 或 Markdown 那样告诉你"这是一个表格标题"或"这是一个三级公式"。当你用传统 PDF 解析工具提取文本时,得到的通常是一段按阅读顺序拼接的纯文本,表格结构、图片位置、公式内容全部丢失。

传统 OCR 的困境就在这里。它能把扫描件里的像素转成字符,但它不知道哪些字符属于同一列、哪些被包在表格单元格里、哪个公式的分数线跨越三行。这种结构性损失导致文本提取后,你需要人工重排数据、重新关联图注与图片、手动修复公式的 LaTeX 表示。这是目前 PDF解析、表格提取、公式识别、图片提取等任务在实际工程中反复踩坑的根本原因。

MinerU 的云端 SDK/API 把这一层逻辑封装进了解析管线。它不再只输出纯文本,而是返回一个 JSON 层级结构------告诉你每个元素的类型(表格、图片、公式、文本块)、坐标(bbox)、阅读顺序以及结构化正文(表格的 HTML、公式的 LaTeX)。你可以直接通过 result.content_list 或下载完整的 zip 包来获取这些结构化数据。

本地部署用户对应的输出文件为 content_list.jsonmiddle.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 表格字符串,可以直接渲染或存入数据库。equationtext 字段包含 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.jsonpara_blocksimage 类型的块会包含直接引用,而在 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_listpage_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_listbbox 用于排序。
  • 需要像素级坐标 → 方案二。page_size 配合 bbox 精确还原位置。
  • 需要旋转元素或页眉页脚 → 方案二 + VLM 后端。
  • 生产级全量提取 → 实战方案,组合 content_listsave_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 控制、坐标精度和代码复杂度的需求,从上述三种方案中选择最匹配的一种。

从 PDF 中提取结构化数据不再意味着只能拿到一段破碎的纯文本。通过版面分析还原文档的逻辑结构,再逐元素提取表格、公式和图片------这种基于结构化元素抽取的路径,使 PDF 的结构化数据可用于下游流水线和自动化处理。

相关推荐
sali-tec1 小时前
C# 基于OpenCv的视觉工作流-章63-点廓距离
图像处理·人工智能·opencv·计算机视觉
Maiko Star2 小时前
让 AI 开口说话:Spring AI Alibaba 语音合成(TTS)实战
java·人工智能·spring·springai
机器学习之心2 小时前
多工况车速数据集训练LSTM-Attention用于车速预测,输出未来多个时间步车速,MATLAB代码
人工智能·matlab·lstm·lstm-attention·车速预测
耀耀切克闹灬2 小时前
初识LlamaIndex (了解LlamaIndex 高层概念)
人工智能
机器之心2 小时前
马斯克官宣xAI解散,22万张GPU算力租给Anthropic
人工智能·openai
机器之心2 小时前
DeepMind入股硬核网游EVE,要让AI学「黑暗森林」
人工智能·openai
机器之心2 小时前
TRAE SOLO移动端上线,手机也能干活了,随时随地Vibe Working
人工智能·openai
2601_956139422 小时前
文体娱媒品牌全案公司哪家强
大数据·人工智能·python
薛定猫AI2 小时前
【深度解析】从 Chatbot 到 AI 数字队友:Claude 高阶能力、模型选型与 API 实战
人工智能