当前位置:首页>python>Python 压缩 PDF,别只盯着“压缩成功”

Python 压缩 PDF,别只盯着“压缩成功”

  • 2026-08-28 12:25:34
Python 压缩 PDF,别只盯着“压缩成功”
一个扫描版合同,原文件 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


deffind_ghostscript() -> str:
    candidates = ("gs", "gswin64c", "gswin32c")

for command in candidates:
        executable = shutil.which(command)
if executable:
return executable

raise RuntimeError(
"未找到 Ghostscript,请先安装并确认 gs 命令已加入环境变量"
    )


defreadable_size(byte_count: int) -> str:
    value = float(byte_count)

for unit in ("B", "KB", "MB", "GB"):
if value < 1024or unit == "GB":
returnf"{value:.2f}{unit}"
        value /= 1024

returnf"{byte_count}B"


defcompress_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()

ifnot 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 notin 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}")

ifnot 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 代码并不难。难的是别把“文件生成了”当成任务完成。

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

最新文章

随机文章