PDF标题提取
PDF 本质上是一个"打印格式",它主要关注字怎么画在纸上,而不是内容的逻辑结构。因此,PDF 内部并没有直接存储"这是第一章正文"这样的标签。
但是,我们可以利用 PDF 的 Outline (大纲/书签) 来重建这种层级结构。
最强大的工具是 PyMuPDF (又名 fitz)。它比 PyPDF2 更快,且能更方便地提取目录(TOC)和页面内容。
核心思路
获取目录 (TOC):从 PDF 提取 [层级, 标题, 页码] 列表。
构建树状结构:利用"层级"信息,将扁平的列表转换为嵌套的 JSON 树。
准备工作
安装 PyMuPDF
Bash
pip install pymupdf
完整 Python 代码实现
这个脚本会读取 PDF,提取目录结构,最后生成一个层级化的 JSON。
python
import fitz # PyMuPDF
import json
def pdf_to_title_json(pdf_path):
doc = fitz.open(pdf_path)
# 1. 获取目录 (Table of Contents)
# 格式: [[lvl, title, page, dest_dict], ...]
# lvl: 层级 (1, 2, 3...)
# page: 页码 (从1开始,pymupdf通常处理为1-based)
toc = doc.get_toc(simple=False)
if not toc:
print("该 PDF 没有目录 (Outline),无法自动提取层级结构。")
return []
structure = []
# 使用栈来维护当前的父节点路径,以便处理嵌套
# 栈中元素结构: { "node": node_dict, "level": lvl }
stack = []
for i, item in enumerate(toc):
print(item)
level, title, page_num, dest_dict = item[0], item[1], item[2], item[3]
# 创建当前节点
node = {
"title": title,
"level": level,
"start_page": page_num,
"dest_dict": dest_dict,
"children": []
}
# --- 树状结构构建逻辑 ---
if level == 1:
# 一级目录,直接加入根列表,并清空栈,重新作为父节点
structure.append(node)
stack = [{"node": node, "level": level}]
else:
# 如果是子章节,在栈中找到它的父节点 (层级比它小1的节点)
while stack and stack[-1]["level"] >= level:
stack.pop()
if stack:
parent = stack[-1]["node"]
parent["children"].append(node)
stack.append({"node": node, "level": level})
else:
# 异常情况:有子层级但没找到父层级,暂作根节点处理
structure.append(node)
stack.append({"node": node, "level": level})
return structure
# --- 使用示例 ---
pdf_file = "example.pdf"
try:
json_data = pdf_to_title_json(pdf_file)
# 保存为文件
with open("output.json", "w", encoding="utf-8") as f:
json.dump(json_data, f, indent=4, ensure_ascii=False)
print("\n提取标题完成,已保存为 output.json")
except Exception as e:
print(f"发生错误: {e}")
输出示例
json
[
{
"title": "Introduction",
"level": 1,
"start_page": 4,
"dest_dict": {
"kind": 4,
"xref": 10,
"name": "nameddest=section.1",
"zoom": 0.0
},
"children": []
},
{
"title": "Related Work",
"level": 1,
"start_page": 4,
"dest_dict": {
"kind": 4,
"xref": 32,
"name": "nameddest=section.2",
"collapse": true,
"zoom": 0.0
},
"children": [
{
"title": "Related Surveys",
"level": 2,
"start_page": 5,
"dest_dict": {
"kind": 4,
"xref": 100,
"name": "nameddest=subsection.2.1",
"zoom": 0.0
},
"children": []
},
{
"title": "Preliminary",
"level": 2,
"start_page": 6,
"dest_dict": {
"kind": 4,
"xref": 101,
"name": "nameddest=subsection.2.2",
"zoom": 0.0
},
"children": []
}
]
}
]
字段解释
-
"title": "Introduction":目录项的实际文本内容
-
"level": 1 :一级标题
示例结构:
1. 一级标题 ← 就是这个层级 1.1 二级标题 1.2 另一个二级标题 -
"start_page": 4 :这个目录项指向文档的 第 4 页
这是 1-based 的页码(即从 1 开始计数)
-
dest_dict
kind: 4表示链接类型为 命名目标(Named Destination)
在 PyMuPDF 中,链接类型常量:
pythonLINK_NONE = 0 # 无链接 LINK_GOTO = 1 # 页面跳转 LINK_URI = 2 # URI链接 LINK_LAUNCH = 3 # 启动应用程序 LINK_NAMED = 4 # 命名目标 ← 就是这个 LINK_GOTOR = 5 # 跳转到其他文档xref: 10
PDF 文档内部的 交叉引用编号,这是PDF中对象的唯一整数标识。每个PDF中都有一个交叉引用表(可能由多个单独的段组成),用于存储每个对象的相对位置,以便快速查找。交叉引用表的条目数比现有对象的数量多一个:第0项是保留的,不得以任何方式使用。许多PyMuPDF类都有一个xref属性(对于非PDF文件,该属性值为0),可以通过Document.xref_length() - 1来获取PDF中对象的总数。
name: 'nameddest=section.1'
命名目标的具体名称
zoom: 0.0
缩放级别,0.0 通常表示 使用默认缩放
参考
https://pymupdf.readthedocs.io/en/latest/document.html#Document.get_toc