在日常办公、资料归档、邮件附件上传以及网盘同步这些场景里,PDF 体积过大是很多人都会遇到的一个痛点。一份文件动辄几十兆,不仅占空间,上传下载也慢;如果手里积压了成百上千个 PDF,一个一个用编辑器另存去压缩,工作量会非常可怕。本文以“批量压缩 PDF”为目标,从压缩原理讲起,给出命令行方案和 Python 脚本方案,并提供完整可复制的代码、运行步骤、常见报错排查和工程建议,适合有一定办公自动化需求的学生、HR、财务、档案管理员和开发者阅读。
先提醒一点:批量压缩和“批量降低图片画质”不是一回事。如果你只是粗暴地把 PDF 的每一页转成低分辨率图片再重新生成 PDF,页面上原本可选的文本会丢失,打印效果也会明显变差。正确的做法应该是有选择地去重采样图片、清理无用对象、压缩内容流,并保留文字层和基本结构。下面,我们围绕这套思路逐步展开。
1. 背景与核心概念
1.1 PDF 体积为什么越来越大
PDF 并不是一种“越存越小”的格式。很多 PDF 在生成时为了兼容性,会嵌入整套字体;通过 Word、WPS 或设计软件导出的 PDF 往往还保留了大量编辑信息;扫描仪生成的 PDF 更是直接把每一页保存为高分辨率图片。这些因素叠加在一起,文件体积就会快速膨胀。
从根本上说,PDF 是一个容器,它内部至少包含四类东西:
- 文本内容和字体:文本本身不大,但嵌入的完整字体文件可能有好几兆。
- 图片对象:扫描件、截图、商品图、流程图,这些是 PDF 体积的大头。
- 内容流:描述页面布局、绘制指令的代码,通常是可压缩的。
- 元数据和辅助对象:书签、缩略图、属性信息、冗余对象。
如果你的 PDF 是从 CAD、PS 或高分辨率扫描设备导出的,图片部分占比通常超过 90%。这时候,压缩 PDF 的核心问题,就变成了在“视觉可接受”和“文件体积”之间做平衡。
1.2 批量压缩的典型应用场景
批量压缩绝不是“把单个文件多处理几次”那么简单。常见的应用场景包括:
- 电子档案归档:大批量扫描件转存的 PDF 需要长期保存,体积过大会浪费存储空间。
- 邮件附件发送:多数邮箱附件限制在 20MB 到 50MB 之间,超大 PDF 需要降体积后才能发送。
- 网盘同步与知识库搭建:压缩后能显著减少上传时间,也便于检索和分享。
- 电子书整理:从网上下载或自行排版的 PDF 书刊,压缩后可存入阅读器,节省设备空间。
- 业务流程处理:比如人力资源部门批量处理简历、合同,财务部门批量归档电子发票。
这些场景里,如果只靠人工打开软件另存为,效率极低且容易漏处理。批量压缩的价值在于:一次写脚本,永久复用。
1.3 压缩不是简单降低分辨率
很多新手会对“压缩 PDF”产生一个误解:把每页导出成图片,缩小后再拼回 PDF。这种思路只适合打印质量要求极高且不需要文本层的场景。绝大多数办公场景下,我们要的是:
- 尽量保留文字和矢量信息的可编辑性;
- 尽量保留页面的排版效果;
- 对内嵌图片做适度重采样;
- 只删除冗余对象和无效元数据。
所以,批量压缩应该理解为“基于 PDF 内部结构优化”,而不是“把 PDF 当图片重绘”。
2. 环境准备与版本说明
PDF 压缩可以用不同技术栈实现:Java 、Python、命令行工具都可以。本文选择“Python + Ghostscript”的组合,原因是它跨平台、安装简单、脚本适合批量化,且 Ghostscript 的 PDF 优化引擎足够成熟,压缩效果明显。
2.1 推荐技术路线
推荐路线如下:
- 操作系统:Windows 10/11、macOS、Linux 均可,本文示例以通用命令说明为主。
- 脚本语言:Python 3.8 及以上版本,低版本请留意
pathlib和subprocess的兼容性。 - 压缩引擎:Ghostscript,这是开源的 PostScript 和 PDF 解释器,内置
pdfwrite输出设备,专门用于 PDF 重建和优化。 - Python 库:
pikepdf和PyMuPDF作为备选方案,用于兼容加密 PDF、清理无用对象等场景。
版本需要根据你的实际环境调整,本文重点是配置和代码思路。比如你使用的是 Python 3.12 或 3.13,依赖安装方式不变,但个别库的底层版本可能影响运行结果,建议先用小样本测试。
2.2 安装 Ghostscript
Ghostscript 的安装方式按系统区分:
- Windows:从官方渠道下载安装包(如 Ghostscript 的 Windows 安装程序),安装时可以选择“添加到系统 PATH”。如果没勾选,安装后在命令行中找不到
gswin64c,脚本就需要写绝对路径。 - macOS:执行
brew install ghostscript,安装后命令名一般是gs。 - Linux(Debian/Ubuntu):执行
sudo apt-get install ghostscript;CentOS/RHEL 上可以使用sudo yum install ghostscript。
安装完成后,在终端执行:
gs --version如果能输出版本号,说明安装成功。Windows 用户如果遇到“不是内部或外部命令”,说明 Ghostscript 没有加入 PATH,后面会单独说解决方法。
2.3 安装 Python 依赖
Python 侧建议新建一个虚拟环境,避免依赖冲突:
python -m venv pdf_compress_env激活环境后安装依赖:
# Windows pdf_compress_env\Scripts\activate # macOS / Linux source pdf_compress_env/bin/activate然后安装库:
pip install pikepdf pymupdf如果网络较慢,可以加国内镜像源:
pip install pikepdf pymupdf -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后,可以进入 Python 交互环境做一个导入验证:
import pikepdf import fitz print(pikepdf.__version__) print(fitz.__doc__)能正常打印版本信息,说明环境就绪。
3. 批量压缩的核心原理
3.1 PDF 里的体积来源分析
要写出真正有效的压缩脚本,首先要大概知道 PDF 的体积来源。文本 PDF 和扫描件 PDF 的压缩策略完全不同:
- 文本型 PDF(Word 导出、LaTeX 生成的论文、公众号排版导出的文档):体积主要来自嵌入字体和内容流,压缩重点在于字体子集化和流压缩,Ghostscript 能很好处理。
- 图片型 PDF(扫描仪生成、PS 导出、产品图册):体积主要来自内嵌图片,压缩重点在于图片重采样。如果图片是 300 DPI 的彩色扫描件,改成 150 DPI 往往体积能减少一半以上。
- 混合型 PDF:既有大量文字,又有插图、照片、封面图,需要综合处理。
如果你不确定 PDF 的体积类型,可以先开 Python 看一眼页面里的图片数量、颜色空间和分辨率,再决定压缩参数。
3.2 三种主流压缩手段
常见的批量压缩手段可以归纳为三类:
第一类:内容流压缩。PDF 内部的页面内容流可以使用 Flate 等算法压缩,compress_streams就属于这类。它适合对已经解压过的 PDF、或是由某些工具生成时未开启压缩的 PDF。这类方法不改变画面,无损程度最高,但压缩率有限。
第二类:图片重采样。把高分辨率的位图降到合理分辨率,比如从 300 DPI 降到 150 DPI,并统一图片颜色空间。这是大幅减小体积的最有效手段,但会造成一定的细节损失。
第三类:移除冗余对象。PDF 重新生成时,可以丢弃未引用的对象、过时的元数据、重复字体。garbage、clean参数就是做这一类清理。
一份成熟的压缩方案通常三管齐下。Ghostscript 就是通过组合这些策略,把原始 PDF 重建为一个更精简的新 PDF。
3.3 主流工具横向对比
下面这个表格能帮你理解三种常用工具各自的定位:
| 工具/库 | 定位 | 优点 | 局限 |
|---|---|---|---|
| Ghostscript | 命令行压缩引擎 | 压缩层度深,支持 DPI 重采样,批量处理稳定 | 需要额外安装,参数较多,不同版本参数细节有差异 |
| pikepdf | Python PDF 库 | 无损清理、压缩流,适合加密 PDF,API 清晰 | 不做图片重采样时体积下降有限 |
| PyMuPDF | Python PDF 库 | 可清理无用对象,支持渲染和图片操作 | 轻量级清理为主,复杂图像压缩需配合其他手段 |
在实际工程中,推荐把 Ghostscript 作为主压缩引擎,把 pikepdf 作为“打不过 Ghostscript 时”的兜底工具,比如遇到损坏 PDF 或加密 PDF,可以先用 pikepdf 尝试修复和重建。
4. 完整实战:命令行批量压缩 PDF
命令行方案适合临时处理、服务器环境或不需要写完整 Python 程序的场景。它的核心是 Ghostscript。
4.1 使用 Ghostscript 压缩单个 PDF
先看单个文件的压缩命令。假设输入文件是input.pdf,输出文件是output.pdf:
gs \ -sDEVICE=pdfwrite \ -dCompatibilityLevel=1.7 \ -dPDFSETTINGS=/ebook \ -dNOPAUSE \ -dQUIET \ -dBATCH \ -sOutputFile=output.pdf \ input.pdf参数含义如下:
-sDEVICE=pdfwrite:告诉 Ghostscript 使用 PDF 输出设备,目标是生成一个新的 PDF 文件。-dCompatibilityLevel=1.7:输出的 PDF 兼容 1.7 版本,绝大多数阅读器都支持。-dPDFSETTINGS=/ebook:选用“电子书”优化级别,适合普通办公文档。-dNOPAUSE、-dBATCH:不暂停、不交互,适合批量处理。-dQUIET:减少不必要的输出,日志更干净。
执行完成后,对比input.pdf和output.pdf的大小,就能看出压缩效果。
4.2 用批处理脚本压缩文件夹
如果只有几十个 PDF,可以用终端循环。Windows CMD 里可以写:
@echo off set INPUT_DIR=D:\pdf_input set OUTPUT_DIR=D:\pdf_output mkdir %OUTPUT_DIR% 2>nul for %%f in (%INPUT_DIR%\*.pdf) do ( gswin64c -sDEVICE=pdfwrite -dPDFSETTINGS=/ebook -dNOPAUSE -dBATCH -dQUIET -sOutputFile=%OUTPUT_DIR%\%%~nf.pdf %%f ) echo batch compress donemacOS 或 Linux 的 Bash 脚本可以写:
#!/bin/bash input_dir="pdf_input" output_dir="pdf_output" mkdir -p "$output_dir" for pdf in "$input_dir"/*.pdf; do name=$(basename "$pdf") gs -sDEVICE=pdfwrite -dPDFSETTINGS=/ebook -dNOPAUSE -dBATCH -dQUIET \ -sOutputFile="$output_dir/$name" "$pdf" done echo "batch compress done"命令行方案的优点是简单直接,缺点是:
- 无法自动跳过已经压缩过的文件;
- 无法自动记录每个文件的压缩率;
- 无法处理文件名带空格或者路径特殊符号时产生的小问题;
- 用户对整个流程的透明度不高。
所以当你打算把压缩归档变成一个稳健的生产工具时,应该转用 Python 脚本。
5. 完整实战:Python 批量压缩脚本
这一章是文章的核心。我会给出一个可以直接复制运行的 Python 脚本,它不仅能批量压缩 PDF,还会输出压缩日志,跳过不需要处理的文件,并在压缩失败时自动降级处理。
5.1 项目结构
建议按照下面的结构组织文件:
pdf_batch_compress/ ├── pdf_input/ # 放原始 PDF ├── pdf_output/ # 放压缩后的 PDF ├── logs/ # 保存运行日志 └── batch_pdf_compress.py提前把pdf_input、pdf_output、logs三个目录创建好。脚本会自动创建目录,但手动创建便于你理解文件路径关系。
5.2 核心代码:调用 Ghostscript 批量压缩
下面是一个完整的 Python 脚本,文件路径为pdf_batch_compress/batch_pdf_compress.py:
""" 批量压缩 PDF 脚本 依赖: Python 3.8+, Ghostscript 用法: python batch_pdf_compress.py \ --input pdf_input \ --output pdf_output \ --settings ebook \ --min-size-kb 100 \ --skip-existing """ import argparse import logging import shutil import subprocess import time from pathlib import Path logger = logging.getLogger("pdf_compress") # Ghostscript 四档预设 SETTINGS_MAP = { "screen": "/screen", # 屏幕预览,体积最小 "ebook": "/ebook", # 电子阅读与办公文档 "printer": "/printer", # 打印机分辨率 "prepress": "/prepress", # 印刷输出级,体积较大 } def find_ghostscript(): """查找系统中的 Ghostscript 可执行文件。""" candidates = ["gs", "gswin64c", "gswin32c"] for name in candidates: path = shutil.which(name) if path: return path raise FileNotFoundError( "未找到 Ghostscript,请先安装并配置 PATH," "或者修改脚本中 candidates 为绝对路径。" ) def compress_pdf( gs_path: str, input_pdf: Path, output_pdf: Path, settings: str = "/ebook", ) -> bool: """使用 Ghostscript 压缩单个 PDF。""" output_pdf.parent.mkdir(parents=True, exist_ok=True) cmd = [ gs_path, "-sDEVICE=pdfwrite", "-dCompatibilityLevel=1.7", f"-dPDFSETTINGS={settings}", "-dNOPAUSE", "-dBATCH", "-dQUIET", "-sOutputFile", str(output_pdf), str(input_pdf), ] # 捕获标准输出和标准错误,便于排查问题 result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: logger.error("压缩失败: %s", input_pdf.name) logger.error(result.stderr[-1500:] if result.stderr else "无错误信息") return False return output_pdf.exists() and output_pdf.stat().st_size > 0 def batch_compress( input_dir: Path, output_dir: Path, settings: str, min_size_kb: int, skip_existing: bool, use_pikepdf_fallback: bool, ) -> None: """批量遍历输入目录下的 PDF 并压缩。""" gs_path = find_ghostscript() # 只处理 PDF 文件 pdf_files = list(input_dir.rglob("*.pdf")) if not pdf_files: logger.warning("输入目录下没有找到 PDF 文件。") return logger.info("共发现 %d 个 PDF 文件", len(pdf_files)) success_count = 0 skip_count = 0 fail_count = 0 total_size_before = 0 total_size_after = 0 for index, input_pdf in enumerate(pdf_files, start=1): size_before = input_pdf.stat().st_size # 跳过过小文件 if size_before < min_size_kb * 1024: logger.info("[%d/%d] 跳过过小文件: %s (%.1f KB)", index, len(pdf_files), input_pdf.name, size_before / 1024) skip_count += 1 continue # 输出路径:保持相对目录结构是更稳妥的选择 relative_path = input_pdf.relative_to(input_dir) output_pdf = output_dir / relative_path if skip_existing and output_pdf.exists(): logger.info("[%d/%d] 跳过已存在文件: %s", index, len(pdf_files), output_pdf.name) skip_count += 1 continue logger.info("[%d/%d] 正在压缩: %s (%.1f MB)", index, len(pdf_files), input_pdf.name, size_before / 1024 / 1024) ok = compress_pdf(gs_path, input_pdf, output_pdf, settings) if not ok and use_pikepdf_fallback: # 如果是 Ghostscript 无法处理,则尝试用 pikepdf 兜底 ok = compress_with_pikepdf(input_pdf, output_pdf) if ok: size_after = output_pdf.stat().st_size ratio = (1 - size_after / size_before) * 100 if size_before else 0 total_size_before += size_before total_size_after += size_after success_count += 1 logger.info(" 压缩后: %.1f MB,压缩率 %.1f%%", size_after / 1024 / 1024, ratio) else: fail_count += 1 logger.info("处理完成: 成功 %d 个,跳过 %d 个,失败 %d 个", success_count, skip_count, fail_count) if total_size_before: logger.info("总体积从 %.1f MB 降至 %.1f MB,减少 %.1f%%", total_size_before / 1024 / 1024, total_size_after / 1024 / 1024, (1 - total_size_after / total_size_before) * 100) def compress_with_pikepdf(input_pdf: Path, output_pdf: Path) -> bool: """如果 Ghostscript 失败,使用 pikepdf 进行无损清理压缩。""" try: import pikepdf with pikepdf.open(input_pdf) as pdf: pdf.save( output_pdf, compress_streams=True, object_stream_mode=pikepdf.ObjectStreamMode.generate, ) return output_pdf.exists() and output_pdf.stat().st_size > 0 except Exception as exc: logger.error("pikepdf 兜底失败: %s", exc) return False def main(): parser = argparse.ArgumentParser(description="批量压缩 PDF 文件") parser.add_argument("--input", type=Path, default=Path("pdf_input"), help="输入 PDF 目录") parser.add_argument("--output", type=Path, default=Path("pdf_output"), help="输出 PDF 目录") parser.add_argument("--settings", type=str, default="ebook", choices=list(SETTINGS_MAP.keys()), help="压缩预设等级") parser.add_argument("--min-size-kb", type=int, default=100, help="小于该体积的文件跳过,单位 KB") parser.add_argument("--skip-existing", action="store_true", help="输出目录已有同名文件时跳过") parser.add_argument("--fallback", action="store_true", help="Ghostscript 失败时使用 pikepdf 兜底") args = parser.parse_args() # 日志配置:同时输出到控制台和文件 log_dir = Path("logs") log_dir.mkdir(exist_ok=True) log_file = log_dir / f"compress_{time.strftime('%Y%m%d_%H%M%S')}.log" logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[ logging.StreamHandler(), logging.FileHandler(log_file, encoding="utf-8"), ], ) settings_value = SETTINGS_MAP[args.settings] batch_compress( input_dir=args.input, output_dir=args.output, settings=settings_value, min_size_kb=args.min_size_kb, skip_existing=args.skip_existing, use_pikepdf_fallback=args.fallback, ) if __name__ == "__main__": main()这段代码中,以下几个设计点值得注意:
find_ghostscript()自动识别不同系统上的 Ghostscript 命令名称,避免 Windows 和 macOS/Linux 命令名不一致的问题。- 利用
subprocess.run(capture_output=True, text=True)捕获 Ghostscript 的标准输出和错误,方便后续排错。 - 输出路径使用
input_pdf.relative_to(input_dir)保留原有目录层级,适合多级文件夹批量处理。 - 增加
--min-size-kb参数,避免对体积很小的文件做无意义压缩。 - 增加
--skip-existing参数,重复运行脚本时不会重复处理已经生成的同名文件。 - 日志写入
logs目录,文件名带时间戳,方便审计。
5.3 备选方案:PyMuPDF 压缩
如果某些 PDF 不需要 Ghostscript 那样激进的重采样,可以单独写一个基于 PyMuPDF 的版本:
""" 另一个轻量方案:使用 PyMuPDF 清理无用对象并重新压缩内容流。 文件路径:pdf_batch_compress/compress_pymupdf.py """ from pathlib import Path import fitz def compress_with_pymupdf(input_pdf: Path, output_pdf: Path): doc = fitz.open(input_pdf) # garbage=4: 清除未引用的对象、去除重复对象 # deflate=True: 重新压缩内容流 # clean=True: 清理并规范化 PDF 结构 doc.save(output_pdf, garbage=4, deflate=True, clean=True) size_before = input_pdf.stat().st_size size_after = output_pdf.stat().st_size print(f"{input_pdf.name}: {size_before / 1024:.1f} KB -> {size_after / 1024:.1f} KB") doc.close() if __name__ == "__main__": input_dir = Path("pdf_input") output_dir = Path("pdf_output") output_dir.mkdir(exist_ok=True) for pdf_path in input_dir.glob("*.pdf"): out_path = output_dir / pdf_path.name compress_with_pymupdf(pdf_path, out_path)PyMuPDF 胜在轻量,不需要额外安装 Ghostscript,适合 PDF 文件本身结构比较规范、压缩需求不是特别高的场景。但对图片密集型 PDF,它的压缩率往往不如 Ghostscript 明显。
5.4 运行与验证
把需要压缩的 PDF 放入pdf_input,然后执行:
python batch_pdf_compress.py \ --input pdf_input \ --output pdf_output \ --settings ebook \ --min-size-kb 100 \ --skip-existing \ --fallback预期日志输出类似于:
2025-01-15 10:00:01,123 [INFO] 共发现 5 个 PDF 文件 2025-01-15 10:00:02,456 [INFO] [1/5] 正在压缩: 扫描合同.pdf (15.2 MB) 2025-01-15 10:00:04,789 [INFO] 压缩后: 4.3 MB,压缩率 71.7% 2025-01-15 10:00:05,012 [INFO] [2/5] 正在压缩: 产品手册.pdf (42.8 MB) 2025-01-15 10:00:07,345 [INFO] 压缩后: 18.1 MB,压缩率 57.7%注意,压缩率和具体文件内容强相关,不同文件的体积缩减比例可能差异很大,上面只是演示效果。
运行完,打开pdf_output里的文件,随机抽几页检查文字是否可选、图片是否可接受,再决定是否上调或下调压缩档位。
6. 进阶参数与质量平衡
6.1 四档 PDFSETTINGS 详解
Ghostscript 的-dPDFSETTINGS参数有四个常用预设,理解这些预设,才能在不同场景下选对档位:
/screen:最低质量,适合电脑屏幕预览、快速预览。图片会被压到 72 DPI 左右,体积最激进,但打印效果通常不可接受。/ebook:电子设备阅读,约 150 DPI,是多数办公文档的首选,兼顾清晰度和体积。/printer:面向打印机输出,约 300 DPI,适合需要打印但图片质量要求不极端的场景。/prepress:面向印刷出版,体积压缩有限,适合需要保真输出的场景。
如果你是财务或人力资源人员,需要把一堆合同、发票压缩后归档,/ebook足够。如果这些 PDF 之后可能被拿去打印、签章、作为正式材料,建议用/printer,避免因过度压缩影响法律效力或者可读性。
6.2 手动控制图片分辨率
预设档位不一定满足所有需求。某些扫描件只需要彩色 100 DPI、黑白 200 DPI,但/ebook会统一处理。你也可以手动增加参数:
gs \ -sDEVICE=pdfwrite \ -dPDFSETTINGS=/ebook \ -dColorImageResolution=120 \ -dGrayImageResolution=150 \ -dMonoImageResolution=200 \ -dColorImageDownsampleType=/Bicubic \ -dGrayImageDownsampleType=/Bicubic \ -dNOPAUSE \ -dBATCH \ -dQUIET \ -sOutputFile=output.pdf \ input.pdf参数说明:
-dColorImageResolution:彩色图片的目标分辨率。-dGrayImageResolution:灰度图片的目标分辨率。-dMonoImageResolution:黑白图片的目标分辨率。-dColorImageDownsampleType=/Bicubic:重采样算法,双三次插值比平均采样能保留更多细节。
使用手动分辨率参数时,建议先取 3 到 5 个代表性文件做测试,用肉眼确认文字清晰度,再批量处理。
6.3 处理加密 PDF 与元数据保留
批量压缩时经常会遇到加密 PDF。这里的“加密”要区分两种:
- 打开密码:打开文件就需要输入密码。
- 权限密码:可以打开,但禁止打印、复制、编辑。
对于有合法权限的文件,在pikepdf.open()时传入密码即可:
import pikepdf # 打开密码模式下,需要传 password with pikepdf.open("encrypted.pdf", password="123456") as pdf: pdf.save( "output.pdf", compress_streams=True, object_stream_mode=pikepdf.ObjectStreamMode.generate, )需要强调的是:处理加密 PDF 必须在拥有合法授权的前提下进行。切勿绕过他人设置的访问控制,或用这类技术处理没有权限的文件。
如果希望压缩后保留基础元数据,Ghostscript 默认会保留大部分文档信息。你也可以在脚本里追加-dPreserveMetadata=true参数,但要注意部分版本的实现差异。
7. 常见问题与排查思路
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
运行脚本时提示gs: command not found | Ghostscript 未安装,或不在 PATH 中 | 安装 Ghostscript;Windows 把安装目录加入 PATH;或脚本里写绝对路径 |
Windows 提示gswin64c 不是内部或外部命令 | 安装时未勾选加入 PATH | 手动配置系统环境变量,或使用shutil.which难以找到时直接改成绝对路径 |
| 压缩后文件反而变大 | 原 PDF 已经高度压缩;Ghostscript 重新嵌入了字体或重采样效果不明显 | 先统计原始 PDF 的类型;改用compress_streams=True这类无损清理方案,或降低 DPI |
| 压缩后字体变成方块或乱码 | 字体未正确嵌入 | 检查原 PDF 字体嵌入情况,Ghostscript 参数增加-dEmbedAllFonts=true -dSubsetFonts=true |
| 压缩后图片模糊明显 | 使用了/screen档位,或 DPI 设置过低 | 提升到/ebook或/printer,减小重采样强度 |
| 某个 PDF 直接压缩失败 | PDF 文件损坏、加密或结构异常 | 用pikepdf尝试打开并修复,或先转成标准 PDF 再压缩 |
| 批量处理中途中断 | 磁盘空间不足、权限不足 | 清理磁盘,确保输出目录可写,使用日志定位最后处理到哪个文件 |
| 输出文件包含多余空白页 | 原 PDF 有空白页或扫描件包含空白页 | 压缩前先做空白页检测,或用 PDF 编辑工具删除空白页 |
| 压缩后文本无法搜索 | 原 PDF 本来就是扫描图片,没有 OCR 文本层 | 这类文件压缩前可以考虑加 OCR,或保留原文件 |
排查这类问题有一个通用套路:先处理单文件,再处理批量。先用命令行压缩单个 PDF,观察是否成功;失败则查看标准错误输出的末尾部分,定位具体报错信息;成功后再跑到脚本里,排除“批量逻辑”和“单文件本身问题”两方面的原因。
8. 最佳实践与工程建议
8.1 保留原始文件,压缩结果先输出到新目录
批量压缩脚本默认把结果写入pdf_output,不会覆盖pdf_input中的原始文件。这是一个非常重要的安全边界。在实际生产中,建议不要一开始就原地覆盖文件,而是先保持“输入目录”和“输出目录”分离,等确认压缩效果、阅读体验、打印质量都符合预期后,再由人工决定是否替换原文件。
如果是需要长期运行的自动化流程,还可以把“原始文件”和“压缩文件”之间的对应关系记录成 CSV,包括文件名、大小、压缩率、时间戳,方便审计。
8.2 压缩前先做小样本验证
无论你使用 Ghostscript、pikepdf 还是 PyMuPDF,拿到一个新的 PDF 集合时都应该先抽 3 到 5 个代表性文件测试。代表性文件应该包含最小体积、最大体积、图片最多、文字最多的文件。确认压缩参数对这批文件有效之后,再放开批量运行。
这样做的原因很简单:PDF 是高度异构的格式,没有一种参数能通吃所有文件。只有通过小样本验证,才能避免几百个文件处理完后才发现图片质量不可接受。
8.3 记录日志,确保可重复执行
批量压缩脚本需要输出日志。我建议至少记录以下几点:
- 原始文件名和路径;
- 压缩前大小、压缩后大小、压缩率;
- 使用的 Ghostscript 参数;
- 处理是否成功;
- 失败时的错误信息。
日志目录最好按天或者按时间戳归档,防止多次运行相互覆盖。可重复执行是脚本工具与手工操作最大的区别。
8.4 性能优化:并行处理时的资源边界
批量压缩大量 PDF 时,如果机器 CPU 核心较多,并行处理可以显著提速。但要注意,Ghostscript 是 CPU 密集型进程,如果同时启动十几个实例,不仅 CPU 会占满,磁盘 I/O 也会成为瓶颈,反而可能拖慢整体速度,甚至导致系统卡顿。
一个相对稳妥的思路是:先用串行脚本跑通流程,统计每个文件的平均耗时;在 CPU 核数允许的情况下,再用concurrent.futures.ThreadPoolExecutor控制并发数,比如设置为 2 到 4,而不是无脑开满所有线程。
8.5 隐私与合规边界
很多 PDF 包含个人隐私、合同编号、身份证号、财务报表等信息。使用在线 PDF 压缩工具虽然方便,但文件会被上传到第三方服务器,存在泄密风险。对于敏感材料,强烈建议本地脚本处理,不要上传到不信任的在线平台。
另外,对加密 PDF、受限文档的压缩必须遵守合法授权和合规要求。如果没有处理权限,不要尝试绕过限制,也不要对他人资料做未授权操作。
8.6 把压缩纳入归档流程
从工程角度看,PDF 压缩不应该是一次性的“补作业”,而应该纳入归档流程。比如:
- 文件上传前自动压缩;
- 扫描件归档时统一使用
/ebook档位; - 每季度检查一次旧归档文件,对未压缩的 PDF 执行批量压缩;
- 将压缩脚本和参数写入运维手册或项目文档,保证后续接手的人能理解每个参数的作用。
这样,批量压缩就不再是一个临时脚本,而是稳定可维护的工具。
9. 总结与学习路线
本文围绕“如何批量压缩 PDF 文档”这一需求,讲解了 PDF 体积过大的原因、压缩的核心原理,以及三种可行方案。其中最推荐的做法是“Python 脚本调用 Ghostscript”,兼顾压缩率、批量处理能力和可维护性。脚本中包含了自动查找 Ghostscript、跳过小文件、跳过已存在文件、日志输出、pikepdf 兜底等设计,你可以直接复制使用,也可以按自己的目录结构微调。
读完这篇文章,你应该能够完成以下几件事:
- 判断一个 PDF 的体积来源是文本、图片还是混合型;
- 根据使用场景选择
/screen、/ebook、/printer或/prepress四档压缩参数; - 写出自己的批量压缩脚本,并妥善处理日志和异常;
- 排查常见问题,比如压缩后体积变大、字体乱码、工具找不到等。
下一步如果你想继续深入,建议学习三个方向:一是用pikepdf做更精细的 PDF 对象级分析,比如导出图片列表、统计每张图片分辨率;二是结合 OCR 技术,把扫描版 PDF 转换成可搜索文件同时压缩体积;三是把这些脚本封装成带界面的小工具,供团队里不熟悉命令行的同事使用。
批量压缩 PDF 本质上就是一个“分析文件构成、选择合适参数、批量执行、验证结果”的工程问题。动手跑一次脚本,比对压缩前后的文件质量和体积,你会很快找到最适合自己业务场景的参数组合。如果本文对你有帮助,可以先收藏备用,后续压缩其他 PDF 时可以直接套用。