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

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

  • 引言:
  • 功能概述
    • 步骤一:提取PDF标题
    • 步骤二:生成目录和页码,合并PDF
    • 技术亮点
  • 代码
    • 步骤一:提取PDF标题(Step_two.ipynb)
    • 步骤二:生成目录和页码,合并PDF(Step_two.ipynb)

引言:

在学术研究、文档管理等领域,经常需要处理大量的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 PdfReaderdef 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 Nonedef 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_titledef 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 / 2top_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:  # 确保行包含spanfor 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 ifnot 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 titledef 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_titleelse:paper_title = get_pdf_title_2(pdf_path)return paper_titledef 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_pathelse: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 titlesspecific_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 osfrom PyPDF2 import PdfReader, PdfWriter
from reportlab.lib.pagesizes import letter
from reportlab.pdfbase.pdfmetrics import stringWidth
from reportlab.pdfgen import canvasdef 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)breaksplit_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 += 1else:# 分割的部分不添加编号# 分割的行需要空出编号和第一行相同的空间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) + 10dot_line_end = right_margin - stringWidth(str(page_number), "Helvetica", 12) - 10current_position = dot_line_startwhile current_position < dot_line_end:c.drawString(current_position, y_position, ".")current_position += dot_spacey_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_marginc.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)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/744916.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ROS 实时语音交互(一)ASR (流式识别)中文

目录 一、模型选择 二、流程 三、核心代码展示 背景&#xff1a;最近要做一个基于linux的语音交互&#xff0c;windows也可以跑通 一、模型选择 sherpa-ncnn 测试了四五个模型&#xff0c;只有这个模型比较好用&#xff0c;中文识别效果较好 这个模型好用./build/bin/sh…

Day63:WEB攻防-JS应用算法逆向三重断点调试调用堆栈BP插件发包安全结合

目录 前置知识 JS调试分析 JS分析调试结合Burp JS分析调试知识点&#xff1a; 1、JavaScript-作用域&调用堆栈 2、JavaScript-断点调试&全局搜索 3、JavaScript-Burp算法模块使用 前置知识 JS加密数据走向 浏览器调试 1、作用域&#xff1a;&#xff08;本地&全…

代码随想录算法训练营第四十七天|动态规划|198.打家劫舍、213.打家劫舍II、337.打家劫舍III

198.打家劫舍 文章 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的房屋在同一晚上被小偷闯入&#xff0c;系统会自动报警。 给定一个代…

进程间通信的方式及原理

进程间通信&#xff08;Inter-Process Communication, IPC&#xff09;是指在多进程环境下&#xff0c;操作系统提供的一种机制&#xff0c;使得不同进程之间能够交换信息或同步它们的执行。由于每个进程都有自己的独立地址空间&#xff0c;并且操作系统为了保证进程的隔离性&a…

决策树 | 分裂算法:ID3,C4.5,CART

这里写目录标题 一. ID3算法1. 信息增益2. ID3算法特点 二. C4.5算法1. 信息增益率2. C4.5算法特点 三. CART算法1. Gini系数公式2. CART算法特点3. CART回归树的分裂评价指标 小节 在决策树算法逻辑篇中&#xff0c;我们讲解了决策树的构建方式&#xff0c;下面我们来聊一聊决…

不依赖第三方平台,用Dart语言实现 ios 消息推送

仅仅给大家提供代码,还搞不定的欢迎咨询。 void _sendIosPushNotification(BleMessage message, String deviceToken, {bool debugMode = false}) async {final Map<String, dynamic> header = {"alg": "ES256", "kid": GloabelConfigu…

Broken Keyboard (a.k.a. Beiju Text)(UVA 11988)

网址如下&#xff1a; Broken Keyboard (a.k.a. Beiju Text) - UVA 11988 - Virtual Judge (vjudge.net) &#xff08;第三方网站&#xff09; 刚刚开始我是用C的list来做的&#xff0c;不过里面的元素是char&#xff0c;直接TLE 说实话我有点震惊&#xff0c;这不是双端链表…

Day16 面向对象进阶——接Day15

Day16 面向对象进阶——接Day15 文章目录 Day16 面向对象进阶——接Day15一、抽象类及抽象方法二、接口三、多态四、对象转型五、内部类 一、抽象类及抽象方法 //抽象类 public abstract class 类名{//抽象方法public abstract void method(); }1、抽象方法交给非抽象的子类去…

FreeRtos自学笔记4----参考正点原子视频

静态创建任务函数 TaskHandle_t xTaskCreateStatic { TaskFunction_t pxTaskCode, /* 指向任务函数的指针 / const char * const pcName, / 任务函数名 / const uint32_t ulStackDepth, / 任务堆栈大小注意字为单位 / void * const pvParameters, / 传递的任务函数参数 / UBase…

爬虫需要什么类型的代理ip?代理ip是必备的吗?

在信息时代&#xff0c;网络爬虫作为一种重要的数据采集工具&#xff0c;被广泛应用于各行各业。在这个过程中&#xff0c;代理IP成为了一个备受关注的话题。那么&#xff0c;爬虫需要什么类型的代理IP?代理IP是否是必不可少的呢? 今天我们就一起来看看~ 首先&#xff0c;我…

GPU服务器为什么需要DPU?

随着AI模型的复杂度增加以及数据量爆炸性增长&#xff0c;GPU服务器在执行训练和推理任务时&#xff0c;不仅面临计算密集型问题&#xff0c;还必须处理大量的数据移动、网络通信、存储I/O以及安全防护等非计算密集型任务。这些问题往往会成为性能瓶颈&#xff0c;消耗宝贵的CP…

【教学类-34-11】20240314 动物拼图(Midjounery生成线描图,8*8格拼图块 A4整张)(AI对话大师)

作品展示&#xff1a;——A4整页&#xff08;一人2张纸&#xff09; 背景需求&#xff1a; 通过春天拼图的个别化实验&#xff0c;我发现&#xff1a; 【教学类-34-10】20240313 春天拼图&#xff08;Midjounery生成线描图&#xff0c;4*4格拼图块&#xff09;&#xff08;AI…

1094. 拼车

说在前面 &#x1f388;不知道大家对于算法的学习是一个怎样的心态呢&#xff1f;为了面试还是因为兴趣&#xff1f;不管是出于什么原因&#xff0c;算法学习需要持续保持。 题目描述 车上最初有 capacity 个空座位。车 只能 向一个方向行驶&#xff08;也就是说&#xff0c;不…

在Docker上传我们自己的镜像(以springboot项目为例)

首先确定好在我们的centOS服务器上已经安装并配置好docker 配置自己的springboot镜像并运行 获取springboot的jar包 maven clean--》mavenue package --》复制target目录下生成的jar包 在服务器选择一个文件夹上传jar包&#xff0c;我这里选用的文件夹叫做/opt/dockertest…

【数据结构】树与堆 (向上/下调整算法和复杂度的分析、堆排序以及topk问题)

文章目录 1.树的概念1.1树的相关概念1.2树的表示 2.二叉树2.1概念2.2特殊二叉树2.3二叉树的存储 3.堆3.1堆的插入&#xff08;向上调整&#xff09;3.2堆的删除&#xff08;向下调整&#xff09;3.3堆的创建3.3.1使用向上调整3.3.2使用向下调整3.3.3两种建堆方式的比较 3.4堆排…

河南大学数据分析可视化实验-数据分析基础

计算机与信息工程学院实验报告 姓名&#xff1a; 杨馥瑞 学号&#xff1a;2212080042专业&#xff1a;数据科学与大数据分析技术 年级&#xff1a; 2022 课程&#xff1a; 数据分析和可视化 主讲教师&#xff1a; 周黎鸣 辅导教师&#xff1a; 周黎鸣 …

MISC-Catflag

前言 开始拿到这道题&#xff0c;以为是要识别文件类型&#xff0c;后面发现不是&#xff0c;kali识别为ascii文本文件。而用010editor打开&#xff0c;又是一堆看不懂的码 后面发现有很多重复内容1B 5B 43等等&#xff0c;再看题目type flag or cat flag可以联想linux的cat命…

【1】Python零基础起步

什么是编程(Programming) 编程是编定程序的中文简称&#xff0c;就是让计算机代码解决某个问题&#xff08;目的&#xff09;&#xff0c;对某个计算体系规定一定的运算方式&#xff0c;使计算体系按照该计算方式运行&#xff0c;并最终得到相应结果的过程&#xff08;手段&am…

微信小程序(五十九)使用鉴权组件时原页面js自动加载解决方法(24/3/14)

注释很详细&#xff0c;直接上代码 上一篇 新增内容&#xff1a; 1.使用覆盖函数的方法阻止原页面的自动执行方法 2.使用判断实现只有当未登录时才进行方法覆盖 源码&#xff1a; app.json {"pages": ["pages/index/index","pages/logs/logs"],…

【无标题】vmprotect net 混淆效果挺不错

vmprotect net 混淆效果挺不错,测试了一个&#xff0c;以前的写程序。用dnspy测试一下&#xff0c;效果非常好。 sunnf0451qq.com