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

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

时间:2024-03-14 22:58:54浏览次数:28  
标签:title Python 标题 文献 pdf PDF path page

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)

### 第一步:读取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)

### 第三步:读取 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)

标签:title,Python,标题,文献,pdf,PDF,path,page
From: https://blog.csdn.net/weixin_66397563/article/details/136724347

相关文章

  • python上传图片到网站
    使用requests库实现图片上传在Python中,requests库是处理HTTP请求的一个强大工具,它提供了一种简单易用的方法来执行网络请求。在将图片上传到网站的场景中,可以使用requests库中的post方法,将图片作为多部分编码文件(multipart-encodedfile)发送到服务器。第一,需要一份待上传的图......
  • python项目开发——总结笔记(csv excel读取 服务端端口进程 拟合预测 时间格式转化 服
    目录部署服务端程序主服务端控制程序main.py子目录的计算程序jisuan.py读取数据读取csv数据读取读取excel时间格式转换时间戳转datetime并且生成时间序列最后格式化时间 常用函数拟合预测服务端程序控制与维护部署服务端程序主服务端控制程序main.pyfromfl......
  • 【二分法】分巧克力问题/python
    1.看出是用二分法:最大值最小化,最小值最大化,满足条件的最值,用二分法做。2.确定low,high,确定check的条件3.注意: 是当low<high的时候进行循环,当相等或大于的时候输出,while的条件不能写错。 本题是在区间里面找满足条件的最大值,所以,在算mid的时候面对取整的问题让它向大......
  • 少样本知识图谱补全技术研究概述(持续更新,现在读文献还太少,等我读文献的)
    一、少样本知识图谱补全概述和相关内容1、知识图谱概述1.1知识图谱定义        知识图谱(knowledgegraph,KG)用结构化的形式描述客观世界中概念、实体及其关系,它将互联网的信息表达成更接近人类认知世界的形式,提供了一种更好地组织、管理和理解互联网海量信息的能力。......
  • python post测试
    pythonpost测试 importrequests#设置请求的URLurl='http://example.com/api/post'#准备要发送的数据,这里假设有一个中文字段'name'data={'name':'张三',#中文名字'age':30}#发送POST请求,指定headers中的Content-Type为applica......
  • Python学习随记(二)
    Python学习随记(二)print函数#hello,aworld为print函数所输出测内容,sep='|'中表示使用|替换为输出内容间原本的空格,#end=''使用空格替换print函数结尾原本的换行符print("hello","aworld",sep='|',end='')#检测多行注释是否为字符串print(&......
  • python下载win32gui的库失败解决教程
    1、进入这个网站https://www.lfd.uci.edu/~gohlke/pythonlibs/界面如下:因为这些安装包都是按照字母顺序排序的,所以就向下翻到pywin32的位置就行;选择跟自己的python版本相对应的这个库的版本,点击即可下载;等待下载完成:2、进入到pycharm软件里面,运用命令实现库的安装python-......
  • 初识python
    师从黑马程序员字面量python中常用的6种数据的类型通过三对引号进行注释,例: """hellowrold"""数据类型使用type查看数据类型name="黑马"name_type=type(name)print(name_type)类型转化 运算符print("5/2=",5/2)print("5//2=",5//2)print("2......
  • 有手就会Python自定义模块使用
    1.自定义模块自定义模块一般是在项目中根据自己的需求进行的封装项目中自定义了额一个模块,module.pyname="张三"age=23weight=160height=187deftest():print("测试的方法")defdemo():print("天使的眼泪")deffn():print("老鼠爱大米")2.......
  • Python使用RocketMQ(消息队列)
    消息队列在日常开发中比较常用的开发中间件,每家大厂一般都会具有自己的消息队列服务器。本文主要讲述Python中如何使用RocketMQ的相关SDK。希望大家在阅读本文前可以先了解一下RocketMQ的基本知识。使用 pipinstallrocketmq-ihttps://pypi.tuna.tsinghua.edu.cn/sim......