Python:自动化处理PDF文档集合,提取文献标题、合并文献PDF并生成目录和页码

Python:自动化处理PDF文档集合,提取文献标题、合并文献PDF并生成目录和页码

引言:

在学术研究、文档管理等领域,经常需要处理大量的PDF文档。手动整理这些文档既耗时又低效。本文介绍一个使用Python自动化这一过程的方法,包括提取PDF文件的标题,生成目录,添加页码,并最终合并为一个PDF文件。这不仅提高了工作效率,也增加了文档的可用性和可读性。

功能概述

本项目通过两个主要步骤实现PDF文档的自动化处理:

  1. 提取PDF文档的标题:从每个PDF文件中提取标题,并保存到一个CSV文件中。这一步允许用户手动校对和修正自动提取的标题。
  2. 生成目录和页码,然后合并PDF文件:根据校对后的标题,自动生成目录页,为每个PDF文件的每一页添加页码,最后将所有文件合并成一个PDF。

步骤一:提取PDF标题

首先,将所有待处理的PDF文件放入指定的目录中。运行第一步脚本(Step_one.ipynb),该脚本自动遍历目录中的每个PDF文件,提取其标题,并将文件名及对应的标题保存到一个CSV文件中。

这一步骤涉及PDF元数据的读取和文本提取技术。对于难以直接从元数据中获取标题的情况,脚本尝试从PDF的内容中分析出可能的标题。处理完所有文件后,用户可以检查CSV文件,并手动修正错误的标题。

步骤二:生成目录和页码,合并PDF

在校对完CSV文件中的标题后,运行第二步脚本(Step_two.ipynb)。该脚本首先根据CSV文件中的信息生成一个目录页,然后为每个PDF页面添加页码,并将所有PDF文件合并为一个。

目录页的生成考虑了标题的长度,对过长的标题进行适当的分行处理,确保目录的整洁性。页码的添加在页面底部中央,通过绘制白色矩形覆盖原有页码区域后添加新的页码信息,以避免页码重叠。最终,所有页面(包括目录页和带有新页码的原始页面)被合并成一个PDF文件。

技术亮点

  • 文本提取与处理 :通过PyMuPDFPyPDF2库提取PDF文件的文本和元数据,使用正则表达式和文本处理技术清洗和格式化标题。
  • 动态内容生成 :使用reportlab库动态生成包含自定义文本(如页码和目录项)的PDF页面。
  • 文档合并与修改 :利用PyPDF2库合并PDF页面,并在合并过程中添加自定义内容。

通过这个Python项目,我们可以自动化处理一系列复杂的PDF文档管理任务,包括提取标题、生成目录、添加页码和合并文件。这大大减轻了手动处理的负担,使得管理大量PDF文档变得既简单又高效。无论是学术研究者、图书管理员还是文档管理专业人士,都可以从这个项目中受益。

代码

步骤一:提取PDF标题(Step_two.ipynb)

python 复制代码
### 第一步:读取pdf_dir路径下所有.pdf为后缀的文件,打开CSV文件以写入文件名和标题
### 第二步:手动对CSV文件内错误标题进行修改


 # 读取路径下所有.pdf为后缀的文件
pdf_dir = '老师的论文集/'
 # 合并后的PDF名字
output_pdf_path = "合并后/老师的论文集.pdf"
 # 用于中间 存放文件名与标题的CSV文件
TitlesCSV = '合并后/老师的论文集.csv'```

import csv
import html
import os
import re  # 导入正则表达式模块

import fitz  # PyMuPDF
from PyPDF2 import PdfReader


def find_non_text_chars(sentence):
    # 用于检测提取的文本中是否出现非文本类型的,若有则通过类似title = title.replace("fi", "fi")替换
    import regex as re
    # 定义正则表达式,匹配非文本字符(除了字母、数字、空格和标点符号之外的字符)
    non_text_pattern = re.compile(r'[^a-zA-Z0-9\s\p{P}]', re.UNICODE)
    # 使用正则表达式搜索句子中的非文本字符
    non_text_chars = non_text_pattern.findall(sentence)
    # 打印出非文本字符及其类型
    for char in non_text_chars:
        print(title)
        print(f"非文本字符 '{char}' 的类型是 '{type(char)}\n\n'")
    return None


def get_pdf_title_1(pdf_path):
    """读取PDF文件的标题,并进行处理。"""
    with open(pdf_path, 'rb') as pdf_file:
        pdf_reader = PdfReader(pdf_file)
        doc_info = pdf_reader.metadata
        # 尝试从文档信息中获取标题
        paper_title = doc_info.get('/Title', 'untitled') if doc_info else 'untitled'

        # 如果标题有效,则进行进一步处理
        if paper_title != 'untitled' and paper_title != 'Untitled' and not paper_title.endswith('.pdf'):
            # 解码HTML实体
            paper_title = html.unescape(paper_title)
            # 替换不适合作为文件名的字符
            paper_title = re.sub(r'[:/\\*?"\'<>|]', ' ', paper_title)
        else:
            # 无效的标题,返回默认值
            paper_title = 'untitled'
        return paper_title


def get_pdf_title_2(pdf_path):
    # 检查文件名是否符合特定模式
    filename = os.path.basename(pdf_path)
    if filename == "[SCI 】ions for nonlinear dynamical systems.pdf":
        return "Estynamical systems"

    doc = fitz.open(pdf_path)
    first_page = doc[0]  # 只查看第一页

    # 获取页面上所有文本块,每个块包含文字、字体大小和位置
    blocks = first_page.get_text("dict")["blocks"]
    # 只考虑页面上半部分的文本块
    mid_y = first_page.rect.height / 2
    top_blocks = [b for b in blocks if b['type'] == 0 and b['bbox'][3] < mid_y]

    # 提取每个文本块的字体大小和文本内容
    text_blocks_with_size = []
    for block in top_blocks:
        if 'lines' in block:  # 确保文本块包含行
            for line in block['lines']:
                if 'spans' in line:  # 确保行包含span
                    for span in line['spans']:
                        if 'size' in span and len(span['text'].strip()) >= 2:  # 检查span中是否有size信息且文本长度符合要求
                            text_blocks_with_size.append((span['text'], span['size'], span['bbox']))

    # 排除特定关键词
    excluded_keywords = ["Research Article", "Physica A", "Neurocomputing",
                         "Sustainable Energy Technologies and Assessments"]
    filtered_blocks = [block for block in text_blocks_with_size if
                       not any(keyword in block[0] for keyword in excluded_keywords)]

    # 在过滤后的文本块中基于字体大小和垂直位置来识别可能的标题
    if filtered_blocks:
        max_font_size = max([size for _, size, _ in filtered_blocks], default=0)
        possible_title_blocks = [block for block in filtered_blocks if block[1] == max_font_size]

        # 合并具有相同最大字体大小的连续文本块
        title_texts = [block[0] for block in possible_title_blocks]
        title = " ".join(title_texts) if title_texts else "untitled"
    else:
        title = "untitled"

    doc.close()
    title = title.replace("fi", "fi")
    title = title.replace("ff", "ff")
    # 查找句子中的非文本字符
    find_non_text_chars(title)
    return title


def get_pdf_title(pdf_path):
    # 先使用get_pdf_title_1获取标题,若获取失败则使用get_pdf_title_2获取
    paper_title = get_pdf_title_1(pdf_path)  # 假设这是从PDF提取标题的函数
    # 编写一个正则表达式来匹配以连续4个数字和.pdf为后缀的字符串
    # 匹配以连续三个数字和.pdf结尾的字符串,或者包含空格和点的字符串,以及不包含空格但包含点的字符串
    regex_pattern = r'\d{3}\.pdf$|^[A-Z]+-\w+\s\d+\.\.\d+$|\w+\.\d+\s\d+\.\.\d+$|^[a-zA-Z]+_\d+\w*$'

    # 判断条件:标题不是'untitled'且不匹配正则表达式(即不是以连续4个数字和.pdf结尾)
    if paper_title != 'untitled' and not re.search(regex_pattern, paper_title):
        return paper_title
    else:
        paper_title = get_pdf_title_2(pdf_path)
        return paper_title


def get_titles_from_directory(directory_path, specific_file):
    titles = []
    specific_pdf_path = None  # 用于存储特定文件的路径
    for root, dirs, files in os.walk(directory_path):
        for file in files:
            if file.lower().endswith('.pdf'):
                pdf_path = os.path.join(root, file)
                if file == specific_file:  # 如果当前文件是特定文件
                    specific_pdf_path = pdf_path
                else:
                    try:
                        title = get_pdf_title(pdf_path)
                        titles.append((file, title))
                    except Exception as e:
                        print(f"Error processing {file}: {e}")

    # 处理特定文件
    if specific_pdf_path:
        try:
            title = get_pdf_title(specific_pdf_path)
            titles.insert(0, (specific_file, title))  # 将特定文件的标题插入到列表的最前面
        except Exception as e:
            print(f"Error processing {specific_file}: {e}")

    return titles


specific_file = "lic health.pdf"

 # 替换为你的PDF文件所在的目录路径
directory_path = pdf_dir
titles = get_titles_from_directory(directory_path, specific_file)

with open(TitlesCSV, 'w', newline='', encoding='utf-8') as csv_file:
    csv_writer = csv.writer(csv_file, delimiter=',')
    csv_writer.writerow(['Files', 'Title'])  # 写入头部信息
    for file, title in titles:
        # 写入文件名和标题
        csv_writer.writerow([file, title])

步骤二:生成目录和页码,合并PDF(Step_two.ipynb)

python 复制代码
### 第三步:读取 Step_one.ipynb获取的标题的CSV文件
### 第四步:根据文件名字 标题 合并PDF 并生成目录与页码

# 读取路径下所有.pdf为后缀的文件
pdf_dir = '老师的论文集/'
# 合并后的PDF名字
output_pdf_path = "合并后/老师的论文集.pdf"
# 用于中间 存放文件名与标题的CSV文件
TitlesCSV = '合并后/老师的论文集.csv'

import csv
import io
import os

from PyPDF2 import PdfReader, PdfWriter
from reportlab.lib.pagesizes import letter
from reportlab.pdfbase.pdfmetrics import stringWidth
from reportlab.pdfgen import canvas


def create_footer_page(footer_text):
    packet = io.BytesIO()
    c = canvas.Canvas(packet, pagesize=letter)
    width, height = letter  # letter页面的宽度和高度
    font_name = "Helvetica"  # 使用的字体
    font_size = 12  # 字体大小
    cover_height = font_size + 4  # 覆盖区域的高度稍大于字体大小,以确保完全覆盖原有页码
    cover_y_position = 28  # 覆盖区域的Y位置,根据需要进行调整以确保覆盖原有页码

    # 计算文本宽度和起始X位置以居中文本
    text_width = c.stringWidth(footer_text, font_name, font_size)
    text_start_position = (width - text_width) / 2

    # 绘制一个足够大的白色矩形以覆盖原有页码
    c.setFillColorRGB(1, 1, 1)  # 设置填充颜色为白色
    c.rect(0, cover_y_position, width, cover_height, stroke=False, fill=True)

    # 在页脚区域居中添加文本,高度可以根据需要调整
    c.setFont(font_name, font_size)  # 设置字体和大小
    c.setFillColorRGB(0, 0, 0)  # 设置文本颜色为黑色
    c.drawString(text_start_position, 32, footer_text)  # 绘制居中的页脚文本

    c.save()
    packet.seek(0)
    return PdfReader(packet)


# 定义用于分割过长标题的函数,以适应页面宽度
def split_title(title, available_width, font_name="Helvetica", font_size=12):
    split_titles = []  # 存储分割后的标题部分
    # 循环直到标题宽度小于可用宽度
    while stringWidth(title, font_name, font_size) > available_width:
        split_point = len(title)  # 初始分割点设置为标题长度
        # 寻找适合分割的位置,使分割后的宽度小于可用宽度
        while split_point > 0 and stringWidth(title[:split_point] + "-", font_name, font_size) > available_width:
            split_point -= 1  # 逐字符减少分割点

        if split_point == 0:  # 如果找不到分割点,添加整个标题并结束循环
            split_titles.append(title)
            break

        split_titles.append(title[:split_point] + "-")  # 添加分割后的标题部分
        title = title[split_point:]  # 准备处理剩余的标题部分

    if title:  # 确保添加剩余的未分割部分
        split_titles.append(title)

    return split_titles


# 添加目录页的函数,包含书签的标题和页码
def add_catalog_page(bookmarks):
    packet = io.BytesIO()  # 创建内存流以存储PDF数据
    c = canvas.Canvas(packet, pagesize=letter)  # 创建PDF画布
    width, height = letter  # 获取页面尺寸
    top_margin = 60  # 顶部边距
    bottom_margin = 60  # 底部边距
    y_position = height - top_margin  # 初始Y坐标位置
    c.setFont("Helvetica-Bold", 16)  # 设置目录标题字体和大小
    c.drawString(280, y_position, "Directory")  # 绘制目录标题
    y_position -= 30  # 更新Y坐标为目录项

    c.setFont("Helvetica", 12)  # 设置目录项字体和大小
    left_margin = 72  # 左边距
    right_margin = width - 72  # 右边距
    dot_space = 5  # 点线间隔
    different_title_spacing = 25  # 不同标题间隔
    same_title_line_spacing = 15  # 同一标题行间隔

    title_number = 1  # 标题编号初始值

    for title, page_number in bookmarks:
        split_titles = split_title(title, right_margin - left_margin - 25, "Helvetica", 12)  # 分割长标题

        for index, part_title in enumerate(split_titles):
            if index == 0:
                # 对新标题的第一部分添加编号
                formatted_number = str(title_number).zfill(2)
                full_title = f"{formatted_number}. {part_title}"
                title_number += 1
            else:
                # 分割的部分不添加编号
                # 分割的行需要空出编号和第一行相同的空间
                full_title_blank = " " * len(str(title_number).zfill(2) + ".   ")
                full_title = f"{full_title_blank}{part_title}"

            c.drawString(left_margin, y_position, full_title)  # 绘制标题

            if index == len(split_titles) - 1:  # 在最后一部分标题处添加页码
                c.drawRightString(right_margin, y_position, str(page_number))  # 绘制页码

                # 绘制连接标题和页码的点线
                dot_line_start = left_margin + stringWidth(full_title, "Helvetica", 12) + 10
                dot_line_end = right_margin - stringWidth(str(page_number), "Helvetica", 12) - 10
                current_position = dot_line_start
                while current_position < dot_line_end:
                    c.drawString(current_position, y_position, ".")
                    current_position += dot_space

            y_position -= same_title_line_spacing  # 更新Y坐标为同一标题的下一行

        y_position -= different_title_spacing - same_title_line_spacing  # 为下一个标题更新Y坐标,减去已应用的间隔

        if y_position < bottom_margin:  # 如果超出页面底部,创建新页面
            c.showPage()
            y_position = height - top_margin
            c.setFont("Helvetica", 12)  # 确保新页面使用相同的字体设置

    c.save()  # 保存PDF数据到内存流
    packet.seek(0)  # 将内存流指针重置到起始位置
    return PdfReader(packet)  # 创建PDF阅读器对象,返回包含目录页数据的对象


# 读取CSV文件
pdf_titles_info = []
with open(TitlesCSV, 'r', encoding='utf-8') as csvfile:
    reader = csv.reader(csvfile)
    next(reader)  # 跳过标题行
    for row in reader:
        # 假设第一列是文件名,第二列是标题
        pdf_titles_info.append(row)

# 准备工作区
all_pages = []
bookmarks = []
total_pages = 0

# 更新:根据pdf_titles_info直接处理文件
for filename, title in pdf_titles_info:
    pdf_path = os.path.join(pdf_dir, filename)
    bookmarks.append((title, total_pages + 1))  # 使用提供的标题而不是重新获取
    reader = PdfReader(pdf_path)
    for page in reader.pages:
        all_pages.append(page)
        total_pages += 1

# 创建目录页
writer = PdfWriter()
catalog_pdf = add_catalog_page(bookmarks)  # 这里假设add_catalog_page可以处理bookmarks列表
for page in catalog_pdf.pages:
    writer.add_page(page)

# 为每页添加页脚
current_page_number = 1
for page in all_pages:
    footer_pdf = create_footer_page(f"Page number:{current_page_number}")
    page.merge_page(footer_pdf.pages[0])
    writer.add_page(page)
    current_page_number += 1

# 保存最终的PDF文件
output_pdf_path = output_pdf_path
with open(output_pdf_path, "wb") as f_out:
    writer.write(f_out)
相关推荐
湫ccc7 分钟前
《Python基础》之基本数据类型
开发语言·python
吃肉不能购23 分钟前
Label-studio-ml-backend 和YOLOV8 YOLO11自动化标注,目标检测,实例分割,图像分类,关键点估计,视频跟踪
运维·yolo·自动化
drebander1 小时前
使用 Java Stream 优雅实现List 转化为Map<key,Map<key,value>>
java·python·list
威威猫的栗子1 小时前
Python Turtle召唤童年:喜羊羊与灰太狼之懒羊羊绘画
开发语言·python
小小大侠客2 小时前
IText创建加盖公章的pdf文件并生成压缩文件
java·pdf·itext
墨染风华不染尘2 小时前
python之开发笔记
开发语言·笔记·python
Dxy12393102162 小时前
python bmp图片转jpg
python
麦麦大数据2 小时前
Python棉花病虫害图谱系统CNN识别+AI问答知识neo4j vue+flask深度学习神经网络可视化
人工智能·python·深度学习
LKID体2 小时前
Python操作neo4j库py2neo使用之创建和查询(二)
数据库·python·neo4j
LKID体2 小时前
Python操作neo4j库py2neo使用之py2neo 删除及事务相关操作(三)
开发语言·python·neo4j