本文约 4,788 字,预计阅读 12 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:改完数据库,小工具和菜单全空了
换域名、上 HTTPS、把站点从子目录挪到根目录,是运维里最常见的三类操作。很多人的第一反应是:进 phpMyAdmin,执行一句 UPDATE wp_posts SET post_content = REPLACE(post_content, 'http://old.com', 'https://new.com');,执行完显示”影响 8000 行”,很满意。
然后打开前台,文章里的图片好了,但侧边栏的小工具全没了、菜单只剩一个、主题的自定义设置(Logo、颜色)全部回到默认。后台主题选项页一片空白,导出的数据里出现 s:34:"https://new.com/logo.png"; 这种明显不对的字符串。
原因只有一个:WordPress 里大量数据不是纯字符串,而是 PHP 序列化字符串。 序列化数据里带着长度标记,用 SQL 的 REPLACE() 只改内容不改长度,反序列化时就会失败,整个字段被判为无效。这篇文章讲清它为什么会坏,以及怎么换才安全。
原理分析:序列化字符串里的长度陷阱
PHP 的 serialize() 会把数组变成带类型与长度的字符串:
a:1:{s:8:"logo_url";s:25:"http://old.com/logo.png";}
a:1 是数组 1 项,s:8 表示后面那个字符串长 8(logo_url),s:25 表示 http://old.com/logo.png 长 25。
现在把 http://old.com 换成 https://new.com,长度从 25 变成 26,但 s:25 没变。PHP 反序列化时按 25 个字符去读,读出来的字符串和后面的结构对不上,unserialize() 返回 false,WordPress 的 maybe_unserialize() 拿不到数组,小工具、菜单、主题选项就全部失效。
| 数据位置 | 是否为序列化/结构化 | 直接 REPLACE 的后果 |
|---|---|---|
wp_posts.post_content |
纯 HTML(古腾堡评论是 JSON) | 一般安全,但块注释里的 URL 也含长度无关 |
wp_postmeta.meta_value |
常为序列化数组 | 极易失效 |
wp_options.option_value |
小工具、主题选项常为序列化 | 极易失效 |
wp_options.option_value(Elementor 数据) |
JSON | 长度不影响,但需整体替换 |
wp_posts.guid |
URL | 官方不建议改,改了会让阅读器重复推送 |
除了序列化,还有两个隐藏坑:
- GUID:它是文章的永久标识符,不是真实链接。改域名时改 GUID 会导致 RSS 阅读器把老文章当新文章重新推送一遍。
- JSON 里的转义:古腾堡内容与 Elementor 的
_elementor_data是 JSON,URL 里如果有反斜杠转义(如https:\/\/old.com),只替换https://old.com会漏掉。这也是 WP-CLI 提供--precise之类的严格模式的原因之一。
保姆级实操
第一步:完整备份,且验证备份可用
# 导出数据库(含 --add-drop-table,便于整体回滚)
wp db export backup-before-replace.sql --add-drop-table
# 确认文件真的生成了
ls -lh backup-before-replace.sql
没有验证过的备份不算备份。至少确认文件大小合理,最好在另一台机器上恢复一次。
第二步:优先用 WP-CLI(最可控)
# 1) 先空跑,看会改多少行、哪些表
wp search-replace 'http://old.com' 'https://new.com' --all-tables --dry-run
# 2) 正式执行:跳过 guid、递归处理对象、严格匹配
wp search-replace 'http://old.com' 'https://new.com' \
--all-tables \
--skip-columns=guid \
--recurse-objects \
--precise \
--report-changed-only
# 3) 只改某几张表(更保守的写法)
wp search-replace 'http://old.com' 'https://new.com' wp_posts wp_postmeta wp_options
参数含义:
--dry-run:只统计不改,正式执行前必跑;--skip-columns=guid:跳过 GUID 列;--recurse-objects:递归处理序列化里的对象;--precise:用 PHP 严格处理,避免部分匹配问题;--report-changed-only:只打印真正被改的表,输出更干净。
第三步:不用命令行就用 Better Search Replace 插件
后台「工具 → Better Search Replace」:
- Search for 填
http://old.com,Replace with 填https://new.com; - Select tables 里选全部表(默认不含 GUID 列,插件会提示);
- 勾选
Run as dry run先跑一遍,插件会列出每张表将要修改的行数; - 确认无误后取消 dry run 再执行一遍;
- Case-Sensitive、Replace GUIDs 两项按需要决定,默认保持关闭即可。
同类的还有 Inpsyde 的「Search & Replace」插件,逻辑基本一致。二者都会正确处理序列化长度。
第四步:同步站点地址
替换只改了内容里的字符串,站点本身的配置还得更新:
wp option update home 'https://new.com'
wp option update siteurl 'https://new.com'
改完马上执行:
wp rewrite flush --hard
wp cache flush
第五步:如果必须自己写 PHP(理解原理用)
WP-CLI 的内部做法是先反序列化、递归替换字符串、再重新序列化。等价的简化实现如下,供理解原理,生产环境仍建议用现成工具:
function mimangfei_recursive_replace( $value, $from, $to ) {
if ( is_string( $value ) ) {
return str_replace( $from, $to, $value );
}
if ( is_array( $value ) ) {
foreach ( $value as $k => $v ) {
$value[ $k ] = mimangfei_recursive_replace( $v, $from, $to );
}
return $value;
}
if ( is_object( $value ) ) {
foreach ( get_object_vars( $value ) as $k => $v ) {
$value->{$k} = mimangfei_recursive_replace( $v, $from, $to );
}
return $value;
}
return $value;
}
// 用法:取出 → 判断 → 递归替换 → 重新序列化写回
$raw = get_option( 'widget_text' );
$data = maybe_unserialize( $raw );
$data = mimangfei_recursive_replace( $data, 'http://old.com', 'https://new.com' );
update_option( 'widget_text', $data );
第六步:验证
# 检查还有没有残留的旧域名
wp db query "SELECT COUNT(*) FROM wp_posts WHERE post_content LIKE '%old.com%';"
# 检查序列化是否还完整(能反序列化出来)
wp eval "var_dump( is_array( maybe_unserialize( get_option('widget_text') ) ) );"
前台抽查:首页、一篇带图集的旧文章、一个小工具区域、一个主题选项控制的区域(如页脚版权)。
常见踩坑
- 直接用 SQL 的
REPLACE()。 这是本文开头那场事故的根因,永远不要对可能含序列化数据的表直接写UPDATE ... REPLACE()。 - 忘了
--dry-run。 一次把不该改的东西改了,比如把邮件模板里的短链接也换了。 - 改了 GUID。 导致 RSS 阅读器把全部历史文章当新内容推一遍,订阅用户被骚扰。
- 只换域名没换协议。 上 HTTPS 时只把
old.com换成new.com,结果还是有http://混合内容,浏览器报”不安全”。协议和域名要一起换。 - 旧域名里带
www。www.old.com和old.com是不同字符串,要分别替换,或用//old.com这种协议相对形式做一次兜底。 - 缓存没清。 页面缓存、对象缓存、CDN 缓存三层都要清,否则前台看起来”没生效”。
- 案例: 某站把
http://www.abc.com换成https://abc.com,管理员用一句 SQL 全表REPLACE()搞定。结果导航菜单、侧边栏小工具、主题的首页区块设置全部丢失,只有文章正文正常。排查发现wp_options里widget_*与theme_mods_*全是序列化数据,长度错一位就反序列化失败。最后从备份恢复,改用wp search-replace重跑,前后耗时不过十分钟。结论:批量替换不是”改字符串”,而是”改完还能被正确反序列化”,这两件事只有专用工具能保证。
总结
替换操作的红线只有两条:先备份并验证,再动手;绝不用裸 SQL 改可能含序列化的数据表。 工具选择上,有命令行就用 wp search-replace(先 --dry-run),没有就用 Better Search Replace 之类会处理序列化长度的插件(同样先勾选 dry run)。
执行顺序:备份 → dry run → 正式替换(跳过 GUID)→ 更新 home/siteurl → 刷新固定链接与缓存 → 抽查验证 → 保留备份至少一周。站点迁移场景里,替换通常是最后一步,前面的数据搬运可以参考 WP All Import 批量导入配置 与 WP All Export 导出数据;而如果你关心替换后定时任务还能不能正常跑,WP-Cron 与系统 cron 配合 那篇也值得顺手确认。
