如何从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

相关推荐
Java后端的Ai之路4 小时前
【Python 教程15】-Python和Web
python
冬奇Lab6 小时前
一天一个开源项目(第15篇):MapToPoster - 用代码将城市地图转换为精美的海报设计
python·开源
二十雨辰8 小时前
[python]-AI大模型
开发语言·人工智能·python
Yvonne爱编码8 小时前
JAVA数据结构 DAY6-栈和队列
java·开发语言·数据结构·python
前端摸鱼匠9 小时前
YOLOv8 环境配置全攻略:Python、PyTorch 与 CUDA 的和谐共生
人工智能·pytorch·python·yolo·目标检测
WangYaolove13149 小时前
基于python的在线水果销售系统(源码+文档)
python·mysql·django·毕业设计·源码
AALoveTouch9 小时前
大麦网协议分析
javascript·python
ZH15455891319 小时前
Flutter for OpenHarmony Python学习助手实战:自动化脚本开发的实现
python·学习·flutter
xcLeigh9 小时前
Python入门:Python3 requests模块全面学习教程
开发语言·python·学习·模块·python3·requests
xcLeigh9 小时前
Python入门:Python3 statistics模块全面学习教程
开发语言·python·学习·模块·python3·statistics