今天又跟同事学到了实用的工作技巧。做工程资料经常需要把同一编号、同一桥梁的零散 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__(self, root):
self.root = root
self.root.title("PDF按桥梁合并工具")
self.root.geometry("700x650")
self.root.resizable(False, False)
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=20, pady=5)
tk.Button(file_frame, text="选择PDF文件...", command=self.select_files,
bg="#2196F3", fg="white", font=("微软雅黑", 11),
width=20, height=2).pack(side="left", padx=5)
tk.Button(file_frame, text="清空选择", command=self.clear_files,
bg="#FF9800", fg="white", font=("微软雅黑", 11),
width=10, height=2).pack(side="left", padx=5)
list_frame = tk.LabelFrame(self.root, text="已选择的文件", font=("微软雅黑", 10))
list_frame.pack(fill="both", expand=True, padx=20, pady=5)
self.file_listbox = tk.Listbox(list_frame, height=10, width=80)
self.file_listbox.pack(side="left", fill="both", expand=True, padx=5, pady=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=20, pady=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=100, length=600)
self.progress.pack(padx=20, pady=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=15, height=2).pack(side="left", padx=10)
tk.Button(btn_frame, text="退出", command=self.root.quit,
bg="#f44336", fg="white", font=("微软雅黑", 12),
width=10, height=2).pack(side="left", padx=10)
self.result_text = tk.Text(self.root, height=6, width=80)
self.result_text.pack(padx=20, pady=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(self, filename):
"""
从文件名提取桥梁信息
格式: 编号 序号桥名... 或 编号 桥名...
"""
name = Path(filename).stem
parts = name.split(' ', 1)
iflen(parts) !=2:
returnNone, None
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(self, files):
"""按编号前缀分组"""
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(self, message):
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(self, file_list, output_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()
夜雨聆风