用阿里云 ECS 跑一套 WebP 批处理

简介: 本文介绍了一套基于阿里云ECS与Python/Pillow的博客图片批量优化方案:自动缩放超大图、转为高质量WebP、保留原图与目录结构,并通过多进程提升效率。脚本详述EXIF校正、Alpha处理、并发调优及效果评估,兼顾实用性与可复现性。

图文博客里,图片往往比正文更占空间。一张手机照片直接传上去,页面上只显示几百像素宽,浏览器却要下载整张原图。文章多起来以后,备份包越来越大,换主题、迁移站点时,还得跟着搬一遍。
逐张打开图片、改尺寸、另存为,处理几张还行。面对一个按年份和文章分类的图片目录,我更愿意把规则写进脚本:原图留着,批量生成适合网页展示的版本,同时记下处理结果。
这篇文章围绕这个任务展开。计算放在阿里云 ECS 上,Python 负责遍历目录和分配任务,Pillow 完成缩放与 WebP 编码。重点放在三个问题上:图片该怎么处理,多核算力怎么用,以及处理完之后怎么判断这次优化值不值。

1. 先把处理规则定下来

这里处理的是博客配图中的静态照片,输入格式为 JPEG 和 PNG。规则很简单:

  • 长边超过 1920 像素时等比例缩小,小图保留原尺寸。
  • 输出 WebP,使用有损编码,quality=82、method=4。
  • 保留目录层级,原文件名后追加 .webp。
  • 原图单独保存,每次任务写入新的输出目录。

    1920 和 82 是这套方案的起始参数。最终取值要拿实际页面看:照片边缘、暗部和渐变有没有明显损伤,放大查看是否还够用。
    文字密集的终端截图、二维码、架构图放在另一个目录,继续使用 PNG。照片和截图的关注点不同,给所有图片套同一组参数,后面很容易花时间返工。
    WebP 的 quality 控制有损编码质量;method 在编码速度与压缩效果之间取舍,范围为 0—6。这两个参数的含义可以直接查 Pillow WebP 文档。
    整个流程如下:

    本地照片目录
      ↓ 上传
    ECS 工作目录 → 枚举图片 → 进程池处理 → WebP 目录
                                        ├── manifest.jsonl
                                        └── summary.json
    

2. ECS 环境:先记配置,再谈性能

下面的部署命令以 Ubuntu 24.04 为例。实例可以从 u2a 的 4 vCPU、8 GiB 规格起步,对应 ecs.u2a-c1m2.xlarge。这给 1、2、4、8 个进程的对照留出了观察空间,8 个进程用来观察超过 vCPU 数量后的变化。
这里有个选型细节:阿里云文档写明,u2a 兼容多代 AMD EPYC 处理器,支持 AMD Turin。因此,实际 CPU 型号也要记录,后面的性能分析才有依据。规格信息见 阿里云 u2a 官方文档。
创建实例后,通过 SSH 登录。这个任务通过命令行运行,安全组的 SSH 入方向来源限定为自己的公网 IP 即可。
安装环境:

sudo apt-get update
sudo apt-get install -y python3-venv sysstat

mkdir -p ~/image-job/raw ~/image-job/results
cd ~/image-job

python3 -m venv .venv
source .venv/bin/activate
python -m pip install Pillow==12.3.0

lscpu > cpu.txt
uname -a > system.txt
python --version > python-version.txt
python -m pip freeze > requirements.txt
python -c "from PIL import features; print('WebP:', features.check('webp'))"

最后一条命令应输出 WebP: True。把照片上传到 raw/,把下面的脚本保存为 batch_webp.py,就可以开始处理。

3. 脚本里几个容易忽略的细节

单张图片的处理顺序是:打开文件、纠正方向、转换颜色模式、缩放、编码。
手机照片的显示方向可能写在 EXIF 里。如果只读取像素再另存,输出图有可能横过来。脚本先调用 ImageOps.exif_transpose(),把方向落实到像素上。这一步的行为见 Pillow ImageOps 文档。
透明图片也要单独判断。带 Alpha 通道的图,以及使用调色板透明信息的 PNG,都转成 RGBA;其他图片转成 RGB。源图已有的 ICC 配置随输出保存。
文件名用 photo.jpg.webp 这样的形式。有些目录里会同时存在 photo.jpg 和 photo.png,如果都写成 photo.webp,就会发生覆盖。多留一个原始后缀,回查来源也方便。
完整脚本如下:

import argparse
import json
import time
from concurrent.futures import ProcessPoolExecutor
from pathlib import Path

from PIL import Image, ImageOps


def convert_one(job):
    source, target = map(Path, job)
    try:
        target.parent.mkdir(parents=True, exist_ok=True)
        with Image.open(source) as original:
            if getattr(original, "n_frames", 1) != 1:
                raise ValueError("Only static images are supported")
            image = ImageOps.exif_transpose(original)
            icc = image.info.get("icc_profile", b"")
            has_alpha = "A" in image.getbands() or "transparency" in image.info
            image = image.convert("RGBA" if has_alpha else "RGB")
            image.thumbnail((1920, 1920), Image.Resampling.LANCZOS)
            image.save(target, "WEBP", quality=82, method=4, icc_profile=icc)
        return {
   
            "source": str(source), "target": str(target), "status": "ok",
            "input_bytes": source.stat().st_size,
            "output_bytes": target.stat().st_size,
        }
    except (OSError, ValueError) as exc:
        return {
   "source": str(source), "status": "error", "error": str(exc)}


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("source", type=Path)
    parser.add_argument("target", type=Path)
    parser.add_argument("--workers", type=int, default=4)
    args = parser.parse_args()
    if args.workers < 1:
        parser.error("--workers must be positive")
    files = sorted(
        p for p in args.source.rglob("*")
        if p.is_file() and p.suffix.lower() in {
   ".jpg", ".jpeg", ".png"}
    )
    if not files:
        parser.error("No JPEG or PNG files found")
    args.target.mkdir(parents=True, exist_ok=False)
    jobs = [
        (str(p), str(args.target / p.relative_to(args.source).with_name(p.name + ".webp")))
        for p in files
    ]
    started = time.perf_counter()
    with ProcessPoolExecutor(max_workers=args.workers) as pool:
        rows = list(pool.map(convert_one, jobs, chunksize=1))
    seconds = time.perf_counter() - started
    with (args.target / "manifest.jsonl").open("w", encoding="utf-8") as stream:
        for row in rows:
            stream.write(json.dumps(row, ensure_ascii=False) + "\n")
    good = [r for r in rows if r["status"] == "ok"]
    input_bytes = sum(r["input_bytes"] for r in good)
    output_bytes = sum(r["output_bytes"] for r in good)
    summary = {
   
        "workers": args.workers, "success": len(good),
        "failed": len(rows) - len(good), "seconds": round(seconds, 3),
        "images_per_second": round(len(good) / seconds, 3),
        "input_bytes": input_bytes, "output_bytes": output_bytes,
        "saved_percent": round(100 * (1 - output_bytes / input_bytes), 2)
        if input_bytes else None,
    }
    (args.target / "summary.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8"
    )
    print(json.dumps(summary, ensure_ascii=False, indent=2))
    raise SystemExit(1 if summary["failed"] else 0)


if __name__ == "__main__":
    main()

执行一次:

python batch_webp.py raw results/first-run --workers 4

输出目录里,manifest.jsonl 逐张记录源文件、目标文件、状态和体积;summary.json 汇总成功数、失败数、耗时和总大小。坏文件和动态 PNG 会留下错误记录,其他图片继续处理。
主进程把路径交给子进程,子进程自己打开图片。这样进程间传递的是文件路径,解码后的大块像素数据留在各自的进程里。这里使用 Python 标准库的 ProcessPoolExecutor,后续通过实测决定进程数。

4. 并发开多少,用同一批图片比较

处理规则确定以后,再调整并发数。否则,一边降低图片质量,一边增加进程,最后很难解释耗时为什么变了。
先完整运行一次,让依赖和输入数据经过首次读取。预热结果放在独立目录:

python batch_webp.py raw results/warmup --workers 1

接着分别测试 1、2、4、8 个进程,每组跑三次:

for run in 1 2 3; do
  for workers in 1 2 4 8; do
    python batch_webp.py raw "results/r${run}-w${workers}" \
      --workers "$workers"
  done
done

每轮使用新目录,编码参数和输入文件保持一致。比较前先检查各组成功数和失败数,确认处理的是同一批文件。数据接近时,可以再轮换各组的执行顺序,观察排序是否影响结论。
脚本统计的是处理阶段的墙钟时间,包括进程池启动、图片读取、缩放、编码和输出写入;上传、下载、环境安装都在计时区间之外。这次比较的是预热后的批处理表现。
运行期间,在另一个终端观察:

mpstat -P ALL 1
iostat -xz 1
free -h

ECS 控制台也可以查看 CPU 等基础监控。需要内存等操作系统级指标时,可以安装云监控插件,具体采集范围见 阿里云实例运维说明。短任务的变化用终端观察更直接。
我会先看吞吐,再看 CPU、内存和磁盘指标。如果进程从 4 个增加到 8 个,吞吐已经不变,内存占用却继续增长,就把进程数收回到 4。假如 CPU 还有空闲,磁盘等待持续升高,则继续检查输入输出阶段。
图片解码后的内存占用也要算进去。一张 4000×3000 的 RGBA 图片,仅像素数据就约 48 MB,缩放和编码过程还需要额外空间。进程数越多,同时驻留的图片也越多。

5. 这套方案适合用到哪里

这套处理方式适合集中整理照片、迁移博客,以及批量生成网页配图。输入目录和编码参数固定后,换一批照片仍然可以沿用同一个入口,失败文件也有清单可查。
ECS 在这里承担的是一个独立的批处理环境:运行版本固定,任务可以留在远端执行,也便于在同一套代码下比较不同实例规格。实际使用时,端到端收益还要把素材上传和结果下载的时间算进去。
还有一个容易漏掉的收尾工作:脚本保留了原文件后缀,博客引用也要相应改为 photo.jpg.webp,并检查页面是否正常加载。先挑一篇文章替换,确认路径和显示效果,再批量修改。
我更看重这项工作的可重复性。压缩规则、运行环境和结果记录齐了,下一次就能直接回答:这批图片用了多久、缩小了多少、哪些文件需要重新处理。后续是否换更大的实例,也有数据可比较。

相关文章
|
19小时前
|
弹性计算 前端开发 应用服务中间件
在阿里云 ECS 上部署静态站点的文件覆盖问题
本文介绍如何在阿里云ECS(Ubuntu 24.04)上通过Nginx部署静态站点,并实现安全、可回滚的版本化发布:使用`releases/`多版本目录+`current`软链接切换,避免更新中断与覆盖风险;强调安全组与防火墙配置区分、原子化链接替换、内容验证及资源缓存策略,兼顾简易性与可靠性。(239字)
|
弹性计算 开发工具 对象存储
ECS实例通过OSS内网地址访问OSS资源
ECS实例通过OSS内网地址访问OSS资源
857 0
|
6月前
|
人工智能
阿里云百炼全模型通用抵扣AI节省计划是什么意思,如何使用?
阿里云AI通用型节省计划是预付费折扣套餐:每月承诺最低消费(如10元),即可享约50%调用折扣。自动抵扣百炼平台150+大模型(Qwen、DeepSeek、GLM等)的按量费用,覆盖推理、工具调用等,但不含调优与部署。额度当月有效、不结转。新手可先用7000万Token免费额度。
|
2月前
|
人工智能
别读错!阿里云 Qoder 音标 /ˈkoʊdər/,个人社区版‑企业版资费完整解析
阿里云Qoder(读作“抠的儿”)是面向开发与办公的AI智能体,原名通义灵码。提供免费社区版及多档付费方案,涵盖个人、团队与企业需求,官网可查详情与优惠。阿里云Qoder CN官网:https://t.aliyun.com/U/fEiOLV
370 1
|
Oracle 关系型数据库 大数据
|
7月前
|
Kubernetes Cloud Native 安全
深度解析:2026年云原生技术发展趋势与企业数字化转型实践
本文深度解析2026年云原生五大趋势:K8s向云原生OS演进,Wasm容器崛起;服务网格成熟落地,“适度微服务”成主流;Serverless加速商业化,开源框架破锁定;DevSecOps深度融合,安全左移常态化;整体呈现平台化、智能化、安全化发展态势。(239字)
|
9月前
|
物联网 开发者 异构计算
从入门到精通:大模型微调实战全攻略
本文系统讲解大模型微调实战:涵盖环境搭建、数据准备、主流方法(LoRA/QLoRA)、完整训练流程及过拟合、显存不足等常见问题解决方案,并分享数据质量、混合精度、评估体系等进阶技巧,助力开发者快速定制专属大模型。
从入门到精通:大模型微调实战全攻略
|
开发者
《钉钉应用开发者速成手册》之“钉钉开放平台名词解释”
在开始开发前,建议您先了解开放平台的名词解释。
1900 0
《钉钉应用开发者速成手册》之“钉钉开放平台名词解释”
|
5月前
|
人工智能 运维 Serverless
基于函数计算的 AI 大模型 Serverless 化部署方案与实践
本文提出基于阿里云函数计算的Serverless化AI大模型部署方案,覆盖文生图(ComfyUI+Flux)、语音合成(GPT-Sovits)及开源LLM等场景,具备免运维、极致弹性、按量付费优势,显著降低硬件成本与试错门槛。