本文约 6,642 字,预计阅读 17 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:正文里那堆方括号,其实就是失效的短代码
换主题后访客最先发现的异常,往往是正文里凭空多出一串方括号:[row][col size="6"]产品介绍[/col][/row]。站长在编辑器里看不到任何异常,因为编辑器里这块内容是通过旧主题的短代码渲染的,现在渲染逻辑没了,只剩原始标记被当成纯文本输出。
这类残留的麻烦之处在于分布广、形态杂。一个用了五年的站点,短代码可能出现在三种地方:正文 post_content、自定义字段 postmeta(尤其是页面构建器留下的)、以及小工具文本。而且同一套短代码在不同时期的写法还不一样——早期是 [col size=6](不带引号),后来变成 [col size="6"],再后来加了 [col size="6" class="mt-2"]。想用一条简单的替换语句清干净,几乎不可能。
处理顺序上有个重要原则:先兜底、再清理。兜底是让站点立刻恢复正常显示,清理是长期把内容里的历史包袱去掉。跳过兜底直接清理,风险极高——一旦替换语句写错,正文就永久损坏了。
原理分析:短代码是怎么被解析的,失效后为什么变明文
WordPress 在输出 the_content() 时,会经过 do_shortcode() 过滤器。它的工作流程是:用 get_shortcode_regex() 生成一条正则,扫描正文里所有形如 [name attr="v"]...[/name] 的结构;对每个命中项,去全局的 $shortcode_tags 里查找 name 对应的回调函数;找到了就调用回调替换成 HTML,找不到就原样返回。
所以「失效」的表现分两种:
| 情况 | 表现 | 原因 |
|---|---|---|
| 短代码未注册 | [row] 方括号原样显示 |
$shortcode_tags 里没有 row |
| 回调返回空 | 方括号消失,内容也消失 | 回调存在但逻辑依赖旧主题 |
| 嵌套未递归 | 外层正常,内层 [col] 明文 |
回调里没调用 do_shortcode( $content ) |
| 属性名变化 | 显示出来了但参数没生效 | 新回调的 shortcode_atts 字段不同 |
第二种最危险——它不发警告,内容静默消失。第三种最常见,因为很多主题作者写短代码时忘了递归处理嵌套内容。
另外要区分两类短代码的归属:WordPress 核心自带的(、、、、、)换任何主题都还在;插件提供的([contact-form-7 id="123"]、WooCommerce 的 [products])只要插件还在就还在;主题定义的([row]、[col]、[button]、[testimonial])换主题即失效。清理的目标只针对第三类,前两类千万别动——误删 [contact-form-7] 会让所有表单消失。
保姆级实操
步骤 1:先全量盘点到用了哪些短代码
# 用 WP-CLI 把正文里出现的所有方括号标签抓出来(需 MySQL 8 支持 REGEXP_SUBSTR)
wp db query "
SELECT REGEXP_SUBSTR(post_content, '\\\\[[a-zA-Z0-9_-]+') AS tag, COUNT(*) AS cnt
FROM wp_posts
WHERE post_status IN ('publish','draft','private')
AND post_content LIKE '%[%'
GROUP BY tag ORDER BY cnt DESC LIMIT 50;" --skip-column-names
没有 WP-CLI 时,用 PHP 借助核心自带的正则函数一次性统计,准确率更高:
<?php
// 放在主题临时执行,或用 wp eval-file 运行
global $wpdb;
$rows = $wpdb->get_col(
"SELECT post_content FROM {$wpdb->posts}
WHERE post_status IN ('publish','draft','private')
AND post_content LIKE '%[%'"
);
$found = array();
foreach ( $rows as $content ) {
if ( preg_match_all( '/' . get_shortcode_regex() . '/s', $content, $m ) ) {
foreach ( $m[2] as $tag ) {
$found[ $tag ] = ( $found[ $tag ] ?? 0 ) + 1;
}
}
}
arsort( $found );
foreach ( $found as $tag => $cnt ) {
$registered = shortcode_exists( $tag ) ? '已注册' : '未注册(失效)';
printf( "%-24s %-14s %d\n", '[' . $tag . ']', $registered, $cnt );
}
get_shortcode_regex() 是核心函数,用它能保证和 do_shortcode() 用同一套规则,比手写正则可靠得多。输出里标「未注册」的就是要处理的对象。
步骤 2:先做兼容层兜底(不要直接改内容)
在插件或新主题里注册同名短代码,把旧标记翻译成新主题的 HTML。这一步让站点立刻恢复正常,且完全不触碰数据库:
<?php
/**
* Plugin Name: 旧主题短代码兼容层
* Description: 兼容历史 [row] [col] [button] 短代码,便于后续平滑清理
*/
add_shortcode( 'row', 'mmf_legacy_row' );
function mmf_legacy_row( $atts, $content = null ) {
return '<div class="mmf-grid">' . do_shortcode( $content ) . '</div>';
}
add_shortcode( 'col', 'mmf_legacy_col' );
function mmf_legacy_col( $atts, $content = null ) {
$atts = shortcode_atts( array(
'size' => '12',
'class' => '',
), $atts, 'col' );
$size = absint( $atts['size'] );
$class = trim( preg_replace( '/[^a-z0-9_\- ]/i', '', $atts['class'] ) );
return sprintf(
'<div class="mmf-grid__col--%d %s">%s</div>',
$size,
esc_attr( $class ),
do_shortcode( $content )
);
}
add_shortcode( 'testimonial', 'mmf_legacy_testimonial' );
function mmf_legacy_testimonial( $atts, $content = null ) {
$atts = shortcode_atts( array( 'name' => '' ), $atts, 'testimonial' );
return sprintf(
'<blockquote class="mmf-quote"><p>%s</p><cite>%s</cite></blockquote>',
do_shortcode( $content ),
esc_html( $atts['name'] )
);
}
注意每个回调都必须对 $content 再调用一次 do_shortcode(),否则嵌套结构会残留。
步骤 3:确认兼容层稳定后,再做批量替换
替换前必须完整备份,并用 --dry-run 先看影响范围:
# 备份
wp db export /tmp/before-shortcode-cleanup.sql
# 干跑:统计会命中多少条
wp search-replace '[row]' '' --include-columns=post_content --dry-run
wp search-replace '[/row]' '' --include-columns=post_content --dry-run
确认数量合理后再执行。带属性的短代码无法用 search-replace 处理,需要写脚本:
<?php
// 用 wp eval-file cleanup.php 执行,务必先备份
global $wpdb;
$posts = $wpdb->get_results(
"SELECT ID, post_content FROM {$wpdb->posts}
WHERE post_content LIKE '%[col %'
OR post_content LIKE '%[/col]%'"
);
$changed = 0;
foreach ( $posts as $post ) {
$new = preg_replace_callback(
'/\[col([^\]]*)\](.*?)\[\/col\]/s',
function( $m ) {
// 抽出 size 属性,转成新主题的 div
preg_match( '/size=["\']?(\d+)["\']?/', $m[1], $size );
$n = isset( $size[1] ) ? $size[1] : '12';
return sprintf(
'<div class="mmf-grid__col--%d">%s</div>',
(int) $n,
$m[2]
);
},
$post->post_content
);
if ( $new !== $post->post_content ) {
wp_update_post( array(
'ID' => $post->ID,
'post_content' => $new,
) );
$changed++;
}
}
printf( "已清理 %d 篇文章中的 col 短代码\n", $changed );
wp_update_post() 会触发修订版本与缓存刷新,比直接写 SQL 安全。数据量特别大时(上万篇)才考虑直接 SQL 更新,且要同步处理 post_modified 与缓存。
步骤 4:清理自定义字段与小工具里的残留
-- 查看自定义字段里的短代码残留
SELECT post_id, meta_key
FROM wp_postmeta
WHERE meta_value LIKE '%[row]%'
OR meta_value LIKE '%[/col]%'
LIMIT 50;
小工具(WordPress 5.8 之后是区块)内容存在 wp_posts 里 post_type = 'wp_block' 或 wp_global_styles,也可能在 wp_options 的 widget_* 记录里,需要单独检查:
SELECT option_name FROM wp_options
WHERE option_value LIKE '%[row]%' AND option_name LIKE 'widget_%';
步骤 5:清完后把兼容层留一段时间再移除
兼容层的作用是「即使有漏网之鱼也不会显示成明文」。建议保留三个月,期间配合全站抓取(Screaming Frog 里搜索 [ 出现在正文的页面)确认零残留后再删除插件。
常见踩坑
- 用
strip_shortcodes()直接剥掉:这个函数会把短代码连同包裹的内容一起删除,[button text="立即咨询"]虽然没了括号,但按钮本身也消失了。 - 正则里忘了
s修饰符:.*?默认不匹配换行,跨行的嵌套短代码一个都匹配不到,清理后看起来「没变化」。 - 误伤核心与插件短代码:
、、[contact-form-7]必须排除,否则表单和相册全废。用shortcode_exists()做白名单过滤最稳。 - 直接改 SQL 却没清缓存:
wp_update_post()之外的写法要手动clean_post_cache(),否则前台仍显示旧内容。 - 案例: 某站点用一条
UPDATE wp_posts SET post_content = REPLACE(post_content, '[row]', '')清理,结果发现带属性变体的[row class="full"]一个都没清掉,正文里仍然残留方括号;更糟的是另一条语句把[column](页面构建器的短代码,当时插件仍在用)也替换了,导致 200 多篇页面的三栏布局塌陷成单栏。教训:替换前必须先用shortcode_exists()把仍在生效的短代码排除,且属性变体必须用正则而非字符串替换。 - 忽略了修订版本:
wp_posts里post_type = 'revision'的记录也含有旧短代码,虽然不影响前台,但会干扰统计。统计时加AND post_type = 'post'条件。 - 清理后没重新生成站点地图:内容变了要触发一次 sitemap 更新,便于搜索引擎尽快重新抓取。
总结
短代码残留的处理顺序固定为:盘点(用 get_shortcode_regex() + shortcode_exists() 区分失效与生效)→ 兼容层兜底(注册同名短代码,回调内递归 do_shortcode())→ 备份后批量替换(简单形式用 wp search-replace,带属性变体用正则脚本)→ 全站复查 → 三个月后移除兼容层。
判断标准:全站抓取结果里,正文出现未闭合方括号的页面数为零;且核心短代码与插件短代码一个没少。清理属于内容层手术,务必放在 主题迁移到新主题时内容保全方案 之后执行,确认 CPT 与字段都已平移完毕再动手。
