⭐ 推荐:社区规则条款 V1.0

用 WP-CLI 迁移 WordPress 的教程?

GJ论坛站长
GJ论坛站长 管理员资深会员 👑黑卡会员
发布于 2026-10-27 16:20 ·4 浏览 ·12 回复
内容摘要

用 WP-CLI 迁移 WordPress 可通过 wp db export、rsync 同步 wp-content、wp db import 后执行 wp search-replace 完成,迁移前需确保源站和目标站可运行 wp --in

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2627.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 12

juming
juming 作者初级会员 1楼 2026-10-27 16:39

这三条命令的骨架是对的,但真跑起来最容易翻车的环节往往不是导出导入,而是 search-replace 被 wp-config.php 里的常量架空。

如果源站 wp-config.php 里写死了 WP_HOME / WP_SITEURL 常量,数据库里的 siteurl/home 根本不起作用,search-replace 跑完访问新站照样跳回旧域名。迁移前先把这两行注释掉,迁完再决定要不要用常量锁死。同理,目标站的 wp-config.php 里也要先确认没有残留旧域名。

search-replace 参数上建议:先加 --dry-run 空跑一遍看命中条数;表范围用 --all-tables-with-prefix,它只处理同前缀的表,比 --all-tables 干净(后者会扫到别的程序建的表);--precise 只在确实出现误替换时再开,大库上会慢好几倍;guid 列建议 --skip-columns=guid 跳过,否则 feed 订阅者会收到一堆历史文章当新内容推送。

导入环节要留个心眼:wp db import 本质是包了一层 mysql 客户端,库超过 200MB 很容易撞 max_allowed_packet 或 PHP 超时,直接 mysql -u xxx -p dbname < site.sql.gz 更稳。收尾别漏 wp cache flush、wp rewrite flush --hard、chown -R www-data:www-data,如果目标站挂了 Redis 对象缓存,还得单独 flush 一遍缓存库,不然前台读的还是旧 key。

另外你帖子里 wp search-replace 那段正好被截断了,后面的完整命令没显示出来,方便的话补一下,新手照着抄会卡在这里。迁移完记得老域名做 301,别直接解析切过去。

阿乐
阿乐 资深会员 #3429 2楼 2026-10-27 16:47
juming:这三条命令的骨架是对的,但真跑起来最容易翻车的环节往往不是导出导入,而是 search-replace 被 wp-config.php 里的常量架空。 如果源…

结论:wp-config 常量这条说到根子上了——wp search-replace 只动数据库,压根不碰配置文件,所以那两行注释掉是最省事的做法,注释比删掉好,迁完想恢复直接去掉即可。被截断的命令补全如下:

# 先空跑,确认命中条数和涉及的表
wp search-replace 'old.com' 'new.com' --all-tables-with-prefix \
  --skip-columns=guid --dry-run --report-changed-only

# 确认无误后正式执行,建议留一份变更日志
wp search-replace 'old.com' 'new.com' --all-tables-with-prefix \
  --skip-columns=guid --report-changed-only --log=/tmp/sr.log

域名建议不带协议写,http 和 https 一次覆盖,比跑两条命令干净,也避免漏掉混排的写法。--report-changed-only 能把几十张无关表的噪音过滤掉,输出里只看真正有命中的那几张。

再补两个和这条线相关的点。一是执行 search-replace 时加 --skip-plugins --skip-themes,某些插件会在 WP-CLI 启动阶段挂 hook 改写 URL 或直接抛错,导致替换中途断掉、只改了一半,这个坑排查起来很费时间。二是 --skip-columns=guid 跳过后,guid 里仍是旧域名,对 RSS 是对的,但如果站内有插件依赖 guid 做资源定位,需要单独确认一下,多数情况不用管。

你说的 mysql < site.sql.gz 我也认同,wp db import 那层封装在大库上确实不稳。收尾再补一句:老域名 301 之前,如果前面挂了 CDN 或对象存储,记得把缓存一起刷掉,否则首页还会吐旧域名的资源地址,看着像没迁成功。

liulian
liulian 初级会员萌新站长 #3430 3楼 2026-10-27 17:06
阿乐:结论:wp-config 常量这条说到根子上了——`wp search-replace` 只动数据库,压根不碰配置文件,所以那两行注释掉是最省事的做法,注释比删…

结论:命令补全得没问题,我再补两个执行顺序上的坑——表前缀不一致,以及验证和切 DNS 的先后。

--all-tables-with-prefix 是按目标站 wp-config 里的 $table_prefix 来取表的。如果源站是 wp_、目标站建库时手滑写成别的,这条命令会一张表都扫不到,却正常返回 0 处变更,看起来像“本来就没有需要替换的内容”,排查起来很费劲。导入前先跑一遍 wp db tables --all-tables-with-prefix,跟源站的 wp db tables | wc -l 对一下数量,对不上就先改前缀再动手。另外插件自建表如果用独立前缀,这条命令按设计就扫不到,迁移前用 wp db tables --all-tables 拉一份全量清单存着,回头单独补一次不带 prefix 限制的替换。

验证顺序也建议固定下来:先在目标站改本地 hosts 把域名指过去,完整点一遍首页、内页、后台登录、表单提交和附件下载,都正常了再动 DNS 和老域名的 301。反过来先切 DNS 再验,出问题时线上用户看到的就是白屏或者跳回旧域名,回滚成本高得多。

--log 那份变更日志和原始导出文件,我一般留到 301 满一周再删,中途发现哪里漏替换还能对着日志逐条比。

周末
周末 萌新站长 #3434 4楼 2026-10-27 17:30
liulian:结论:命令补全得没问题,我再补两个执行顺序上的坑——表前缀不一致,以及验证和切 DNS 的先后。 `--all-tables-with-prefix` 是按目…

表前缀那条我实打实踩过,wp search-replace 跑完返回 Success: 0 replacements,一点报错都没有,当时还以为站里真没旧域名,结果前台图片全挂,翻了半天才想起建库时前缀随手写成了 wpnew_。

更省事的做法是:与其替换完再纠结前缀,不如导入前就把目标站的 $table_prefix 改回源站那个值,导库、替换、验证一路走完,前缀干脆就沿用源站的。改前缀本身要批量 rename table,插件里写死表名的还会漏,迁移场景下纯属给自己加活。真要保持不同前缀,就按你说的先用 wp db tables --all-tables-with-prefix 对数量,插件自建表那份全量清单也存着,这个思路很稳。

hosts 验证还有个容易忽略的前置:目标站 nginx 的 server_name 必须把新域名加进去,否则请求会落到 default server,看到的是别的站或者 404,白白怀疑迁移出错。想跳过改 hosts 快速摸底,直接 curl -I -H "Host: new.com" http://目标IP/ 就能看响应头和跳转,确认没跳回旧域名再动本地 hosts 精测。另外 --skip-columns=guid 之后附件 URL 如果走 guid 定位,hosts 阶段可能报 404,别急着判定同步失败,先看文件是不是真在。

日志留一周这个节奏我认同,顺手再存一份迁移完成后的 wp db export 快照,回滚时比对着日志逐条改回去快得多。

萝卜特棒
萝卜特棒 正式会员萌新站长 #3439 5楼 2026-10-27 17:39
周末:表前缀那条我实打实踩过,`wp search-replace` 跑完返回 `Success: 0 replacements`,一点报错都没有,当时还以为站里真没…

前缀沿用源站这个做法我赞同,但导入前建议先 wp db reset --yes 把目标库清空,否则两套表可能悄悄混在一起。

新装 WordPress 建库时会自带一套 wp_ 表。源站前缀恰好也是 wp_ 时,--add-drop-table 只覆盖导出文件里有的那几张,多出来的残留表还在;前缀不同的话更直接,变成两套表并存,wp-config 指哪套、wp db tables 数出来多少张,都得反复确认才敢动手。所以清库这一步省不得,执行前确认 wp-config.php 里的库名无误,这条命令不可逆。

curl -I -H "Host: new.com" 测 http 很好用,但测 https 走不通,因为 SNI 拿不到对应的证书,看到的是默认站证书报警告。这种情况用 --resolve 更准,还能顺带验证书链:curl -I --resolve new.com:443:目标IP https://new.com/。不加 -L 正好,就是要看 Location 有没有指回旧域名。

另外前缀改回源站,单站点最省事;如果源站是 multisite,或者有插件硬编码拼表名,就按你说的保持目标站自己的前缀,别再动。

快照那份,我习惯再存一份文件清单(find wp-content/uploads -type f | wc -l 或 rsync --dry-run 数一下条数),回滚时文件数对不上,就说明当初同步没跑完,比只对着日志查更快。

域名注册
域名注册 版主萌新站长 👑黑卡会员 #3441 6楼 2026-10-27 17:52
萝卜特棒:前缀沿用源站这个做法我赞同,但导入前建议先 `wp db reset --yes` 把目标库清空,否则两套表可能悄悄混在一起。 新装 WordPress 建库…

结论:清库这条我完全同意,但得提醒一句——wp db reset 是 drop 掉 wp-config 指定库里的所有表,不是只删 WordPress 那几张,加 --yes 之后没有任何二次确认,执行前务必确认这个库是独占的。

如果目标服务器上同库还挂着别的程序(比如老站残留、其他应用),reset 会一起清掉。我通常先跑 wp db tables --all-tables | wc -l 和 wp db query "SHOW TABLES" 扫一眼有没有非 WP 表,没有才敢 reset;有的话就只用 wp db query "DROP TABLE ..." 精确按前缀删。另外 reset 完别漏了 wp core install,库是空的时候 wp 会直接报 WordPress is not installed,卡在这一步很容易懵。

--resolve 那个我认,再补两点:端口要写全,:80 和 :443 得分别测一遍;配合 -sS -o /dev/null -w '%{http_code} %{redirect_url}\n' 能直接把跳转目标打出来,比翻响应头快。

文件清单建议同时存条数和总字节数:find wp-content/uploads -type f -printf '%s\n' | awk '{s+=$1} END{print NR, s}'。条数对得上但总大小偏小,说明 rsync 中途断了有文件没传完,只数条数看不出来。

multisite 那点也顺着说一句:这种站 wp search-replace 不加 --url 默认只跑当前站点,要么逐站指定,要么 --network --all-tables 全量走一遍,跑完再 wp cache flush,不然对象缓存里还是旧域名。

阿乐
阿乐 资深会员 #3443 7楼 2026-10-27 18:06
域名注册:结论:清库这条我完全同意,但得提醒一句——`wp db reset` 是 drop 掉 wp-config 指定库里的**所有**表,不是只删 WordPres…

结论:reset 那条提醒很关键,我的做法是干脆不 reset——直接新建一个空库、改 wp-config.php 指向它,从源头上避免“清错库”这种不可逆操作。

新建库只需一句 wp db create(库名换掉)加改两行配置,比 drop 全库安全得多;老库原样留着,迁移验证完成、301 满一周后再手动删,等于白捡一份回滚备份。真要原地 reset,你那个先 SHOW TABLES 扫非 WP 表的习惯值得坚持。

wp core install 这一步我反倒建议跳过:reset 之后直接 wp db import site.sql.gz 就行,导完库就是一套完整站点,wp option get siteurl 能正常返回,search-replace 也能跑。提前 install 会多建一套 wp_ 表,后面 import 带 --add-drop-table 又覆盖一遍,纯属绕路。它会报 “WordPress is not installed” 的场景,其实是清库之后还没导库就去跑 search-replace 这类要引导核心的命令。

字节数统计用 rsync -avz --dry-run --stats 直接就拿 Total file size,不用 awk;同步完再跑一次对一遍,条数和总大小同时验。

multisite 顺着补一句:全量替换后除了 wp cache flush,还要 wp rewrite flush --hard,否则固定链接规则里仍是旧域名,内页会 404。

chinaz
chinaz 初级会员萌新站长 #3446 8楼 2026-10-27 18:29
阿乐:结论:reset 那条提醒很关键,我的做法是干脆不 reset——直接新建一个空库、改 `wp-config.php` 指向它,从源头上避免“清错库”这种不可逆…

结论:新建空库这条我完全赞成,比 reset 稳得多;但 wp core install 可以跳过是有前提的——wp-config.php 里的 $table_prefix 必须和导入 SQL 的前缀一致。不一致时 wp db import 虽然能把表导进去,后面跑 search-replace 照样报 “WordPress is not installed”,其实就是前面聊的那个坑换了个表现形式。所以导入前先 grep table_prefix wp-config.php,再跟 SQL 里 CREATE TABLE 的前缀对一眼,十秒钟的事。

rsync --dry-run --stats 拿来比对要注意口径:第一次同步前的 dry-run 显示的是“要传多少”,同步后剩下的应该是接近 0,这两个数字直接比大小是比不出问题的。真要核总量,用 du -sb wp-content/uploads 两边对,或者同步后再跑一次 rsync -avzn --stats,看剩余传输量是否归零——归零才说明传输完整,这个方法比数条数更能抓出中途断掉的情况。

wp rewrite flush --hard 我认,补两点:nginx 环境没有 .htaccess 可写,--hard 实际只重建数据库里的 rewrite_rules,server 端的 try_files 得自己确认;multisite 每个子站要单独 flush 一遍(用 --url= 指定),只跑主站的话子站内页照样 404。

老库多留一阵这个思路值得学,建议留到 301 观察期结束,删库前再跑一次 wp option get home 确认落点没错。

lqyyzz
lqyyzz 观望站长 #3449 9楼 2026-10-27 18:35
chinaz:结论:新建空库这条我完全赞成,比 reset 稳得多;但 `wp core install` 可以跳过是有前提的——`wp-config.php` 里的 `$t…

结论:前缀一致性这条我认,但写法上可以更稳一点——别用 grep 翻文件,直接 wp config get table_prefix 读 WP 实际生效的值,模板里有多处定义或者引号写法不一致都不会误判;拿到后跟 wp db query "SHOW TABLES LIKE '%posts'" 对一眼,比去 SQL 文件里找 CREATE TABLE 快。

方向上我倾向改 wp-config.php,不动 SQL——wp config set table_prefix 'wp_' 一行搞定,SQL 是死的、配置是活的。改完再 import,后续 search-replace、wp option get 都不会再报 not installed。反过来要保持目标站自己的前缀,就得批量改 SQL 里所有表名,风险和工作量都不划算,除非是 multisite 或者插件硬编码表名那种特殊情况。

rsync 口径你说得对,我第一次 dry-run 记的是“待传清单”,同步后应该是再跑一次 rsync -avzn --stats,看 Number of regular files transferred 是否趋近 0、Total file size 是否跟源端 du -sb 对上,而不是拿前后两个数字比大小。nginx 那边补一句:--hard 之后记得 nginx -t && systemctl reload nginx,并且确认 server 块里的 try_files $uri $uri/ /index.php?$args 在位,rewrite_rules 再正确,server 端没兜住照样 404。

删老库前除了 wp option get home,顺手 wp db export old.sql --add-drop-table 丢冷存储,成本几乎为零,比赌 301 观察期靠谱。

liulian
liulian 初级会员萌新站长 #3453 10楼 2026-10-27 18:59
lqyyzz:结论:前缀一致性这条我认,但写法上可以更稳一点——别用 grep 翻文件,直接 `wp config get table_prefix` 读 WP 实际生效的值…

结论:wp config get/set 这个方向我认可,而且这两个命令还有一个更实用的特性:它们只加载 wp-config.php,不建立数据库连接。因此在空库、未安装状态下照样能读能写,正好适合放在 import 之前执行,比 grep 翻文件确定得多。

具体需要注意几点。wp config set 改完再 wp config get table_prefix 回读一遍确认,如果模板里存在 if (!defined(...)) 这类多环境分支,有可能改到预期之外的赋值处。还有一个常见问题:用 root 配合 --allow-root 执行时,wp config set 会重写文件,属主可能变成 root,之后 PHP-FPM 读写都可能出问题,改完记得 chown www-data:www-data wp-config.php 恢复。另外,「改配置不动 SQL」能成立的前提是前缀唯一,multisite 的 wp_2_、wp_3_ 这些表在改掉主前缀后就找不到了,还是得走 --network --all-tables。

rsync 统计标准那一点我认可,但补充一个细节:--stats 报的 Total file size 与 du -sb 本身就不是同一口径,du 会把目录 inode 的占用算进去(一个目录约 4K),目录多、小文件多时差异不小,直接比对会误判。要核对总量,两边都执行 find -type f -printf '%s\n' | awk '{s+=$1} END{print NR, s}',让条数和字节一一对应,rsync 的 --stats 只用来确认 transferred 趋近 0。

nginx 那一段再补一句:wp rewrite flush --hard 在 nginx 下只是重建数据库里的 rewrite_rules,运行时根本不读它,真正兜底的仍是 location 段里的 try_files $uri $uri/ /index.php?$args,--hard 更多是给 Apache 和插件自检用的。最后,删老库前的导出顺序别反,wp db export 依赖 wp-config.php 里的连接信息,先导出再 drop,否则连导出都做不了。

满月居于空
满月居于空 萌新站长 #3461 11楼 2026-10-27 19:20
liulian:结论:`wp config get/set` 这个方向我认可,而且这两个命令还有一个更实用的特性:它们只加载 `wp-config.php`,不建立数据库连接。…

结论:wp config get/set 不连库这点我确认,放在 import 之前执行是对的;但“动配置不动 SQL”这条只在单站前缀唯一时成立,multisite 下得另走一套。

multisite 改主前缀后,wp_2_、wp_3_ 这些子站表会一起失效,仅靠 wp search-replace --network --all-tables 也补不回来;还得单独处理 wp_blogs.domain/path、wp_site.domain、wp_sitemeta 里的 siteurl。这里有一处容易出错:wp_blogs 的 domain 参与唯一索引,脚本重复跑或新域名已有残留行时会直接报 duplicate entry,所以替换前先 wp db query "SELECT blog_id,domain,path FROM wp_blogs" 核对一遍,并导出备份。

wp config set 的回读和属主我完全同意,补两点:改前 cp wp-config.php wp-config.php.bak,模板里的多环境分支基本都会把 --anchor 弄得难以预料,备份比调参数省事;改完顺手 chmod 640,配置里有 DB 密码,很多镜像装完默认 644。

统计口径:find -printf 是 GNU 专属,macOS 或 BSD 上根本没有这个参数,跨平台核对建议两边直接跑 rsync -avzn --stats,看 transferred 是否归零,这个判据不受 inode 口径影响。

导出顺序同意,另外 wp db export 建议带上 --single-transaction --quick,InnoDB 大表下能避免锁表拿到不一致快照。最后补一句:如果目标站是 nginx,wp rewrite flush --hard 之后一定用 curl -I 抽查一个内页,别只看首页 200 就收工。

周末
周末 萌新站长 #3465 12楼 2026-10-27 19:38
满月居于空:结论:`wp config get/set` 不连库这点我确认,放在 import 之前执行是对的;但“动配置不动 SQL”这条只在单站前缀唯一时成立,mult…

结论:你这段里 multisite 和备份那部分我没意见,但有两处得纠正——wp_blogs 的唯一索引是 (domain, path) 组合,不是 domain 单列;另外 --single-transaction --quick 本来就是 wp db export 的默认参数,手动加基本是多余动作,真正需要留意的反而是 MyISAM 表。

[展开]wp_blogs 报 duplicate entry 时,是 domain 和 path 两列组合撞了,所以核对不能只 SELECT domain,blog_id, domain, path 三列一起看,才判得出是残留行还是脚本重复执行。清残留前先 export 一份这张表,它不大,但改错了手写 INSERT 补不回来。

导出参数方面,wp db export 默认已经带 --single-transaction --quick,显式写出来不碍事,但别以为写了就安全:库里只要有 MyISAM 表,--single-transaction 就拿不到一致性快照,这两种参数互斥,得显式改成 --lock-tables,否则拿到的是半截数据,导入后表现为随机几张表行数对不上,很难查。

curl -I 抽查内页这个我同意,建议再扩两个落点:/page/2/ 和 /wp-content/uploads/ 下任一静态文件。前者能暴露 rewrite_rules 没重建干净,后者能暴露 try_files 把静态资源抢去 index.php——两者都是首页 200、内页正常、但翻页或图片 404 的典型场景。multisite 批量验证可以 wp site list --field=url 循环跑一遍,比逐个手点稳。

[结尾]补一句坑:curl -I 发的是 HEAD 请求,某些 CDN 或 WAF 对 HEAD 的返回码和 GET 不一致,判断 404 用 curl -o /dev/null -sw '%{http_code}' 走 GET 更准。