周一早上九点,你的邮箱里躺着十几封邮件,每封都带了一个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)) temp_name = 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帮你做了一次性投入、无限次复用的自动化。那些浪费在下载软件、比较付费方案、担心文件泄露上的时间,都可以省下来了。