如何从pdf中提取带层级的标题结构

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": []
            }
        ]
    }
]

字段解释

  1. "title": "Introduction":目录项的实际文本内容

  2. "level": 1 :一级标题

    示例结构:

    复制代码
    1. 一级标题 ← 就是这个层级
       1.1 二级标题
       1.2 另一个二级标题
  3. "start_page": 4 :这个目录项指向文档的 第 4 页

    这是 1-based 的页码(即从 1 开始计数)

  4. dest_dict
    kind: 4

    表示链接类型为 命名目标(Named Destination)

    在 PyMuPDF 中,链接类型常量:

    python 复制代码
    LINK_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

相关推荐
飞Link2 小时前
后端架构选型:Django、Flask 与 Spring Boot 的三剑客之争
spring boot·python·django·flask
偷心伊普西隆2 小时前
Python EXCEL 半自动化切分数据集
python·自动化·excel
沛沛老爹2 小时前
从Web到AI:多模态Agent图像识别Skills开发实战——JavaScript+Python全栈图像处理方案
java·javascript·图像处理·人工智能·python·rag
hhy_smile2 小时前
Basic knowledge of Python
python
DS随心转小程序2 小时前
ChatGPT和Gemini转pdf
人工智能·ai·chatgpt·pdf·豆包·deepseek·ds随心转
chao_7892 小时前
跳跃游戏系列【贪心算法】
python·算法·游戏·贪心算法·贪心
飞Link2 小时前
K 折交叉验证(K-Fold Cross Validation)全解析:原理、代码实践、应用场景与常见坑点
人工智能·python·机器学习
衫水2 小时前
如何在离线情况下部署项目(前端VUE + 后端Python)
前端·vue.js·python
【赫兹威客】浩哥2 小时前
【赫兹威客】框架模板-后端命令行部署教程
python·django