
1. 为什么我放弃了在线PDF工具转而在本地搭了一套批量处理工作流先说一个场景。你是不是也遇到过这种情况手上有一批PDF要处理——几十份报价单要合并成一个文件发给领导一百多页的扫描合同要拆成单页存档或者一整年攒下来的电子发票要转成Excel交给财务。打开浏览器搜PDF批量处理跳出来一堆在线工具结果不是限页数就是限大小好不容易传上去转完下载一看水印比内容都大。我在这个坑里蹲了很长时间直到后来彻底换了一套思路所有批量PDF处理全部在本地完成用命令行和Python脚本解决。这篇文章就把我这套积累下来的工作流完整分享出来包括工具选型、核心命令、Python脚本以及那些实际操作中踩过的坑。先说清楚这套方案适合谁。如果你只是偶尔转一个PDF那在线工具随便用用就行但如果你和我一样经常面对几十上百个文件的批处理需求或者对文件内容有保密要求、不愿意把合同和发票传到别人的服务器上那本地工具链是唯一靠谱的选择。尤其是涉及大量文件时本地处理的效率优势完全是碾压级的。1.1 在线工具的三个隐形代价隐私、效率、批量能力很多人在评估在线PDF工具时只盯着能不能转这一个标准实际上手之后才发现代价全藏在使用过程里。第一个代价是隐私。你把自己的合同、身份证扫描件、工资单传到一个不知名网站上对方服务器上发生了什么你完全不知道。我的原则很简单内容敏感的文档绝不经过第三方服务器。这不是多疑是做这行久了的基本职业习惯。第二个代价是效率。在线工具的流程永远是上传-等待-下载一个文件可能只要几十秒但如果你有100个文件就要重复100次。有些网站支持批量上传但免费用户往往要排队高峰期等上几分钟很正常。更烦人的是很多工具会把批量任务拆成单个文件分别下载最终你还是要手工把这些文件整理一遍。第三个代价是批量能力。大多数在线工具只能做单一操作要么只转格式要么只合并要么只压缩。但实际办公场景里的需求往往是复合的——文件要先拆开、去掉某些页、再合并、压缩最后重命名。这种多步骤流水线操作在线工具基本做不到。1.2 本地命令行方案的真正优势可脚本化、可复现、不依赖网络本地方案的核心优势其实是把批量操作从手工劳动变成了程序逻辑。一个命令跑下去100个文件一次处理完期间你可以去干别的事回头直接拿结果。第二个优势是可复现性。同一个脚本这个月能用下个月也能用。你只需要维护一套工具链和脚本库以后任何重复性的PDF处理需求都是几分钟的事。在线工具做不到这一点——今天这个网站还能用明天可能域名就变了或者免费额度被砍了。第三个优势是稳定。本地处理不依赖网络也没有上传下载的带宽损耗。处理100个10MB的文件在线方案要上传1000MB再下载1000MB本地方案只是磁盘读写和CPU计算时间差通常在十倍以上。我现在的处理环境很简单一台日常用的电脑装上qpdf、pdfcpu、PyMuPDF、OCRmyPDF这几个开源工具配合一些自己写的Python脚本就能覆盖日常90%以上的需求。下面逐个说清楚。2. 工具选择四个开源工具各管一摊各司其职很多人问过我怎么选工具我的答案是不要找一个万能工具而是让每个工具做它最擅长的事。这套组合里的每一个工具都是在各自领域被验证过很多年的成熟方案组合在一起几乎没有死角。2.1 qpdf合并拆分与加密的瑞士军刀qpdf是一个命令行工具也是整个工作流的基石。它的定位是PDF结构层面的操作合并、拆分、旋转、加密、解密、提取页面都很擅长。它最大的优点是你不用写代码一条命令解决问题适合快速操作和嵌入脚本。安装很简单Linux和macOS用户一般直接用包管理器装Windows用户可以从GitHub仓库下载可执行文件或者如果你用Scoop/Chocolatey也可以直接命令行安装。我日常用的最多的几个功能合并把多个PDF按指定顺序拼成一个文件拆分按页码范围提取内容解密去掉那些自己设置的打开密码加密给文件加一把锁再发给别人2.2 pdfcpu跨平台批量处理的补充pdfcpu是一个用Go写的工具单个可执行文件跨平台支持非常好不需要运行时环境。它在批量处理上的表现比qpdf更灵活一些尤其是批处理多个文件、批量加水印、批量设置元数据这些场景语法更友好。我在工作流里主要用pdfcpu处理两类需求一是给一批PDF批量添加页眉页脚或水印二是批量检查PDF文件是否损坏或符合标准。它和qpdf不是竞争关系而是互补——qpdf擅长精细操作pdfcpu擅长批量操作。2.3 PyMuPDFPython生态里的PDF操作核心PyMuPDFfitz是我最依赖的库几乎所有的复杂操作都是用它完成的。它基于MuPDF解析和渲染PDF的速度非常快API也很直观。安装一行命令就行pip install PyMuPDF导入后命名为fitz用起来是Python风格而非PDF底层术语风格。它真正强的地方在于可以精确读取每一页的文本、图片、链接、书签可以把PDF页面渲染成图片做缩略图或截图可以重写PDF比如删页、插页、替换页面内容完全在内存中处理长文件不会像有些工具一样先转成临时文件对我来说只要需求稍微复杂一点比如找出PDF里所有包含某个关键字的页面并提取出来qpdf和pdfcpu就很难做但PyMuPDF写十几行代码就行。2.4 OCRmyPDF把扫描件变成可搜索文字的PDF最后一个工具是OCRmyPDF。它解决的是扫描件的痛点你用手机拍了一堆纸质文件或者公司扫描仪扫出来的是纯图片型PDF里面的文字没法选中、没法搜索、没法复制。OCRmyPDF做的事情就是在原有PDF基础上加一层文字层视觉内容不变但底层有了可搜索、可复制的文本。它的底层依赖Tesseract OCR引擎支持中文识别配合语言包效果不错。处理完的PDF文件大小通常会比原文件大一些但文字内容可以检索了这个得失我觉得完全值得。这套组合的最终效果就是简单操作用命令行复杂操作用Python批量操作全部脚本化。下面进入实操环节。3. 高频场景的批量处理实操命令与脚本都能直接抄这一部分我尽量把场景写细命令和脚本都可以直接复制去用。我不会只给代码还会说明每个参数的含义和适用边界。3.1 批量合并几种场景下的不同写法合并PDF是最高频的需求但不同场景下的处理方式不一样我拆成三种来说。场景一按文件名字典序合并比如一堆扫描件命名为scan_01.pdf、scan_02.pdf……要按顺序合并成一个文件。直接用qpdfqpdf --empty --pages *.pdf -- out.pdf这个命令的含义是从零开始把当前目录下所有PDF文件按通配符展开的顺序合并输出到out.pdf。需要注意*.pdf的展开顺序是Shell决定的一般就是文件名字典序。如果你的文件名是1.pdf、2.pdf、10.pdf字典序会变成1、10、2这时候需要先重命名成01.pdf、02.pdf这种固定位数的格式或者用下面的Python脚本按自然排序处理。场景二按指定顺序合并某些具体文件如果只要合并其中几份比如a.pdf、c.pdf、b.pdf按这个顺序qpdf --empty --pages a.pdf c.pdf b.pdf -- merged.pdf--pages后面按顺序写文件名就行每个文件默认全部页面都被纳入。场景三合并时顺便把每个文件的页码标注出来这个需求经常出现在做标书、汇总结案材料的时候希望每个子文件从新的一页开始并且保留各自的内部页数。qpdf的--pages后可以加z参数意为最终PDF的第z页我不常用这个方式更灵活的做法是直接用PyMuPDF控制插入位置import fitz def merge_pdf_with_bookmarks(file_list, output): result fitz.open() for idx, f in enumerate(file_list, start1): src fitz.open(f) result.insert_pdf(src) # 在每个源文件开头位置添加书签方便跳转 toc result.get_toc() toc.append([1, f文档{idx}, result.page_count - src.page_count 1]) result.set_toc(toc) src.close() result.save(output) merge_pdf_with_bookmarks([a.pdf, b.pdf, c.pdf], merged.pdf)这段代码不仅合并了文件还自动为每份源文件生成了书签最终PDF的目录里可以直接跳转到对应文档开头。实测在几百页的标书场景下非常好用。3.2 批量拆分按页数、按大小、按书签拆分一般有三种需求逻辑按固定页数、按固定大小、按书签结构。分开说。按固定页数拆分比如把一个200页的PDF拆成每20页一份共10份。用qpdf配合循环for start in $(seq 1 20 181); do end$((start 19)) if [ $end -gt 200 ]; then end200; fi qpdf --pages input.pdf $start-$end -- part_${start}.pdf done这段Shell脚本里seq 1 20 181生成1、21、41……这些起始页码每次取20页最后一份自动截到尾页。按文件大小拆分比如某个PDF太大邮件发不出去想拆成每个≤5MB的块。这个用命令行不太好判断页数我一般用PyMuPDF先渲染每页并按大小估算或者更简单的方式先按页数粗拆再用压缩命令处理。后面压缩部分会细说。按书签结构拆分如果PDF自带书签目录按章节拆是最合理的比如一本培训教材按每一章拆成独立文件。PyMuPDF可以读取书签结构并据此拆分import fitz def split_by_toc(input_pdf, output_dir): doc fitz.open(input_pdf) toc doc.get_toc() for level, title, page in toc: if level 1: # 只处理一级章节 # 找到下一个一级书签的起始页 next_pages [p for lv, t, p in toc if lv 1 and p page] end_page min(next_pages) - 1 if next_pages else doc.page_count new_doc fitz.open() new_doc.insert_pdf(doc, from_pagepage-1, to_pageend_page-1) safe_title title.replace(/, _).replace(\\, _) new_doc.save(f{output_dir}/{safe_title}.pdf) new_doc.close() doc.close() split_by_toc(manual.pdf, ./split)这个脚本会沿着PDF的目录结构把每个一级章节的内容单独存成一个PDF文件文件名直接取章节标题。注意页码那里有个-1的偏移因为PyMuPDF的页码从0开始计数而TOC里记录的是从1开始的页码这是最容易出错的地方。3.3 PDF转Word选对工具转出来才能编辑关于PDF转Word先说一个很多人不知道的事实市面上绝大多数工具转出来的Word本质上是把PDF页面变成图片再塞进Word里看起来能打开实际上一个字都不能改。判断方法很简单转换完成之后用Word打开文件试着选中一段文字如果选不中、一选就是一整块图那说明是图片型结果。我推荐的方式分两层第一层如果你的PDF本身就是电子版文字可选中、可复制直接用开源的pandoc或LibreOffice就可以高质量转换不需要花哨的工具。用LibreOffice的命令行模式转soffice --headless --convert-to docx input.pdf这个命令会把整个目录下的PDF批量转成Word输出文件名不变扩展名改成.docx。LibreOffice在处理排版简单的文档时效果很好表格和标题基本能保留结构。第二层如果PDF是扫描件直接转Word是没意义的必须先走OCR。正确的流程是先用OCRmyPDF给扫描件加文字层再转Word这样Word里就是真正的可编辑文本。流程示例ocrmypdf --language chi_sim --output-type pdf scan.pdf ocr_scan.pdf soffice --headless --convert-to docx ocr_scan.pdf这里--language chi_sim指定简体中文语言包。识别质量取决于原始扫描件的清晰度杂乱的背景和模糊的字迹会明显影响效果所以扫描阶段尽量用300dpi以上分辨率别怕文件大。3.4 批量加页码、水印、压缩办公楼里的真实需求加页码、加水印、压缩这三件事几乎是每个行政或项目岗都绕不开的。分开讲。批量加页脚页码用PyMuPDF给每个页面底部居中位置写入页码最灵活import fitz def add_page_numbers(input_pdf, output_pdf): doc fitz.open(input_pdf) page_count doc.page_count for i, page in enumerate(doc, start1): # 在页面底部居中位置插入文字 page_width page.rect.width footer_y page.rect.height - 30 text f{i} / {page_count} # 插入前先计算文字宽度使文字水平居中 tw fitz.get_text_length(text, fontnamehelv, fontsize10) page.insert_text((page_width / 2 - tw / 2, footer_y), text, fontnamehelv, fontsize10, color(0.2, 0.2, 0.2)) doc.save(output_pdf) add_page_numbers(original.pdf, numbered.pdf)这个脚本用了fitz.get_text_length来计算文字宽度确保页码水平居中而不是固定x坐标。颜色用的是深灰色打印出来不突兀。批量处理就是把这段代码放进一个循环遍历整个目录。批量加水印pdfcpu处理水印非常顺手。文字水印pdfcpu stamp add -p CONFIDENTIAL -font Helvetica-Bold -size 48 -opacity 0.3 input.pdf output.pdf图片水印比如公司logopdfcpu stamp add -p logo.png -pos center -opacity 0.2 input.pdf output.pdf-pos center可以换成top-left、bottom-right等位置。批量处理时把多个文件路径一次传给pdfcpu stamp add即可它会逐个处理。批量压缩压缩PDF是最容易被误解的操作。先说原理PDF压缩主要靠两件事一是压缩或重采样里面的图片二是剔除冗余结构。对纯文字型PDF压缩空间很小因为文字信息本身已经很小了对扫描件或带大图的PDF压缩空间很大但也伴随着清晰度下降。我用的是Ghostscript的命令行压缩这是我自己实测过可控性最好的方案gs -sDEVICEpdfwrite -dCompatibilityLevel1.4 \ -dPDFSETTINGS/ebook -dNOPAUSE -dQUIET -dBATCH \ -sOutputFilecompressed.pdf input.pdf-dPDFSETTINGS有三个常用档位/screen最低质量适合屏幕预览、/ebook中等质量适合一般阅读和邮件发送、/printer高质量适合打印。实际工作中我一般日用选/ebook文件能缩小到原来的三分之一左右。批量压缩一批文件时写个循环mkdir -p compressed for f in *.pdf; do gs -sDEVICEpdfwrite -dCompatibilityLevel1.4 \ -dPDFSETTINGS/ebook -dNOPAUSE -dQUIET -dBATCH \ -sOutputFilecompressed/${f%.pdf}_c.pdf $f done注意Ghostscript会把文件名里的空格当成参数分隔符所以${f%.pdf}_c.pdf和$f一定要加引号。这个坑我踩过好几次。4. 批量处理最容易翻车的细节我踩过的坑和验证方法这一部分是全文里我觉得最有价值的。工具用法网上到处都有但这些坑绝大多数是实际操作中才会碰到的写出来免得你重走一遍。4.1 表格识别转Word之后版面全乱的问题PDF转Word最大的坑是表格。LibreOffice和部分商用工具对表格型PDF的处理结果往往惨不忍睹——行对不齐、列错位、数字串行。原因在于PDF本质上是一个排版容器它记录的只是每个字符的坐标位置并不像HTML或Word那样有表格结构的概念。转写工具只能通过字符坐标去猜测哪里是表头、哪里是单元格边界猜错的概率在复杂表格上非常高。我的建议是如果是带表格的PDF转Word做好人工校对的心理准备。实际操作中可以分两步走先用工具批量转换再用脚本抽出身上的关键字段做验证。比如转完之后用Python的python-docx库打开Word检查里面是否还保留着预期的标题文字或特定数值抽几个关键位置验证一下。还有一种更稳的思路如果表格是财务或数据用途直接用Python提取表格内容存成Excel比转成Word折腾版面更符合实际需求。PyMuPDF的page.find_tables()方法可以直接识别页面上的表格结构import fitz def extract_tables(input_pdf, output_excel_pathNone): doc fitz.open(input_pdf) all_tables [] for page in doc: tabs page.find_tables() for tab in tabs.tables: all_tables.append(tab.extract()) doc.close() # 如果需要写Excel可以配合openpyxl或pandas处理 return all_tables tables extract_tables(report.pdf) print(tables[0]) # 第一页的第一个表格数据这个功能用来从报表类PDF里扒数据比肉眼录入快得多。4.2 压缩率的预期管理压到什么程度才叫成功很多人用Ghostscript压完发现文件没小多少就开始怀疑命令写错了。其实不是而是PDF的内容类型决定了压缩上限。纯文字型PDF一页的文字信息很少压缩率能到90%就已经很好但如果是扫描版PDF图片占了大头压缩率取决于图片本身的分辨率和内容复杂度。判断压缩效果是否正常的办法是处理前后分别用pdfinfopoppler-utils包里的工具查看文件信息重点看页面尺寸、分辨率和元数据如果页面尺寸没变、内容没缺失只是体积变小那就是正常状态。还有个容易被忽视的点CMYK颜色的扫描件压出来的体积通常比RGB大转成灰度模式可以进一步缩小。需要海报精度的就保留彩色普通归档用灰度完全足够体积能再砍一半。4.3 加密与乱码处理PDF时的隐蔽陷阱批量处理PDF时最隐蔽的坑是加密文件。有些PDF虽然能打开但设置有限制编辑限制打印之类的权限标记。这种文件直接传给qpdf或PyMuPDF处理会报权限错误或生成空白页。更隐蔽的是某些企业级PDF打开时不需要密码但内部的字体子集是加密的直接提取文本会出现乱码或空白。处理思路分两步。先检测再处理qpdf --show-encryption input.pdf如果输出提示有密码或权限限制先用下面命令去掉限制前提是你本来就能打开这个文件qpdf --decrypt input.pdf output.pdf--decrypt会把PDF里的加密信息去除生成的output.pdf就可以正常被其他工具处理了。注意这一步只是去掉复制编辑限制不是破解文件的打开密码如果你真的连打开密码都不知道那我也没办法。字体乱码的问题一般出现在Linux环境下直接处理Windows生成的PDF。如果转换后文字变成方框或乱码检查系统里有没有安装PDF内嵌字体对应的字体族。安装中文字体fonts-noto-cjk之类的包能解决大部分问题。4.4 批量重命名文件管理的最后一道工序批量处理完文件很多人的习惯是_output.pdf_final.pdf这种命名等到一个月后再看完全分不清哪个是哪个。我有一套自己做批量处理的固定命名规则结构项目名_内容类型_版本号_日期.pdf例2025年度合同_合并版_v02_20250115.pdf禁止使用无法追溯的名称比如1234.pdf、新建文档.pdf在批量处理流程里重命名这一步我用Python加pathlib库来做它比正则表达式拼接更易读import pathlib def batch_rename(directory, prefix, ext.pdf): p pathlib.Path(directory) for i, f in enumerate(p.glob(f*{ext}), start1): new_name f{prefix}_{i:03d}{ext} f.rename(f.with_name(new_name)) batch_rename(./output, 合同_扫描件)i:03d会把数字补成三位保证排序正确。这个脚本的好处是改名规则都集中在变量里不会出现一边操作一边想命名的混乱状态。5. 把批量处理变成一键操作自动化延伸的三种玩法工具和脚本是基础但如果每次都手动打开终端敲命令那还算不上神仙工具。真正的效率提升在于把整套流程自动化到这个程度你把文件扔进某个文件夹剩下的自动完成。5.1 监视文件夹方案放进去就自动处理这是我用的最多的方式。思路是用一个后台脚本持续监视某个文件夹一旦有新文件出现自动按规则处理并移动到输出目录。Windows上用Python的watchdog库macOS和Linux上用watchdog同样适用。示例监视~/PDF_Inbox文件夹发现新的PDF就自动合并到merged.pdf并压缩输出import time import shutil from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler import subprocess import pathlib class PDFHandler(FileSystemEventHandler): def on_created(self, event): if event.src_path.endswith(.pdf): time.sleep(1) # 等待文件写入完成 process_pdf(event.src_path) def process_pdf(file_path): # 这里可以写你自己的处理逻辑比如压缩、加水印、移动位置 out_dir pathlib.Path(~/PDF_Output).expanduser() cmd fgs -sDEVICEpdfwrite -dPDFSETTINGS/ebook -dNOPAUSE -dQUIET -dBATCH -sOutputFile{out_dir}/{pathlib.Path(file_path).name} {file_path} subprocess.run(cmd, shellTrue) # 处理完的源文件归档 shutil.move(file_path, f{file_path}.done.pdf) if __name__ __main__: event_handler PDFHandler() observer Observer() observer.schedule(event_handler, pathstr(pathlib.Path(~/PDF_Inbox).expanduser())) observer.start() try: while True: time.sleep(1) except KeyboardInterrupt: observer.stop() observer.join()这段脚本的关键在于on_created事件只触发一次而大文件写入需要时间所以加了sleep(1)等写入完成否则打开文件时会读到不完整的数据。生产环境建议用on_modified事件配合文件大小稳定性判断逻辑更严密。5.2 右键菜单集成选中文件直接调用脚本不打开终端直接在文件管理器里右键选中一批PDF点一下菜单项就完成处理。Windows下可以用注册表加右键菜单macOS用Automator或快捷指令Linux桌面环境各不同。以Windows为例把Python脚本注册成右键菜单项echo off reg add HKEY_CLASSES_ROOT\*.pdf\shell\压缩PDF /ve /d 压缩并加水印 /f reg add HKEY_CLASSES_ROOT\*.pdf\shell\压缩PDF\command /ve /d \C:\Python39\python.exe\ \D:\scripts\pdf_compress.py\ \%1\ /f这样选中任意PDF文件右键菜单里就会出现压缩并加水印的选项。脚本内部可以接收命令行参数sys.argv[1]代表选中的文件路径执行压缩加水印的操作。5.3 定时批量任务夜间自动压缩归档第三种玩法适合固定的、重复性的归档需求。比如每天晚上11点自动把某个目录下的PDF全部压缩并移到归档文件夹。Windows可以用任务计划程序macOS和Linux用cron或launchd。cron配置示例每天23点执行0 23 * * * /usr/local/bin/python3 /home/user/scripts/nightly_pdf_archive.py脚本里做的事很简单遍历指定目录 → 压缩所有PDF → 按日期生成子目录 → 移动文件 → 写入日志。这样你早上到公司打开文件夹所有文件已经按日期归档好压缩也完成了。不过这里要提醒一句自动化跑挂了要能发现。建议脚本里加一个简单的日志和邮件或桌面通知功能不然某天命令因路径变动失败你会连续好几天都在用旧文件而不自知直到某次需要时才惊觉。其实工具套路的最后一步是沉淀自己的脚本库用这套工作流处理PDF已经快两年了最大的感受不是能处理和不能处理的差别而是处理一个文件和处理一百个文件对我来讲已经没有区别了。每多一个需求我就写一段脚本、录一个命令沉淀在自己的工具库目录里。下一次遇到同类型需求只需要改几个路径和参数就能复用。最后分享一个小技巧给所有脚本加一个统一的--dry-run参数预演时不实际修改文件只打印将要执行什么操作。很多批量脚本就是这么救回来的——你永远不希望在一个命令失误之后才发现200个文件已经被改名改得找不回来了。稳定压倒一切批量处理尤其如此。