
背景:这篇解决什么问题
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 实战是磁盘清理脚本:图片转完省下的空间是一次性的,日志和缓存的增长是持续的,两者搭配着用,磁盘才不容易悄悄写满。
