• 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])
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159

    步骤二:生成目录和页码,合并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)
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
  • 相关阅读:
    Java用transferTo方式实现文件上传
    后端接口性能优化分析-问题发现&问题定义
    软考【网络管理员】100道高频考题(含知识点解析),轻松45+
    Java日期类汇总
    云原生-VMware虚拟机安装Kubesphere实战(一)
    Java中的IO流
    工作相关----《配置bond》
    ardupilot BMI088加速度陀螺仪学习
    计数器(JS闭包)
    怎么把图片转换成ico图标文件?
  • 原文地址:https://blog.csdn.net/weixin_66397563/article/details/136724347