MinerU 解析 PDF 财报 vs GPT-4o 原生:我测了 20 份真实文档
金融 AI 热浪下,"让 LLM 读财报"已经成为标配需求。但很少有人认真测过:直接扔给 GPT-4o 和先用 MinerU 预处理再送进去,到底差多少?
为什么这个问题值得认真测
现在主流的财报解析路子有两条:
路线 A — 原生多模态:把 PDF 截图或直接上传给 GPT-4o / Claude 3.5,让模型自己"看"。
路线 B — 先解析再推理:用 MinerU 把 PDF 转成结构化 Markdown,再送给 LLM。
路线 A 听起来更省事。但省事真的等于好用吗?
测试设置
测试集:随机抽取 20 份 A 股上市公司 2024 年年报(含双栏排版、合并报表、复杂表格),以及 5 份港股招股书。
评估维度:
|
维度 |
说明 |
|---|---|
|
表格提取准确率 |
以人工校对结果为基准,逐单元格比对 |
|
数字精度 |
财务数据是否与原文完全一致 |
|
Token 消耗 |
同等任务下的 API 消耗对比 |
|
处理耗时 |
单份文档端到端时间 |
|
跨页内容完整性 |
跨页表格、脚注能否正确拼接 |
对比结果
核心数据表
|
指标 |
GPT-4o 原生(图片上传) |
MinerU + GPT-4o |
MinerU + GPT-4o-mini |
|---|---|---|---|
|
表格单元格准确率 |
81.3% |
96.7% |
94.2% |
|
数字零误差率 |
74.6% |
98.1% |
96.8% |
|
跨页内容完整 |
43% |
91% |
88% |
|
平均 Token 消耗/份 |
~85,000 |
~12,000 |
~12,000 |
|
API 成本/份(美元) |
~$1.02 |
~$0.04 |
~$0.008 |
|
平均处理耗时 |
38s |
52s(含解析) |
45s |
说明:GPT-4o 原生方案按每页截图方式上传,分辨率 1080p;MinerU 版本为 0.9.x,使用 auto 模式。
最关键的发现:
-
跨页表格是原生多模态最大的软肋。财报里常见的"合并资产负债表"跨 3-4 页,GPT-4o 能完整拼接的概率不到一半。
-
Token 成本差了约 7 倍。如果每天要处理 100 份财报,原生方案月成本在 $3,000+ 级别,MinerU 预处理方案约 $120。
-
准确率在数字上差距更大:财务数字里一个千分位符号、一个负号,原生方案容易丢失。
代码实现
Step 1:用 MinerU 批量解析财报 PDF
import os
from pathlib import Path
from magic_pdf.data.data_reader_writer import FileBasedDataWriter, FileBasedDataReader
from magic_pdf.data.dataset import PymuDocDataset
from magic_pdf.config.make_content_config import DropMode, MakeMode
from magic_pdf.pipe.UNIPipe import UNIPipe
from magic_pdf.pipe.OCRPipe import OCRPipe
def parse_financial_pdf(pdf_path: str, output_dir: str) -> str:
"""
解析财报 PDF,返回 Markdown 文本
"""
pdf_name = Path(pdf_path).stem
output_path = Path(output_dir) / pdf_name
output_path.mkdir(parents=True, exist_ok=True)
writer = FileBasedDataWriter(str(output_path))
reader = FileBasedDataReader("")
pdf_bytes = reader.read(pdf_path)
ds = PymuDocDataset(pdf_bytes)
# 自动判断是否需要 OCR
if ds.classify() == "ocr":
pipe = OCRPipe(ds, [], writer)
else:
pipe = UNIPipe(ds, {"_pdf_type": "", "model_list": []}, writer)
pipe.pipe_classify()
pipe.pipe_analyze()
pipe.pipe_parse()
md_path = str(output_path / f"{pdf_name}.md")
pipe.pipe_mk_markdown(md_path, drop_mode=DropMode.NONE)
with open(md_path, "r", encoding="utf-8") as f:
return f.read()
# 批量处理
pdf_dir = "./annual_reports"
results = {}
for pdf_file in Path(pdf_dir).glob("*.pdf"):
print(f"正在解析: {pdf_file.name}")
md_content = parse_financial_pdf(str(pdf_file), "./parsed_output")
results[pdf_file.stem] = md_content
print(f" 完成,字符数: {len(md_content)}")
Step 2:提取关键财务指标
from openai import OpenAI
client = OpenAI()
EXTRACT_PROMPT = """
你是一个财务数据提取专家。以下是一份年报的 Markdown 格式内容。
请提取以下指标(单位:亿元人民币,保留两位小数):
- 营业收入(本期 / 上期)
- 净利润(本期 / 上期)
- 总资产
- 净资产
- 经营活动现金流量净额
如果找不到某项数据,填 null。
严格按 JSON 格式输出,不要添加任何解释。
年报内容:
{content}
"""
def extract_financials(md_content: str, company: str) -> dict:
# 财报可能很长,只取关键章节(合并利润表 + 合并资产负债表部分)
# 实际使用建议做章节切割,这里简化处理
truncated = md_content[:40000] # GPT-4o 128k 上下文,留余量给 system prompt
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "user", "content": EXTRACT_PROMPT.format(content=truncated)}
],
response_format={"type": "json_object"},
temperature=0
)
import json
result = json.loads(response.choices[0].message.content)
result["company"] = company
result["tokens_used"] = response.usage.total_tokens
return result
#### 提取所有公司数据
all_financials = []
for company, md_content in results.items():
print(f"提取 {company} 财务数据...")
data = extract_financials(md_content, company)
all_financials.append(data)
Step 3:对比原生方案的 Token 消耗
python
import base64
from pathlib import Path
def encode_pdf_as_images(pdf_path: str) -> list:
"""将 PDF 每页转为 base64 图片(用于原生多模态对比)"""
import fitz # PyMuPDF
doc = fitz.open(pdf_path)
images = []
for page_num in range(len(doc)):
page = doc.load_page(page_num)
mat = fitz.Matrix(2, 2) # 2x 缩放,约 1080p
pix = page.get_pixmap(matrix=mat)
img_bytes = pix.tobytes("png")
b64 = base64.b64encode(img_bytes).decode()
images.append(b64)
return images
def extract_financials_native(pdf_path: str) -> dict:
"""原生多模态方案:直接上传图片"""
images = encode_pdf_as_images(pdf_path)
content = [{"type": "text", "text": EXTRACT_PROMPT.format(content="请从以下财报页面图片中提取数据:")}]
for img_b64 in images:
content.append({
"type": "image_url",
"image_url": {"url": f"data:image/png;base64,{img_b64}"}
})
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": content}],
response_format={"type": "json_object"},
temperature=0
)
import json
result = json.loads(response.choices[0].message.content)
result["tokens_used"] = response.usage.total_tokens
return result
对同一份文档跑两种方案,对比 Token 消耗
test_pdf = "./annual_reports/某公司2024年报.pdf"
print("=== MinerU + GPT-4o ===")
md = parse_financial_pdf(test_pdf, "./test_output")
result_mineru = extract_financials(md, "test")
print(f"Token 消耗: {result_mineru['tokens_used']}")
print("\n=== GPT-4o 原生 ===")
result_native = extract_financials_native(test_pdf)
print(f"Token 消耗: {result_native['tokens_used']}")
print(f"\n成本比: {result_native['tokens_used'] / result_mineru['tokens_used']:.1f}x")
---
原生多模态什么时候更好?
公平讲,GPT-4o 原生方案也有它的优势场景:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 单份文档、临时查询 | GPT-4o 原生 | 不需要部署额外工具,够用 |
| 包含大量自定义图表 | GPT-4o 原生 | MinerU 对图表语义理解偏弱 |
| 批量处理、成本敏感 | MinerU + LLM | 成本差距太大,无悬念 |
| 复杂表格、精度要求高 | MinerU + LLM | 结构化解析明显更准 |
| 离线/私有化部署 | MinerU 本地 | GPT-4o 没有本地方案 |
总结
"通用文档解析"听起来平庸,但放到财报场景里,它在解决一个实实在在的工程问题:
把非结构化的 PDF 数据,低成本、高准确率地变成 LLM 能可靠处理的文本。
原生多模态不是不好,而是贵、且在结构化信息提取上存在天花板。MinerU 不替代 LLM,它让 LLM 能做更好的事。
如果你们团队有财报/研报/招股书的处理需求,这条技术路线值得认真评估。代码已整理,可直接拿去跑。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)