乐于分享
好东西不私藏

PDF 按桥梁编号自动分组合并

PDF 按桥梁编号自动分组合并

今天又跟同事学到了实用的工作技巧。做工程资料经常需要把同一编号、同一桥梁的零散 PDF,整合为一份完整文件。传统手动操作:新建空白 PDF、逐页插入内容、逐一整理归类重命名。碰上几十个、上百个桥梁文件,往往要耗费一两天时间,不仅枯燥费力,还容易出现编号错乱、页面漏缺、合并出错等问题。

其实这种纯机械、高重复的低效工作,完全可以借助 AI 编写 Python 脚本,一键自动化搞定,彻底告别手动苦力!

# -*- coding: utf-8 -*-

"""

PDF按桥梁合并工具

根据文件名中的桥梁编号分组合并PDF

文件名格式示例:

  C4-3-5-5 1黄河大桥全桥工程数量表.pdf

  C4-3-5-5 黄河大桥桥型图(共2页).pdf

合并后文件名格式:

  C4-3-5-5 黄河大桥.pdf

使用方法:

    双击运行,选择PDF文件后自动分组合并

"""

import tkinter as tk

from tkinter import filedialog, messagebox, ttk

from pathlib import Path

import re

import os

import sys

import traceback

# 优先使用pypdf(PyPDF2的继任者,修复了很多bug)

HAS_PYPDF =False

USE_PYPDF =False

try:

from pypdf import PdfWriter, PdfReader

    HAS_PYPDF =True

    USE_PYPDF =True

print("使用pypdf库")

exceptImportError:

pass

ifnot HAS_PYPDF:

try:

from PyPDF2 import PdfWriter, PdfReader

        HAS_PYPDF =True

print("使用PyPDF2库")

exceptImportError:

pass

classPdfMergeByBridgeTool:

def__init__(selfroot):

self.root = root

self.root.title("PDF按桥梁合并工具")

self.root.geometry("700x650")

self.root.resizable(FalseFalse)

self.selected_files = []

self.progress_var = tk.DoubleVar(value=0)

self.create_widgets()

ifnot HAS_PYPDF:

            messagebox.showerror("错误""无法导入pypdf/PyPDF2\n\n请手动安装: pip install pypdf")

defcreate_widgets(self):

        tk.Label(self.root, text="PDF按桥梁合并工具",

font=("微软雅黑"16"bold")).pack(pady=10)

        tk.Label(self.root, text="文件名格式: 编号 序号桥名...\n合并后: 编号 桥名.pdf",

fg="gray"font=("微软雅黑"9)).pack(pady=2)

        file_frame = tk.Frame(self.root)

        file_frame.pack(fill="x"padx=20pady=5)

        tk.Button(file_frame, text="选择PDF文件..."command=self.select_files,

bg="#2196F3"fg="white"font=("微软雅黑"11),

width=20height=2).pack(side="left"padx=5)

        tk.Button(file_frame, text="清空选择"command=self.clear_files,

bg="#FF9800"fg="white"font=("微软雅黑"11),

width=10height=2).pack(side="left"padx=5)

        list_frame = tk.LabelFrame(self.root, text="已选择的文件"font=("微软雅黑"10))

        list_frame.pack(fill="both"expand=Truepadx=20pady=5)

self.file_listbox = tk.Listbox(list_frame, height=10width=80)

self.file_listbox.pack(side="left"fill="both"expand=Truepadx=5pady=5)

        scrollbar = tk.Scrollbar(list_frame, command=self.file_listbox.yview)

        scrollbar.pack(side="right"fill="y")

self.file_listbox.config(yscrollcommand=scrollbar.set)

        output_frame = tk.Frame(self.root)

        output_frame.pack(fill="x"padx=20pady=5)

self.output_path = tk.StringVar()

        tk.Label(output_frame, text="输出文件夹:").pack(side="left")

        tk.Entry(output_frame, textvariable=self.output_path, width=45).pack(side="left"padx=5)

        tk.Button(output_frame, text="浏览..."command=self.select_output_folder).pack(side="left")

self.progress = ttk.Progressbar(self.root, variable=self.progress_var,

maximum=100length=600)

self.progress.pack(padx=20pady=5)

        btn_frame = tk.Frame(self.root)

        btn_frame.pack(pady=10)

        tk.Button(btn_frame, text="开始合并"command=self.start_merge,

bg="#4CAF50"fg="white"font=("微软雅黑"12"bold"),

width=15height=2).pack(side="left"padx=10)

        tk.Button(btn_frame, text="退出"command=self.root.quit,

bg="#f44336"fg="white"font=("微软雅黑"12),

width=10height=2).pack(side="left"padx=10)

self.result_text = tk.Text(self.root, height=6width=80)

self.result_text.pack(padx=20pady=5)

self.result_text.insert("1.0""等待操作...\n")

self.result_text.config(state="disabled")

defselect_files(self):

        files = filedialog.askopenfilenames(

title="选择PDF文件",

filetypes=[("PDF文件""*.pdf"), ("所有文件""*.*")]

        )

if files:

forfilein files:

iffilenotinself.selected_files:

self.selected_files.append(file)

self.file_listbox.insert("end", Path(file).name)

self.log(f"已选择 {len(self.selected_files)} 个文件")

ifnotself.output_path.get() andself.selected_files:

self.output_path.set(str(Path(self.selected_files[0]).parent))

defclear_files(self):

self.selected_files.clear()

self.file_listbox.delete(0"end")

self.log("已清空选择")

defselect_output_folder(self):

        folder = filedialog.askdirectory()

if folder:

self.output_path.set(folder)

defextract_bridge_info(selffilename):

"""

        从文件名提取桥梁信息

        格式: 编号 序号桥名...  或  编号 桥名...

        """

        name = Path(filename).stem

        parts = name.split(' '1)

iflen(parts) !=2:

returnNoneNone

        number = parts[0].strip()

        rest = parts[1].strip()

# 去掉开头的数字序号

        match = re.match(r'^(\d+)(.*)', rest)

if match:

            rest = match.group(2).strip()

# 提取桥名

        bridge_keywords = ['特大桥''大桥''中桥''小桥''互通''通道''涵洞']

        bridge_name =None

for keyword in bridge_keywords:

            idx = rest.find(keyword)

if idx >=0:

                bridge_name = rest[:idx +len(keyword)]

break

ifnot bridge_name:

            bridge_name = rest

# 清理末尾括号

        bridge_name = re.sub(r'\(.*\)$''', bridge_name).strip()

return number, bridge_name

defgroup_files_by_bridge(selffiles):

"""按编号前缀分组"""

        groups = {}

for file_path in files:

            filename = Path(file_path).name

            number, bridge_name =self.extract_bridge_info(filename)

if number:

if number notin groups:

                    groups[number] = {'bridge_name': bridge_name, 'files': []}

if bridge_name andlen(bridge_name) <len(groups[number]['bridge_name']):

                    groups[number]['bridge_name'= bridge_name

                groups[number]['files'].append(file_path)

else:

                groups[filename] = {'bridge_name': filename, 'files': [file_path]}

        result = {}

for number, data in groups.items():

            output_name =f"{number}{data['bridge_name']}"

            result[output_name] = data['files']

return result

deflog(selfmessage):

self.result_text.config(state="normal")

self.result_text.insert("end", message +"\n")

self.result_text.see("end")

self.result_text.config(state="disabled")

self.root.update()

defclear_log(self):

self.result_text.config(state="normal")

self.result_text.delete("1.0""end")

self.result_text.config(state="disabled")

defmerge_pdf_files(selffile_listoutput_path):

"""合并多个PDF文件(使用PdfWriter逐页复制,绕过PdfMerger的bug)"""

try:

            writer = PdfWriter()

for pdf_file in file_list:

                reader = PdfReader(str(pdf_file))

for page in reader.pages:

                    writer.add_page(page)

withopen(str(output_path), 'wb'as f:

                writer.write(f)

returnTrue

exceptExceptionas e:

self.log(f"  合并失败: {type(e).__name__}{e}")

self.log(f"  详细错误: {traceback.format_exc()}")

returnFalse

defstart_merge(self):

ifnot HAS_PYPDF:

            messagebox.showerror("错误""无法导入pypdf/PyPDF2\n\n请手动安装: pip install pypdf")

return

ifnotself.selected_files:

            messagebox.showwarning("警告""请先选择PDF文件")

return

        output_folder =self.output_path.get().strip()

ifnot output_folder:

            messagebox.showwarning("警告""请选择输出文件夹")

return

        output_path = Path(output_folder)

ifnot output_path.exists():

            messagebox.showerror("错误""输出文件夹不存在")

return

        groups =self.group_files_by_bridge(self.selected_files)

ifnot groups:

            messagebox.showinfo("提示""无法从文件名中提取桥梁信息")

return

        group_info ="\n".join([f"  {name}{len(files)} 个文件"for name, files in groups.items()])

ifnot messagebox.askyesno("确认",

f"将合并为 {len(groups)} 个PDF文件\n\n{group_info}\n\n是否继续?"):

return

self.clear_log()

self.log(f"开始处理 {len(self.selected_files)} 个文件")

self.log(f"识别到 {len(groups)} 个桥梁组")

self.log("="*50)

        success_count =0

        error_count =0

        total =len(groups)

for i, (group_name, files) inenumerate(groups.items()):

            progress = (i / total) *100

self.progress_var.set(progress)

self.root.update()

            safe_name = re.sub(r'[\\/:*?"<>|]''_', group_name)

            output_file = output_path /f"{safe_name}.pdf"

self.log(f"[合并] {group_name}")

self.log(f"  包含 {len(files)} 个文件:")

for f in files:

self.log(f"    - {Path(f).name}")

ifself.merge_pdf_files(files, output_file):

self.log(f"  ✓ 已保存: {output_file.name}\n")

                success_count +=1

else:

                error_count +=1

self.progress_var.set(100)

self.root.update()

self.log("="*50)

self.log(f"完成!成功:{success_count} 失败:{error_count}")

        messagebox.showinfo("完成",

f"合并完成!\n成功: {success_count}\n失败: {error_count}")

defmain():

    root = tk.Tk()

    app = PdfMergeByBridgeTool(root)

    root.mainloop()

if__name__=="__main__":

    main()