Python:自动化处理PDF文档集合,提取文献标题、合并文献PDF并生成目录和页码
引言:
在学术研究、文档管理等领域,经常需要处理大量的PDF文档。手动整理这些文档既耗时又低效。本文介绍一个使用Python自动化这一过程的方法,包括提取PDF文件的标题,生成目录,添加页码,并最终合并为一个PDF文件。这不仅提高了工作效率,也增加了文档的可用性和可读性。
功能概述
本项目通过两个主要步骤实现PDF文档的自动化处理:
- 提取PDF文档的标题:从每个PDF文件中提取标题,并保存到一个CSV文件中。这一步允许用户手动校对和修正自动提取的标题。
- 生成目录和页码,然后合并PDF文件:根据校对后的标题,自动生成目录页,为每个PDF文件的每一页添加页码,最后将所有文件合并成一个PDF。
步骤一:提取PDF标题
首先,将所有待处理的PDF文件放入指定的目录中。运行第一步脚本(Step_one.ipynb
),该脚本自动遍历目录中的每个PDF文件,提取其标题,并将文件名及对应的标题保存到一个CSV文件中。
这一步骤涉及PDF元数据的读取和文本提取技术。对于难以直接从元数据中获取标题的情况,脚本尝试从PDF的内容中分析出可能的标题。处理完所有文件后,用户可以检查CSV文件,并手动修正错误的标题。
步骤二:生成目录和页码,合并PDF
在校对完CSV文件中的标题后,运行第二步脚本(Step_two.ipynb
)。该脚本首先根据CSV文件中的信息生成一个目录页,然后为每个PDF页面添加页码,并将所有PDF文件合并为一个。
目录页的生成考虑了标题的长度,对过长的标题进行适当的分行处理,确保目录的整洁性。页码的添加在页面底部中央,通过绘制白色矩形覆盖原有页码区域后添加新的页码信息,以避免页码重叠。最终,所有页面(包括目录页和带有新页码的原始页面)被合并成一个PDF文件。
技术亮点
- 文本提取与处理 :通过
PyMuPDF
和PyPDF2
库提取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)