周一早上九点,你的邮箱里躺着十几封邮件,每封都带了一个PDF附件——上周会议的签到表、各小组的报告、财务的报销凭证。领导在微信群里发了一条消息:“把这些文件合并成一个,发到公司群里。”
你打开第一个PDF,又打开第二个,鼠标拖来拖去,发现Adobe Acrobat弹出一个窗口:“您的试用期已结束。”同事推荐了在线合并工具,你上传完所有文件,等待进度条走到100%,页面弹出五个字:“请付费解锁。”
这不是你的错。PDF合并这件事看起来简单,真上手才发现处处是坑。在线工具要么限制页数,要么加水印,要么悄悄把你的文件传到不明服务器。桌面软件不是收费就是臃肿得像个航空母舰。
你需要的其实很简单:一个能批量处理、不用花钱、跑起来就完事的工具。Python刚好能做到,而且代码少到你不用是程序员也能看懂。
先把你需要的工具装好
Python合并PDF这件事,全靠一个叫PyPDF2的库。它专门用来处理PDF,能读、能写、能合并、能拆分。
安装就一行命令:
pip install PyPDF2
如果你用的是Python 3,可能需要安装PyPDF2的升级版PyPDF4,用法几乎一样。这里用PyPDF2演示,兼容性最好。
装完之后,打开你的代码编辑器,新建一个Python文件,就叫merge_pdf.py。
最简单的合并:把所有PDF放在一起
假设你的桌面上有一个文件夹叫“待合并”,里面放了三个PDF文件:报告1.pdf、报告2.pdf、报告3.pdf。你想把它们按顺序拼成一个总报告。
代码长这样:
import PyPDF2
import os
创建一个空的PDF写入器
merger = PyPDF2.PdfMerger()
定义文件夹路径
folder_path = r"C:\Users\你的用户名\Desktop\待合并"
获取文件夹里所有PDF文件,并按文件名排序
pdf_files = [f for f in os.listdir(folder_path) if f.endswith('.pdf')]
pdf_files.sort()
逐个添加到合并器
for pdf in pdf_files:
full_path = os.path.join(folder_path, pdf)
merger.append(full_path)
写出合并后的文件
output_path = os.path.join(folder_path, "合并结果.pdf")
merger.write(output_path)
merger.close()
print(f"合并完成,文件保存在:{output_path}")
跑完这段代码,你会发现“待合并”文件夹里多了一个“合并结果.pdf”,里面按顺序包含了所有文件的内容。
这段代码的逻辑很简单:PyPDF2.PdfMerger()创建了一个空容器,相当于一张白纸。append()方法把每个PDF一页一页地贴上去,就像在打印机上叠放纸张。最后write()把结果存下来。
文件顺序乱了怎么办
上面的代码用了sort()来排序,但这只能保证按字母顺序排列。如果文件名是“报告1”、“报告10”、“报告2”,sort()会把“报告10”排在“报告2”前面,因为字符串比较是一个字符一个字符来的。
解决办法有两种。
第一种,给文件命名的时候用数字编号,比如001报告、002报告。这样排序就是按数字顺序,不会乱。
第二种,自己指定顺序:
手动指定顺序
pdf_files = ["签到表.pdf", "小组报告.pdf", "财务凭证.pdf"]
for pdf in pdf_files:
full_path = os.path.join(folder_path, pdf)
if os.path.exists(full_path):
merger.append(full_path)
else:
print(f"警告:找不到文件 {pdf}")
加一个判断,文件不存在的时候跳过并给出提示,防止程序崩溃。
只合并某些页面
有时候你不需要合并整个PDF,只需要其中的几页。比如一个20页的报告,你只要第3到第7页。
PyPDF2允许指定页码范围:
只合并第3到第7页(页码从0开始)
merger.append("报告.pdf", pages=(2, 7))
这里的pages参数接收一个元组,第一个数字是起始页(从0开始),第二个数字是结束页(不包含这一页)。所以(2, 7)表示第3页到第7页,一共5页。
如果你想单独挑几页,可以用另一种方式:
先读取文件
reader = PyPDF2.PdfReader("报告.pdf")
只取第1页、第3页、第5页
pages_to_take = [0, 2, 4]
for page_num in pages_to_take:
merger.append(reader.pages[page_num])
这种方式灵活度更高,你想怎么组合都行。
处理子文件夹里的PDF
真实的场景往往更复杂。你的文件可能分散在不同的子文件夹里:财务组一个文件夹、技术组一个文件夹、销售组一个文件夹。你想把这些文件夹里的所有PDF都合并到一起。
这时候需要用os.walk来遍历文件夹树:
import PyPDF2
import os
merger = PyPDF2.PdfMerger()
root_folder = r"C:\Users\你的用户名\Desktop\各部门报告"
遍历所有子文件夹
for folder_path, subfolders, files in os.walk(root_folder):
for file in files:
if file.endswith('.pdf'):
full_path = os.path.join(folder_path, file)
merger.append(full_path)
print(f"已添加:{full_path}")
merger.write(os.path.join(root_folder, "全部合并.pdf"))
merger.close()
这段代码会从根文件夹开始,一层一层往下找,把所有子文件夹里的PDF都翻出来合并。注意一个问题:这样合并的顺序是按os.walk的遍历顺序来的,不是按文件夹名字排序。如果你需要控制顺序,可以在添加之前先收集所有文件路径,排序之后再添加。
加上书签和目录
合并后的PDF如果页数太多,翻起来很痛苦。加上书签会友好很多。
你可以把每个文件的文件名作为书签插入:
merger = PyPDF2.PdfMerger()
for pdf in pdf_files:
# 记录当前总页数,作为书签的起始位置
merger.append(pdf)
# 获取文件名(不含扩展名)作为书签名称
bookmark_name = os.path.splitext(pdf)[0]
# 在最后添加的书签位置插入书签
# 这里需要先获取当前总页数,PyPDF2的add_bookmark方法需要知道页码
PyPDF2添加书签的API稍微有点绕。更简单的方式是换一个库——pypdf(PyPDF2的现代替代品),它的书签功能更直观。
安装pypdf:
pip install pypdf
然后用pypdf合并并添加书签:
from pypdf import PdfWriter, PdfReader
writer = PdfWriter()
for pdf in pdf_files:
reader = PdfReader(pdf)
# 记录添加之前的页数
start_page = len(writer.pages)
# 添加所有页面
for page in reader.pages:
writer.add_page(page)
# 添加书签,指向这个文件的第一页
bookmark_name = os.path.splitext(pdf)[0]
writer.add_outline_item(bookmark_name, start_page)
with open("带书签的合并文件.pdf", "wb") as f:
writer.write(f)
这样生成的PDF,左侧书签栏里会列出每个原文件的文件名,点击就能跳转到对应位置。
处理加密的PDF
有些PDF设置了打开密码,直接合并会报错。如果你知道密码,可以在读取时解密:
reader = PyPDF2.PdfReader("加密文件.pdf")
if reader.is_encrypted:
reader.decrypt("你的密码")
然后把reader的页面添加到merger里。
如果密码不知道,那基本无解。PDF的加密算法是工业级的,暴力破解不现实。你需要先找文件提供方要密码,或者用专门工具移除密码——但移除密码也需要先输入密码。
大文件合并时的内存问题
合并几十个PDF、几百页文件,PyPDF2跑起来很快。但如果合并上千页的大文件,或者一次合并上百个PDF,可能会遇到内存不足。
PyPDF2是把所有内容加载到内存里再写入的。解决方案是用pypdf的增量写入模式,或者换用PDFtk这个命令行工具——它在底层是用C++实现的,处理大文件比Python高效得多。
如果你坚持用Python,可以这样优化:分批次合并,先合并成几个中间文件,再把中间文件合并成最终结果。
先每10个文件合并成一个临时文件
batch_size = 10
temp_files = []
for i in range(0, len(pdf_files), batch_size):
batch = pdf_files[i:i+batch_size]
temp_writer = PyPDF2.PdfMerger()
for pdf in batch:
temp_writer.append(os.path.join(folder_path, pdf))
tempname = f"temp{i}.pdf"
temp_writer.write(temp_name)
temp_writer.close()
temp_files.append(temp_name)
再合并所有临时文件
final_merger = PyPDF2.PdfMerger()
for temp in temp_files:
final_merger.append(temp)
final_merger.write("最终合并.pdf")
final_merger.close()
清理临时文件
import os
for temp in temp_files:
os.remove(temp)
这种分段合并的方式,内存占用会小很多。
给代码加一个图形界面
代码写好了,但每次运行都要改文件夹路径,同事想用又不会Python。这时候可以加一个简单的图形界面,用tkinter(Python自带的GUI库)实现。
import tkinter as tk
from tkinter import filedialog, messagebox
import PyPDF2
import os
def merge_pdfs():
# 让用户选择文件夹
folder = filedialog.askdirectory()
if not folder:
return
# 获取所有PDF文件
pdf_files = [f for f in os.listdir(folder) if f.lower().endswith('.pdf')]
if not pdf_files:
messagebox.showwarning("提示", "该文件夹中没有PDF文件")
return
pdf_files.sort()
# 合并
merger = PyPDF2.PdfMerger()
for pdf in pdf_files:
full_path = os.path.join(folder, pdf)
merger.append(full_path)
output_path = os.path.join(folder, "合并结果.pdf")
merger.write(output_path)
merger.close()
messagebox.showinfo("完成", f"合并完成!\n文件保存在:{output_path}")
创建窗口
root = tk.Tk()
root.title("PDF合并工具")
root.geometry("300x150")
btn = tk.Button(root, text="选择文件夹并合并PDF", command=merge_pdfs, height=3, width=25)
btn.pack(expand=True)
root.mainloop()
保存为pdf_merger_gui.py,双击运行,会弹出一个窗口,点按钮选择文件夹,剩下的交给程序。你的同事双击就能用,不需要安装Python环境吗?还是需要的——不过你可以用pyinstaller打包成一个exe文件,发给谁都能用。
处理扫描件和图片型PDF
有一种PDF比较特殊:里面不是文字,而是扫描的图片。这类PDF合并的时候,上面所有方法都适用,因为PyPDF2处理的是页面对象,不管里面是文字还是图片。
但合并之后可能会遇到一个问题——文件特别大。图片型PDF本来就大,合并之后更大。如果需要压缩,可以用另外的库,比如pypdf的压缩功能,或者用img2pdf重新生成。
简单压缩的方法:
from pypdf import PdfWriter, PdfReader
reader = PdfReader("大文件.pdf")
writer = PdfWriter()
for page in reader.pages:
# 压缩页面内容
page.compress_content_streams()
writer.add_page(page)
with open("压缩后.pdf", "wb") as f:
writer.write(f)
这个压缩力度有限,但聊胜于无。真正想大幅压缩图片型PDF,需要用OCR或专门的PDF压缩工具。
遇到报错怎么办
合并过程中最常见的报错是“PdfReadError: EOF marker not found”。意思是PDF文件可能损坏或不完整。解决办法是在append之前先验证文件是否能正常读取:
def is_valid_pdf(filepath):
try:
with open(filepath, 'rb') as f:
reader = PyPDF2.PdfReader(f)
# 尝试获取页数,如果出错说明文件有问题
_ = len(reader.pages)
return True
except:
return False
只添加有效的PDF
for pdf in pdf_files:
full_path = os.path.join(folder_path, pdf)
if is_valid_pdf(full_path):
merger.append(full_path)
else:
print(f"跳过无效文件:{pdf}")
另一个常见报错是“Permission denied”,表示文件被其他程序打开(比如你在Adobe Acrobat里正看着这个文件)。关掉文件再运行一次就行。
把合并过程写成日志
如果你要合并的文件很多,想记录哪些成功了、哪些失败了,可以加一个日志功能:
import logging
logging.basicConfig(filename='merge_log.txt', level=logging.INFO,
format='%(asctime)s - %(message)s')
for pdf in pdf_files:
try:
full_path = os.path.join(folder_path, pdf)
merger.append(full_path)
logging.info(f"成功添加:{pdf}")
except Exception as e:
logging.error(f"添加失败:{pdf},错误信息:{str(e)}")
跑完之后打开merge_log.txt,一目了然。
回到那个周一的早晨
现在你手头有了一段能用的Python代码。你双击运行,三秒钟之后,“合并结果.pdf”出现在文件夹里。你把它发到群里,领导回了一个大拇指。
更重要的是,下次再有同事遇到同样的问题,你不用再解释“你试试那个在线工具、注意不要点广告、合并完记得检查顺序”——直接把代码扔过去,或者打包成exe发给他。
PDF合并这件事,Python帮你做了一次性投入、无限次复用的自动化。那些浪费在下载软件、比较付费方案、担心文件泄露上的时间,都可以省下来了。