系列四给平台升级理解能力:不再只用标题摘要这些”现成元数据”,而是读论文全文。全文在 PDF 里——一种为”打印好看”设计的格式,不是为机器读取设计的。这一课把 PDF 变成结构化数据,这是后续语义检索、图表理解一切工作的入口。
前置阅读:系列二第 1 课(arXiv 的 pdf_url 字段就是下载入口)、Pandas 数据处理。
PDF 为什么难解析
PDF 存储的是”某字符画在某坐标”,不是”这是标题、那是段落”。直接抽文本的坑:
- 阅读顺序错乱:双栏论文的文本流常常左右栏交错。
- 连字符断词:行尾的 “transformer” 被拆成 “trans-\nformer”。
- 页眉页脚混入:每页的会议名、页码混进正文。
- 公式表格变乱码:数学符号的字体编码和正文完全不同。
理解这一点就理解了解析工具的分层:简单工具抽字符流,专业工具重建文档结构。
方案对比:三档工具按需求选
| 工具 | 能力 | 适用 |
|---|---|---|
| pypdf | 抽原始文本流 | 单栏简单文档、快速验证 |
| PyMuPDF (fitz) | 文本 + 坐标 + 图片 | 需要版面信息的中等需求 |
| GROBID | 完整结构:标题/作者/摘要/章节/参考文献 | 科研论文的生产级方案 |
先用 PyMuPDF 看原始抽取的样子,理解问题:
import fitz # pip install pymupdf
doc = fitz.open("1706.03762.pdf")
page = doc[0]
# 带坐标的文本块:可以看出栏结构
for block in page.get_text("blocks"):
x0, y0, x1, y1, text, *_ = block
print(f"[{x0:.0f},{y0:.0f}] {text[:60]}")
GROBID:科研论文的生产级解析
GROBID 是用机器学习模型做 PDF 结构化的开源服务,输出标准 TEI XML——标题、作者、摘要、正文章节、参考文献条目全部结构化:
# Docker 起服务
docker run -p 8070:8070 lfoppiano/grobid:0.8.0
import requests
from lxml import etree
def parse_pdf_grobid(pdf_path: str) -> dict:
with open(pdf_path, "rb") as f:
resp = requests.post(
"http://localhost:8070/api/processFulltextDocument",
files={"input": f}, timeout=120)
resp.raise_for_status()
return tei_to_dict(resp.content)
def tei_to_dict(xml_bytes: bytes) -> dict:
ns = {"t": "http://www.tei-c.org/ns/1.0"}
root = etree.fromstring(xml_bytes)
return {
"title": root.findtext(".//t:titleStmt/t:title", namespaces=ns),
"abstract": " ".join(root.findtext(".//t:abstract", namespaces=ns, default="").split()),
"sections": [
{"head": d.findtext("t:head", namespaces=ns),
"text": " ".join("".join(d.itertext()).split())}
for d in root.findall(".//t:body/t:div", namespaces=ns)
],
"references": [
" ".join("".join(b.itertext()).split())
for b in root.findall(".//t:listBibl/t:biblStruct", namespaces=ns)
],
}
参考文献的结构化抽取是 GROBID 的隐藏宝藏:拿到每篇论文的引用列表,就能构建引用网络——平台后续的”相关论文推荐”功能直接受益。
批量处理:解析是平台的一条数据管道
全文解析接进平台数据层,和其他采集器一样要工程化:
def parse_batch(pdf_dir: Path, out_path: Path, state_path: Path):
state = load_json(state_path, default={"done": []})
pdfs = sorted(pdf_dir.glob("*.pdf"))
with open(out_path, "a", encoding="utf-8") as f:
for pdf in pdfs:
if pdf.stem in state["done"]:
continue # 断点续传
try:
record = parse_pdf_grobid(pdf)
record["arxiv_id"] = pdf.stem
f.write(json.dumps(record, ensure_ascii=False) + "\n")
state["done"].append(pdf.stem)
except Exception as e:
log_failed(pdf, e) # 失败单列,不中断批次
if len(state["done"]) % 20 == 0:
save_json(state_path, state) # 定期落状态
GROBID 单篇解析几秒到十几秒,十万篇就是几十上百小时——这条管道必然要断点续传、失败隔离、可并发(GROBID 支持多 worker)。解析产物进 data/cleaned/fulltext/ 层,与元数据集分库管理。
质量评估:解析对不对怎么知道
解析器没有报错,不代表段落顺序正确。抽样时分别查看双栏、扫描件、跨页表格和公式密集页面,并把字段回链到页码或坐标。后续检索答错时,这些定位信息能帮助区分解析错误与模型错误。
PDF 解析没有 100% 准确的工具,质量要量化:
- 字段完整率:标题/摘要/章节非空的比例。GROBID 对排版规范的论文(arXiv 主流模板)完整率通常 95%+,对老式扫描 PDF 会断崖下跌。
- 抽样人工核对:随机 30 篇对照原文,看章节边界、作者顺序是否正确。
- 失败画像:失败集中在哪类 PDF(双栏变体、扫描版、特殊模板)?扫描版 PDF 本质是图片,要走 OCR 路线(那是另一个工具栈),识别出这类直接标记跳过,别让它们拉低管道的成功率数字。
踩坑排查
| 症状 | 原因 | 处理 |
|---|---|---|
| 抽出的文本栏交错 | 双栏布局按坐标抽 | 换 GROBID 或按 x 坐标分栏重排 |
| 章节文本里全是页眉 | 未去重复页眉行 | 按行位置过滤或换 GROBID |
| GROBID 返回 503 | 并发超服务容量 | 降并发,客户端排队重试 |
| 某些 PDF 解析为空 | 扫描版(内容是图片) | 标记需 OCR,不进主流水线 |
| 参考文献解析成一大段 | biblStruct 提取路径错 | 检查 TEI 命名空间 |
| 断词导致检索失败 | 行尾连字符未处理 | 正则合并 -\n 断词 |
作品集证据
本课产出:三档解析方案的对比结论、生产级 GROBID 管道(断点续传 + 失败隔离 + 质量报告)。“我把论文 PDF 结构化成了可用的章节与引用网络数据”是系列四所有后续课的地基。
练习
- 用 PyMuPDF 和 GROBID 分别解析同一篇双栏论文,对比章节结构还原度。
- 实现批量解析管道,故意中断后验证断点续传。
- 对 100 篇解析结果算字段完整率,并给失败案例归类画像。
- 从解析结果构建引用边表(paper → references),统计被引 Top 10。
面试常问
Q:PDF 解析的技术难点本质是什么? PDF 存的是绘制指令(字符+坐标),不含语义结构。解析要从版面坐标重建逻辑结构(栏序、章节、表格),这是计算机视觉 + 版面分析的混合问题,所以 GROBID 这类用序列标注模型的方案远优于规则方案。
Q:怎么评估解析质量? 字段完整率(自动化)、抽样人工核对(准确性)、失败画像(定位系统性问题)。三个维度分别对应覆盖率、正确率、可修复性。
Q:解析管道为什么要失败隔离而不是失败即停? 十万篇里必然有几百篇坏 PDF。失败即停让少数坏数据绑架整个批次;隔离(记录、跳过、单独画像)让好数据的处理不被阻断,坏数据的修复可以另行安排。
💬 评论
这套方案在哪一步卡住了?也欢迎补充你的实现方式或踩坑经验。