直接用pillow逐张处理上万张图片易致内存溢出、文件覆盖或进程卡死,需用processpoolexecutor控制并发、添加内存释放与文件安全校验。

直接用 Pillow 逐张处理上万张图片,大概率会内存爆掉、进程卡死,或者重命名冲突——这不是脚本写得不对,而是没做批量控制和文件安全校验。
为什么不能直接 for 循环调用 Image.save()
常见错误是写个 for 遍历所有 .jpg 文件,打开、缩放、保存。问题在于:
-
Pillow默认不释放图像内存,上万次Image.open()后内存持续上涨,Linux 下可能被 OOM killer 杀掉 - 原图和目标路径混用(比如输出目录没提前创建),
save()报FileNotFoundError但脚本继续跑,最后发现几百张图“消失”了 - 重命名依赖
os.path.basename()或简单计数器,遇到同名但大小写不同(IMG_001.jpg和img_001.JPG)时在 macOS/Linux 下会覆盖 - 没设超时或异常跳过,某张损坏的 JPEG(如截断文件)会让整个脚本 halt 在
Image.open()
用 concurrent.futures.ProcessPoolExecutor 控制并发 + 每次只处理一张
CPU 密集型任务(缩放、压缩)用多进程比多线程更有效,且进程间内存隔离,避免累积泄漏:
from concurrent.futures import ProcessPoolExecutor
import os
from pathlib import Path
<p>def process_one_image(src_path, dst_dir, quality=75, max_width=1920):
try:
from PIL import Image
im = Image.open(src_path)</p><h1>统一转 RGB(避免 PNG 有 alpha 通道导致 save 失败)</h1><pre class="brush:php;toolbar:false;"><pre class="brush:php;toolbar:false;"> if im.mode in ("RGBA", "LA", "P"):
background = Image.new("RGB", im.size, (255, 255, 255))
background.paste(im, mask=im.split()[-1] if im.mode == "RGBA" else None)
im = background
# 等比缩放
if im.width > max_width:
ratio = max_width / im.width
new_size = (max_width, int(im.height * ratio))
im = im.resize(new_size, Image.LANCZOS)
# 构造新文件名:原名去扩展名 + 时间戳哈希(防重名)
stem = Path(src_path).stem
ext = ".jpg"
dst_path = dst_dir / f"{stem}_{abs(hash(src_path)) % 1000000:06d}{ext}"
im.save(dst_path, "JPEG", quality=quality, optimize=True)
return str(dst_path)
except Exception as e:
return f"ERROR {src_path}: {e}"主流程
src_dir = Path("/path/to/originals") dst_dir = Path("/path/to/compressed") dst_dir.mkdir(exist_ok=True)
with ProcessPoolExecutor(max_workers=4) as executor: futures = [ executor.submit(process_one_image, p, dst_dir) for p in src_dir.glob("*.jpg") if p.is_file() ] results = [f.result() for f in futures]
重命名必须带唯一性锚点,别信文件序号
靠 i += 1
enumerate() 生成序号,在中断重跑时必然错乱;靠原文件名哈希又怕碰撞。稳妥做法是:- 用
hashlib.md5(src_path.read_bytes()).hexdigest()[:8]—— 但读全量文件太慢 - 改用
hash((str(src_path), src_path.stat().st_size, src_path.stat().st_mtime)),轻量且足够区分同名不同内容 - 最终文件名格式强制为
{original_stem}_{size_mtime_hash:08x}.jpg,例如vacation_3a7f1b2c.jpg - 如果业务要求“顺序编号”,必须先
sorted()所有路径,再用enumerate(),且把已生成的dst_dir文件列表加载进内存比对,跳过已存在的
压缩参数不是越小越好:quality=75 是实用平衡点
Pillow 的 quality 参数不是“压缩率”,而是 JPEG 编码的量化表强度:
-
quality=100:几乎无损,但体积只比85小 10–15%,人眼无法分辨 -
quality=75:主流网站默认值,体积约为100的 50–60%,画质损失极轻微 quality:块效应明显,尤其在文字、边缘处,且部分手机相册会拒绝显示- 务必加
optimize=True,它会重组 Huffman 表,通常再省 5–10% 体积
真正影响体积的大头是尺寸——一张 8000×6000 的图,哪怕 quality=95,也比一张 1920×1080 的 quality=75 大得多。先缩放,再压缩。
最易被忽略的是:没检查目标磁盘剩余空间。上万张图即使压缩后,也可能占几十 GB。运行前加一行 shutil.disk_usage(dst_dir.parent) 校验,比中途写满再报错强得多。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











