搜索全站
安全防护 12 分钟阅读

WordPress 批量替换文章内容:序列化安全替换与先备份原则

换域名、换 HTTPS、改目录后图片全挂?直接改数据库会把序列化数据弄坏。本文讲清序列化为何会失效,给出插件与 WP-CLI 两种安全替换方案,以及先备份后验证的完整流程。

U
UFO_PF 2020-05-03 · 阅读 12 分钟
Wo 安全防护
阅读提示
本文约 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」:

  1. Search for 填 http://old.com,Replace with 填 https://new.com;
  2. Select tables 里选全部表(默认不含 GUID 列,插件会提示);
  3. 勾选 Run as dry run 先跑一遍,插件会列出每张表将要修改的行数;
  4. 确认无误后取消 dry run 再执行一遍;
  5. 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') ) ) );"

前台抽查:首页、一篇带图集的旧文章、一个小工具区域、一个主题选项控制的区域(如页脚版权)。

常见踩坑

  1. 直接用 SQL 的 REPLACE()。 这是本文开头那场事故的根因,永远不要对可能含序列化数据的表直接写 UPDATE ... REPLACE()。
  2. 忘了 --dry-run。 一次把不该改的东西改了,比如把邮件模板里的短链接也换了。
  3. 改了 GUID。 导致 RSS 阅读器把全部历史文章当新内容推一遍,订阅用户被骚扰。
  4. 只换域名没换协议。 上 HTTPS 时只把 old.com 换成 new.com,结果还是有 http:// 混合内容,浏览器报”不安全”。协议和域名要一起换。
  5. 旧域名里带 www。 www.old.com 和 old.com 是不同字符串,要分别替换,或用 //old.com 这种协议相对形式做一次兜底。
  6. 缓存没清。 页面缓存、对象缓存、CDN 缓存三层都要清,否则前台看起来”没生效”。
  7. 案例: 某站把 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 配合 那篇也值得顺手确认。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。