B2主题 WebP 全站改造完整教程:从批量转换到自动上传一步到位

全站图片批量转 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%)。

二、整体流程总览

整个迁移分为六个阶段,顺序不能乱:先备份、再转换、后清理,最后才是动数据库。

  1. 全站备份:数据库 mysqldump 全量导出 + uploads 目录打包
  2. 安装 cwebp + 准备脚本:Google 官方编码器 + Python 批量脚本
  3. 批量转换 WebP:Python 多进程逐张调用 cwebp,支持断点续跑,有失败就补跑
  4. 清理原图:仅删除「已存在同名 webp」的 jpg/png,防止误删
  5. 修复权限 + 清空图:uploads 归还 www 用户,清理 0 字节坏缩略图
  6. 数据库安全替换:wp_posts 直接 SQL / wp_postmeta 序列化脚本分批 / wp_options 绝不碰
  7. 前台验证显示:抽查文章、封面、头像均正常加载 webp
  8. 装自动转换插件:新上传图片自动转 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 归还权限
310 万+ 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 支持),不装扩展

安装使用

  1. WP 后台 → 插件 → 安装插件 → 上传 b2-webp.zip → 启用;
  2. 设置 → WebP 转换设置 → 调整质量、勾选”删除原图”;
  3. 随便传一张新图测试,媒体库”格式”列变绿即生效。

验证方式:上传一张 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 开发者文档

© 版权声明
THE END
喜欢就支持一下吧
点赞15 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容