Python 批量转 WebP 实战:透明图不发黑、大图先限宽、跑过一遍不再重复

Python 批量转 WebP 实战:透明图不发黑、大图先限宽、跑过一遍不再重复

背景:这篇解决什么问题

WebP 是 Google 推出的图片格式,在观感接近的前提下,文件通常比 JPEG 和 PNG 更小。对博客、相册这类图片多的场景,批量转成 WebP 是最直接的提速手段。

但直接写个三行循环转格式,很容易踩四个坑:带透明通道的 PNG 转完背景变黑;手机拍的照片方向错乱;同名不同格式的图片互相覆盖;已经转过的文件每次重跑又全转一遍。这篇给一个一次写好、能反复跑的脚本,把这四个坑一次性处理掉。

前置知识:会用命令行,会装 Python 包。不需要了解图片编码原理,脚本里的每个关键步骤下面都有说明。

准备:装 Pillow 并确认支持 WebP

Pillow 是 Python 最常用的图片处理库,WebP 编解码由它内置的 libwebp 支持提供。先安装,再用一行命令确认:

pip install pillow
python3 -c "from PIL import features; print(features.check('webp'))"

输出 True 才继续;如果是 False,说明当前 Pillow 没有编入 WebP 支持,换一个官方 wheel 或虚拟环境重装,不要硬着头皮往下跑——那样只会得到一堆报错。

完整脚本:to_webp.py

把下面这段存为 to_webp.py,放在图片文件夹的上一级。脚本只做四件事:递归扫描、按规则转换、体积变大时保留原图、最后打印统计。

import argparse
import io
from pathlib import Path

from PIL import Image, ImageOps

SUFFIXES = {".jpg", ".jpeg", ".png", ".bmp", ".tiff"}


def has_alpha(img):
    if img.mode in ("RGBA", "LA"):
        return True
    return img.mode == "P" and "transparency" in img.info


def convert_one(src, dst, quality, max_width):
    with Image.open(src) as img:
        img = ImageOps.exif_transpose(img)
        if img.width > max_width:
            height = round(img.height * max_width / img.width)
            img = img.resize((max_width, height), Image.LANCZOS)
        img = img.convert("RGBA" if has_alpha(img) else "RGB")
        buf = io.BytesIO()
        img.save(buf, "WEBP", quality=quality, method=6)
        data = buf.getvalue()
    if len(data) >= src.stat().st_size:
        return "larger"  # WebP 反而更大,留着原图更划算
    dst.parent.mkdir(parents=True, exist_ok=True)
    dst.write_bytes(data)
    return len(data)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--src", required=True, help="源图片文件夹")
    ap.add_argument("--dst", required=True, help="输出文件夹")
    ap.add_argument("--quality", type=int, default=80)
    ap.add_argument("--max-width", type=int, default=1600)
    ap.add_argument("--force", action="store_true", help="重新转换已存在的文件")
    args = ap.parse_args()

    src_root, dst_root = Path(args.src), Path(args.dst)
    claimed, done, skipped, larger = set(), 0, 0, 0
    for src in sorted(src_root.rglob("*")):
        if not src.is_file() or src.suffix.lower() not in SUFFIXES:
            continue
        rel = src.relative_to(src_root)
        dst = dst_root / rel.with_suffix(".webp")
        if dst in claimed:  # a.jpg 和 a.png 同名时,第二个改叫 a-png.webp
            dst = dst.with_name(f"{rel.stem}{src.suffix.lower()}.webp")
        claimed.add(dst)
        if dst.exists() and dst.stat().st_mtime >= src.stat().st_mtime and not args.force:
            skipped += 1
            continue
        result = convert_one(src, dst, args.quality, args.max_width)
        if result == "larger":
            larger += 1
            print(f"SKIP(larger) {src} -> 体积未减小,保留原图")
        else:
            done += 1
            old, new = src.stat().st_size, result
            print(f"OK {src} {old} -> {new} 字节(-{100 - new * 100 // old}%)")
    print(f"完成:转换 {done} 张,跳过已处理 {skipped} 张,体积未减小 {larger} 张")


if __name__ == "__main__":
    main()

运行方式:

python3 to_webp.py --src ./raw_photos --dst ./web_photos

脚本里每个关键步骤是干什么的

  • 先纠正方向:ImageOps.exif_transpose 会读取照片里的方向信息,把手机横拍、竖拍的照片转正。没有这一步,竖拍的照片在网页上可能横过来。
  • 透明通道单独处理:带透明通道的图片转成 RGBA,不带透明的转成 RGB。直接把所有图都转 RGB,就是透明 PNG 变黑底的元凶。
  • 超宽图先限宽:默认最宽 1600 像素,网页上几乎用不到更宽的图,先缩尺寸再压缩,体积收益比单纯调低画质更大。LANCZOS 是缩小图片时画质较好的重采样算法。
  • 体积变大就保留原图:小图标、纯色插画转 WebP 后可能反而变大,脚本先在内存里编好、比完大小再落盘,不会用更大的文件替换原图。
  • 重复跑自动跳过:目标文件已存在且比源文件新,就跳过不转。新增照片后重跑一遍,只处理新增的那几张;想全部重转时加 --force。

画质参数怎么选

图片类型quality 建议说明
手机照片、实拍图75–85默认 80 是个稳妥起点,低于 70 容易看出涂抹感
带文字的截图85–90文字边缘对压缩敏感,宁可文件稍大
纯色插画、图标80 左右体积变大时脚本会自动保留原图

先拿三五张代表性的图跑一遍,看体积变化和实际观感,再定全量参数。

常见问题

转完图片发灰、颜色变了? 多半是源图带 CMYK 或特殊色彩模式,脚本统一转成 RGB/RGBA 后颜色会有轻微变化,印刷用途的图不要用这个脚本,网页用途一般可以接受。

旧浏览器打不开 WebP 怎么办? 如今主流浏览器都已支持 WebP;要照顾极老浏览器时,可用 <picture> 标签同时提供 WebP 和 JPEG 两个来源。

原图要不要删? 不要删。脚本把输出写到另一个文件夹,源文件夹原样保留。万一哪天要换格式、调画质,重新跑一遍就行,不用再去找原图。

和这篇配套的另一篇 Python 实战是磁盘清理脚本:图片转完省下的空间是一次性的,日志和缓存的增长是持续的,两者搭配着用,磁盘才不容易悄悄写满。