一个扫描版合同,原文件 38MB,邮件附件限制 20MB。

我第一次拿 Python 处理时,脚本运行正常,输出文件也生成了。再看文件大小:37.6MB。

这类结果最烦。代码没报错,PDF 也能打开,但基本等于没压。

PDF 不是普通文本文件,里面可能同时塞着文字、字体、矢量图、JPEG 图片和扫描页。单纯重新保存一遍,通常只能清掉少量冗余对象。真正占空间的,十有八九还是图片。

我现在处理这类需求,一般让 Python 负责参数校验、批量执行和结果统计,真正的重新采样交给 Ghostscript。这个组合不算花哨,但稳定。

先安装 Ghostscript。

Linux 可以直接执行:

sudo apt install ghostscript

macOS:

brew install ghostscript

Windows 安装后,确认命令行能找到 gswin64c.exe。这一步别跳,很多所谓的 Python 压缩失败,最后只是系统环境变量没配。

先写一个能实际放进脚本目录里用的版本:

from __future__ import annotations
import shutil
import subprocess
from pathlib import Path

def find_ghostscript() -> str:
   candidates = ("gs", "gswin64c", "gswin32c")
   for command in candidates:
       executable = shutil.which(command)
       if executable:
           return executable
   raise RuntimeError(
       "未找到 Ghostscript,请先安装并确认 gs 命令已加入环境变量"
   )

def readable_size(byte_count: int) -> str:
   value = float(byte_count)
   for unit in ("B", "KB", "MB", "GB"):
       if value < 1024 or unit == "GB":
           return f"{value:.2f}{unit}"
       value /= 1024
   return f"{byte_count}B"

def compress_pdf(
   source_file: str | Path,
   target_file: str | Path,
   quality: str = "ebook",
) -> None:
   source = Path(source_file).expanduser().resolve()
   target = Path(target_file).expanduser().resolve()
   if not source.is_file():
       raise FileNotFoundError(f"输入文件不存在:{source}")
   if source.suffix.lower() != ".pdf":
       raise ValueError(f"只处理 PDF 文件:{source.name}")
   if source == target:
       raise ValueError("输出文件不能覆盖原文件,先保留一份退路")
   presets = {
       "screen": "/screen",
       "ebook": "/ebook",
       "printer": "/printer",
       "prepress": "/prepress",
   }
   if quality not in presets:
       raise ValueError(f"不支持的压缩等级:{quality}")
   target.parent.mkdir(parents=True, exist_ok=True)
   gs = find_ghostscript()
   command = [
       gs,
       "-sDEVICE=pdfwrite",
       "-dCompatibilityLevel=1.4",
       f"-dPDFSETTINGS={presets[quality]}",
       "-dNOPAUSE",
       "-dQUIET",
       "-dBATCH",
       "-dDetectDuplicateImages=true",
       "-dCompressFonts=true",
       f"-sOutputFile={target}",
       str(source),
   ]
   result = subprocess.run(
       command,
       capture_output=True,
       text=True,
       timeout=300,
   )
   if result.returncode != 0:
       target.unlink(missing_ok=True)
       error = result.stderr.strip() or "Ghostscript 未返回错误详情"
       raise RuntimeError(f"PDF 压缩失败:{error}")
   if not target.is_file() or target.stat().st_size == 0:
       raise RuntimeError("命令执行结束,但输出文件为空")
   before = source.stat().st_size
   after = target.stat().st_size
   reduced = (1 - after / before) * 100
   print(f"原文件:{readable_size(before)}")
   print(f"新文件:{readable_size(after)}")
   print(f"缩小比例:{reduced:.1f}%")
   if after >= before:
       print("压缩后没有变小,这个 PDF 可能已经做过图片压缩")

if __name__ == "__main__":
   compress_pdf(
       source_file="合同扫描件.pdf",
       target_file="合同扫描件_压缩版.pdf",
       quality="ebook",
   )

这里我默认用 ebook,不是因为它名字好听,而是它在文件大小和可读性之间相对稳妥。

几个等级大概这样选:

screen    文件最小,图片损失明显,适合临时预览
ebook     日常发送、上传系统,通常先试这个
printer   更偏向打印质量,压缩幅度会小一些
prepress  保留质量优先,不适合单纯追求小文件

我不建议一上来就用 screen。身份证、合同小字、发票编号这些内容,一旦压糊了,文件再小也没用。压缩不是只看最后那行“缩小 80%”,至少要抽查首页、印章页和文字最密集的页面。

批量处理也不用重新写一套逻辑:

from pathlib import Path
source_dir = Path("待压缩")
target_dir = Path("压缩结果")
for pdf_file in source_dir.glob("*.pdf"):
   output_file = target_dir / f"{pdf_file.stem}_small.pdf"
   try:
       compress_pdf(pdf_file, output_file, quality="ebook")
   except Exception as exc:
       print(f"[失败] {pdf_file.name}:{exc}")
   else:
       print(f"[完成] {pdf_file.name}")

实际跑批量任务时,我还会盯两个地方。

一个是超时。异常 PDF、超大扫描件或者损坏文件,可能长时间卡住,所以代码里加了 timeout=300。五分钟还没处理完,先停下来查文件,别让一个坏文件堵住整个任务。

另一个是压缩后的体积。文字型 PDF 本身就不大,压缩比例可能很低;扫描版 PDF 每页都是大图,通常才有明显空间。要是输出文件反而更大,不要急着继续调参数,很可能原文件已经优化过,再编码一次只是在折腾。

还有个坑容易漏:加密 PDF。

碰到设置了权限或密码的文件,Ghostscript 可能直接失败。脚本里不要把异常吞掉,更不要生成一个空文件还当成功。保留错误日志和原文件,比强行输出重要。

PDF 压缩这件事,Python 代码并不难。难的是别把“文件生成了”当成任务完成。

我一般只认三个结果:文件能打开,关键页面看得清,体积确实变小。缺一个,这次压缩都不算完。

以上就是“Python 压缩 PDF,别只盯着“压缩成功”!”的详细内容