全站图片批量转 WebP:19 万张图片完整实战复盘
本地批量转换 → 清理原图 → 权限修复 → 数据库安全替换 → 上传自动转换。把踩过的坑一次性讲清楚。
实战环境:Debian 12 · WordPress + B2 主题 | 编码器:Google cwebp 1.4.0
实战成果:成功转换 191,190 张图片(失败仅 251 张),平均压缩率 40.26%,转换节省 15.5 GB,清理原图再释放 4.965 GB,累计节省超过 20 GB。
一、为什么要转 WebP
WebP 是 Google 推出的现代图片格式,在画质几乎无损的前提下,体积比 JPG 平均小 25%~35%,比 PNG 小得更多。对图片量大的 WordPress 站(下载站、素材站、资讯站)来说,全站转 WebP 是收益最直接的提速手段:页面更轻、带宽更省、加载更快。
本次实战实测:原始图片 38.5 GB,转 WebP 后 23 GB,直降 15.5 GB(降幅 40.26%)。
二、整体流程总览
整个迁移分为六个阶段,顺序不能乱:先备份、再转换、后清理,最后才是动数据库。
- 全站备份:数据库 mysqldump 全量导出 + uploads 目录打包
- 安装 cwebp + 准备脚本:Google 官方编码器 + Python 批量脚本
- 批量转换 WebP:Python 多进程逐张调用 cwebp,支持断点续跑,有失败就补跑
- 清理原图:仅删除「已存在同名 webp」的 jpg/png,防止误删
- 修复权限 + 清空图:uploads 归还 www 用户,清理 0 字节坏缩略图
- 数据库安全替换:wp_posts 直接 SQL / wp_postmeta 序列化脚本分批 / wp_options 绝不碰
- 前台验证显示:抽查文章、封面、头像均正常加载 webp
- 装自动转换插件:新上传图片自动转 WebP,永久翻篇
动手之前,必做两件事:① 备份数据库(mysqldump 全量导出);② 备份 uploads 目录(打包到 /root)。后面任何一步出错,都能靠这两份备份救回来。
mysqldump -u用户名 -p 数据库名 > /root/site_db_backup.sql
tar -czf /root/uploads_backup.tar.gz /www/wwwroot/站点目录/wp-content/uploads
三、准备工作:安装 cwebp
cwebp 是 Google 官方的 WebP 编码器,转换质量和速度都是第一梯队。
Linux(Debian / Ubuntu)安装
apt update
apt install -y webp
cwebp -version # 输出 1.4.0 之类即安装成功
单张测试转换
先拿一张图试试手感,确认编码器工作正常:
cwebp -q 80 /www/wwwroot/站点/wp-content/uploads/test.jpg -o /tmp/test.webp
ls -lh /tmp/test.webp # 对比原文件体积
-q 80:质量参数,80 是画质与体积的最佳平衡点,站图推荐 75~85。- PNG 透明图转换后透明通道会保留,不用担心。
四、第一步:批量转换脚本
几万张图不可能手工转,用 Python 多进程 + cwebp 的组合:遍历 uploads 目录所有 jpg/jpeg/png,逐张调用 cwebp 输出同名 .webp。8 进程并发下,19 万张图几个小时就能跑完。
完整脚本(已修复踩坑版)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 批量转换 uploads 下所有 jpg/jpeg/png 为同名 .webp(多进程版)
import os
from multiprocessing import Pool
SRC_DIR = "/www/wwwroot/站点目录/wp-content/uploads" # ← 改成你的 uploads 路径
QUALITY = 80 # WebP 质量
EXTS = ('.jpg', '.jpeg', '.png')
def convert(task):
src, dst = task
try:
# 关键!先读原图大小,再转换(教训见避坑指南第 1 条)
src_size = os.path.getsize(src)
if os.path.exists(dst): # 已有 webp 就跳过
return ('skip', src, 0)
ret = os.system(f'cwebp -q {QUALITY} "{src}" -o "{dst}" >/dev/null 2>&1')
if ret == 0 and os.path.exists(dst) and os.path.getsize(dst) > 0:
return ('ok', src, src_size - os.path.getsize(dst))
return ('fail', src, 0)
except Exception:
return ('fail', src, 0)
def collect():
tasks = []
for root, _, files in os.walk(SRC_DIR):
for f in files:
if f.lower().endswith(EXTS):
src = os.path.join(root, f)
tasks.append((src, os.path.splitext(src)[0] + '.webp'))
return tasks
if __name__ == '__main__':
tasks = collect()
print(f'共 {len(tasks)} 张待处理')
ok = skip = fail = saved = 0
with Pool(8) as p:
for i, r in enumerate(p.imap_unordered(convert, tasks, chunksize=50)):
if r[0] == 'ok': ok += 1; saved += r[2]
elif r[0] == 'skip': skip += 1
else: fail += 1; print('[失败]', r[1])
if (i + 1) % 5000 == 0:
print(f'进度 {i+1}/{len(tasks)} 成功 {ok} 失败 {fail}')
print(f'完成!成功 {ok} 跳过 {skip} 失败 {fail}')
print(f'节省空间:{saved/1024/1024/1024:.2f} GB')
踩坑 1 · 统计永远显示”转换失败”:最初版本的脚本把 os.path.getsize(原图) 写在了删除原图之后——图都删了还怎么读大小?于是每张图都报失败。修复方法:先 getsize 记录原大小,再做后续删除动作。上面脚本已经是修复版。
中断了怎么办?
脚本支持断点续跑:同名 webp 已存在的会自动跳过。万一中途断开,重新运行即可接着上次进度继续,不会重复转换。
五、第二步:清理原图
webp 转出来后,原来的 jpg/png 就是冗余数据(19 万张占着近 5GB)。但绝不能无脑全删——只删”已经存在同名 webp”的原图,防止误删没转换成功的。
安全删除三步法
# ① 导出"有对应 webp 的原图"清单,并统计数量
cd /www/wwwroot/站点目录/wp-content/uploads
find . -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" \) | while read f; do
[ -f "${f%.*}.webp" ] && echo "$f"
done | tee /tmp/to_delete.txt | wc -l
# ② 打开清单抽查几条,确认路径没有异常
head -20 /tmp/to_delete.txt
# ③ 确认无误后执行删除
cat /tmp/to_delete.txt | xargs -d '\n' rm -f
实战数据:本次共清理 158,862 张原图,释放 4.965 GB 空间。清单里没出现的图(即没有对应 webp 的)会自动保留,等转换补齐后再清。
六、第三步:修复权限与空图
这一步解决的是”转换后缩略图集体变空白”的问题——图片本身在,网站却显示不出来。
排查思路
- 打开网站发现缩略图全挂 → 检查文件是否存在、大小是否正常;
- 发现大量 0 字节文件 → 说明曾经有程序以错误身份写过这些文件;
- ls -lh 看文件所有者 → 发现 thumb 目录属于 root,而 PHP 以 www 用户运行 → 权限不符,www 写不进去,留下了空文件。
修复命令
# ① 把整个 uploads 归还给 PHP 运行用户 www
chown -R www:www /www/wwwroot/站点目录/wp-content/uploads
# ② 批量清理 0 字节的坏缩略图
find /www/wwwroot/站点目录/wp-content/uploads/thumb -type f -size 0 -delete
为什么会产生 0 字节文件:root 身份改过 uploads 里的东西之后,PHP(www 用户)对该目录失去写权限,主题生成缩略图时”写了个开头就写不进去”,留下大量空壳文件。清理空文件后,主题会在下次访问时自动重新生成正常缩略图。
七、第四步:数据库安全替换
文件转好了,但数据库里存的还是 .jpg 的旧地址,文章、封面、头像全都指向不存在的文件。数据库替换是最容易翻车的一步,核心原则只有一条:
铁律:wp_options 绝对不能直接跑 SQL 替换。wp_options 里存着主题设置(B2 的封面图、默认头像、用户背景全在里面),而且是 PHP 序列化格式。直接 REPLACE 会破坏序列化长度标记,主题设置直接报废。本次实战就因此把主题设置搞坏过一次,靠备份才救回来。
替换范围一览
| 数据表 / 字段 | 能否直接 SQL | 原因 |
|---|---|---|
| wp_posts → post_content / post_title / guid / post_excerpt | 可以 | 纯文本,REPLACE 长度无副作用 |
| wp_posts → post_mime_type | 可以 | jpeg/png 改成 image/webp |
| wp_postmeta → meta_value | 禁止直接 SQL | 大量序列化数据(缩略图尺寸表等),必须脚本处理 |
| wp_options → option_value | 禁止任何直接替换 | 主题设置全在这,序列化,坏了全站挂 |
① wp_posts:直接 SQL(快)
mysql -u用户名 -p 数据库名 -e "
UPDATE wp_posts SET post_content = REPLACE(post_content, '.jpg', '.webp')
WHERE post_content LIKE '%.jpg%';
UPDATE wp_posts SET guid = REPLACE(guid, '.jpg', '.webp')
WHERE guid LIKE '%.jpg%';
UPDATE wp_posts SET post_mime_type = 'image/webp'
WHERE post_mime_type IN ('image/jpeg','image/png');
"
② wp_postmeta:序列化安全分批处理(稳)
postmeta 里存着 _wp_attachment_metadata(PHP 序列化的缩略图尺寸数组)。序列化字符串带有长度前缀,直接 REPLACE 字符串会让长度对不上、数据解析失败。正确做法是 PHP 循环:maybe_unserialize → 递归替换 → serialize 写回。
<?php
// 加载 WordPress 环境
require __DIR__ . '/wp-load.php';
global $wpdb;
$BATCH = 2000; // 每批处理条数
$map = ['.jpg' => '.webp', '.jpeg' => '.webp', '.png' => '.webp'];
while (true) {
$rows = $wpdb->get_results(
"SELECT meta_id, meta_value FROM {$wpdb->postmeta}
WHERE (meta_value LIKE '%.jpg%' OR meta_value LIKE '%.jpeg%' OR meta_value LIKE '%.png%')
LIMIT {$BATCH}"
);
if (!$rows) { echo "全部完成\n"; break; }
foreach ($rows as $row) {
$data = maybe_unserialize($row->meta_value); // 反序列化
$fixed = replace_deep($data, $map); // 递归替换
$new = is_serialized($data) ? serialize($fixed) : $fixed;
update_postmeta_and_clean($row->meta_id, $new); // 写回并清缓存
}
echo "本批 " . count($rows) . " 条处理完毕\n";
}
function replace_deep($data, $map) {
if (is_array($data)) return array_map(fn($v) => replace_deep($v, $map), $data);
if (is_object($data)) { foreach ($data as $k=>$v) $data->$k = replace_deep($v, $map); return $data; }
if (is_string($data)) return strtr($data, $map);
return $data;
}
- 分批 LIMIT:2 万多条 meta 一口气处理会内存爆炸/超时,每批 2000 条循环跑最稳;
- 写回时记得调用 clean_post_cache 或直接用 update_metadata,避免对象缓存残留旧数据;
- 本站实测处理 21,669 条 postmeta,全部无损。
③ 替换后自检 SQL
# 应该显著大于 0
mysql -u用户名 -p 库名 -e "SELECT COUNT(*) FROM wp_posts WHERE post_content LIKE '%.webp%';"
# 应该接近 0(残留越少越好)
mysql -u用户名 -p 库名 -e "SELECT COUNT(*) FROM wp_posts WHERE post_content LIKE '%.jpg%';"
八、血泪避坑指南(重点)
以下 5 个坑全部在实战中真实踩过,每一个都付出过代价。转 WebP 前把这一节读完,能少走一晚上弯路。
| # | 坑 | 症状 | 解法 |
|---|---|---|---|
| 1 | 脚本统计全部”失败” | 明明转换成功,报告却说 19 万张全失败 | getsize 必须在删除原图之前调用(见第四步脚本注释) |
| 2 | 缩略图集体空白 | 图片文件在,前台显示不出来 | chown -R www:www uploads 归还权限 |
| 3 | 10 万+ 0 字节空图 | thumb 目录全是空壳文件 | find … -size 0 -delete 清理,主题会自动重新生成 |
| 4 | 主题设置被搞坏 | 封面、默认头像、用户背景全部不显示,重传也没用 | wp_options 里的 b2_% 选项被 SQL 替换毁了 → 用转换前的数据库备份恢复 |
| 5 | 备份恢复报主键冲突 | 导入 b2_% 备份时报 Duplicate entry 错误 | 备份里的 INSERT INTO 全部换成 REPLACE INTO 再导入(见下方命令) |
坑 4+5 的完整救援实录
主题设置坏了别慌,只要你有转换前的全量备份 SQL,三步救回:
# ① 从全量备份里单独抽出主题设置表结构+数据
sed -n '/DROP TABLE.*wp_options/,/UNLOCK TABLES/p' 全量备份.sql | mysql -u用户名 -p 库名
# ② 或者只恢复 b2_ 开头的选项:先导出
mysqldump -u用户名 -p 库名 wp_options --where="option_name LIKE 'b2_%'" > b2_options_backup.sql
# mysqldump 报 PROCESS 权限错误时,在命令后加 --no-tablespaces
# ③ 导入报主键冲突 → INSERT INTO 全部换成 REPLACE INTO
sed 's/INSERT INTO/REPLACE INTO/g' b2_options_backup.sql | mysql -u用户名 -p 库名
顺带一提:Better Search Replace 也别用。插件类搜索替换工具对超长序列化字段同样有翻车风险,而且处理 2 万条 meta 极慢。分批 PHP 脚本是唯一又快又安全的方案。
九、一劳永逸:B2 自动转换插件
存量图处理完了,新上传的图怎么办?手动转很快会破功。为此定制了一个轻量单文件插件(约 300 行,无任何依赖),装上之后新图全自动 WebP。
功能清单
| 功能 | 说明 |
|---|---|
| 上传自动转换 | 文章图、头像、用户背景、封面……所有走 WordPress 上传的图片自动转 WebP |
| 自动删除原图 | 转换成功后删掉原 jpg/png(可在设置关闭),不占双份空间 |
| 质量可调 | 设置 → WebP 转换设置,默认 80 |
| 媒体库格式列 | 列表新增”格式”列:绿色 WebP 已转换 / 红色 JPG 未转换,一眼看清 |
| 缩略图同步转 | 各尺寸缩略图一并转换 |
| 单文件零依赖 | 纯 GD 库实现(PHP 5.4+ 自带 WebP 支持),不装扩展 |
安装使用
- WP 后台 → 插件 → 安装插件 → 上传 b2-webp.zip → 启用;
- 设置 → WebP 转换设置 → 调整质量、勾选”删除原图”;
- 随便传一张新图测试,媒体库”格式”列变绿即生效。
验证方式:上传一张 JPG → 媒体库列表”格式”列显示绿色 WebP、文件管理器里原图已删除 = 插件工作正常。之后所有新图不再需要任何手动操作。
十、成果总结与命令速查
迁移成果总账
| 指标 | 数值 |
|---|---|
| 成功转换图片 | 191,190 张(失败 251 张,补跑清零) |
| 平均压缩率 | 40.26% |
| 转换节省空间 | 15.5 GB |
| 清理原图 | 158,862 张,释放 4.965 GB |
| 数据库替换 | wp_posts 全字段 + wp_postmeta 21,669 条(序列化无损) |
| 累计释放空间 | 约 20.4 GB |
常用命令速查表
| 用途 | 命令 |
|---|---|
| 查目录占用 | du -sh /www/wwwroot/站点/wp-content/uploads |
| 单张转换 | cwebp -q 80 输入.jpg -o 输出.webp |
| 批量跑转换 | python3 convert_webp.py |
| 修权限 | chown -R www:www uploads |
| 清空图 | find thumb -type f -size 0 -delete |
| 备份数据库 | mysqldump -u用户名 -p 库名 > /root/db.sql |
| 统计 webp 文章数 | SELECT COUNT(*) FROM wp_posts WHERE post_content LIKE ‘%.webp%’; |
| 备份恢复去冲突 | sed ‘s/INSERT INTO/REPLACE INTO/g’ 备份.sql |
最后的建议:存量图迁移是一次性工程;装上自动转换插件之后,这件事情就永久翻篇了。定期(每月)用 du -sh 看一眼 uploads 体积,保持关注即可。
参考:Google Developers — WebP 官方文档 | WordPress 开发者文档














暂无评论内容