WordPress 经典编辑器代码块无损迁移:批量转换 SyntaxHighlighter 短代码为 Gutenberg 区块
一、发现问题
最近我在使用 Polylang + AutoPoly 插件对博客进行多语言翻译时,发现一个奇怪的现象:
– 用 Gutenberg 编辑器插入的代码块(SyntaxHighlighter 区块),翻译后的英文文章中代码格式完好无损,缩进、尖括号等都正常。
– 而用经典编辑器通过短代码插入的代码块,例如 `php`,翻译后却出现了严重的问题:尖括号被转义成 `<` 和 `>`,缩进丢失,甚至代码结构被破坏。(如图1、图2)
![用经典编辑器通过短代码插入的代码块,例如 `[php]...[/php]`,翻译后却出现了严重的问题:尖括号被转义成 `<` 和 `>`,缩进丢失,甚至代码结构被破坏。(如图1)](https://www.shuijingwanwq.com/wp-content/uploads/2026/06/1-11.png)
![用经典编辑器通过短代码插入的代码块,例如 `[php]...[/php]`,翻译后却出现了严重的问题:尖括号被转义成 `<` 和 `>`,缩进丢失,甚至代码结构被破坏。(如图2)](https://www.shuijingwanwq.com/wp-content/uploads/2026/06/2-8.png)
经过对比分析,我找到了根本原因:AutoPoly 翻译插件在解析文章内容时,能够正确识别 Gutenberg 的区块标记(
wp:syntaxhighlighter/code
),从而不对其内部代码进行翻译或转义处理;但对于经典编辑器中的短代码(`php` 等),插件无法准确判断其边界,于是试图翻译短代码内的内容,导致代码被破坏。(如图3)
![对于经典编辑器中的短代码(`[php][/php]` 等),插件无法准确判断其边界,于是试图翻译短代码内的内容,导致代码被破坏。(如图3)](https://www.shuijingwanwq.com/wp-content/uploads/2026/06/3-9.png)
二、手动验证替换方案
为了确认解决方案是否可行,我决定先手动对一篇文章进行测试。我选择了一篇包含 `php` 代码块的中文文章,在代码编辑器中将短代码手动替换为 Gutenberg 区块标记。
原始短代码形式:(如图4)

替换后的 Gutenberg 区块标记:(如图5)

保存文章后,我删除了对应的英文文章,并让 AutoPoly 重新翻译。结果令人满意:新生成的英文文章中,代码块格式与中文完全一致,没有出现任何转义或错乱。(如图7、图9)


我又用 python、go、text 等短代码分别进行了同样的手动测试,结果全部通过。这证明:将短代码替换为 Gutenberg 区块标记,可以彻底解决翻译时代码块被破坏的问题。
然而,我有数百篇文章使用了经典编辑器,逐个手动替换显然不现实。于是我决定编写一个批量处理方案。经过研究,我实现了“PHP 函数 + WordPress 批量操作”的方法,可以在文章列表页手动勾选需要转换的文章,一键完成替换。
下面将完整方案分享给大家。
三、解决方案概述
将文章中所有 SyntaxHighlighter 短代码(如 php、python)批量转换为 Gutenberg 区块标记,然后让翻译插件重新生成英文文章。该方法支持 30+ 种编程语言,并能自动处理别名映射(如 golang → go、javascript → js)。
四、操作步骤
第一步:备份数据库(非常重要)
在进行任何批量操作前,请务必备份你的 WordPress 数据库。推荐使用 UpdraftPlus 插件,或在主机管理面板的 phpMyAdmin 中导出。
第二步:将以下代码添加到主题的 functions.php
登录 WordPress 后台,进入 外观 → 主题文件编辑器,找到 functions.php 文件,将下面的代码粘贴到文件末尾(注意:如果文件末尾已有 ?> 标记,请把代码放在 ?> 之前)。(如图12)

// 添加批量操作选项
function add_bulk_operation_convert_shortcode( $bulk_actions ) {
$bulk_actions['convert_shortcode_to_block'] = '转换短代码为区块';
return $bulk_actions;
}
add_filter( 'bulk_actions-edit-post', 'add_bulk_operation_convert_shortcode' );
function handle_bulk_operation_convert_shortcode( $sendback, $action, $post_ids ) {
if ( $action !== 'convert_shortcode_to_block' ) return $sendback;
global $wpdb;
$languages = ['bash','shell','cpp','c','css','go','golang','java','js','javascript','perl','pl','php','plain','text','ps','powershell','python','ruby','sql','xml','xhtml','html','yaml','lua'];
$converted_count = 0;
$skipped_count = 0;
foreach ( $post_ids as $post_id ) {
$post = get_post( $post_id );
if ( ! $post ) continue;
$old_content = $post->post_content;
$new_content = $old_content;
$changed = false;
foreach ( $languages as $lang ) {
$open = '[' . $lang . ']';
$close = '[/' . $lang . ']';
$pos = 0;
while ( ($open_pos = strpos( $new_content, $open, $pos )) !== false ) {
$close_pos = strpos( $new_content, $close, $open_pos );
if ( $close_pos === false ) break;
$code_start = $open_pos + strlen( $open );
$code = substr( $new_content, $code_start, $close_pos - $code_start );
$code = html_entity_decode( $code, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
if ( in_array( $lang, ['text','plain'] ) ) {
$block = "<!-- wp:syntaxhighlighter/code -->\n<pre class=\"wp-block-syntaxhighlighter-code\">\n{$code}\n</pre>\n<!-- /wp:syntaxhighlighter/code -->";
} else {
$alias = $lang;
if ( $alias === 'golang' ) $alias = 'go';
if ( $alias === 'javascript' ) $alias = 'js';
if ( $alias === 'pl' ) $alias = 'perl';
$block = "<!-- wp:syntaxhighlighter/code {\"language\":\"{$alias}\"} -->\n<pre class=\"wp-block-syntaxhighlighter-code\">\n{$code}\n</pre>\n<!-- /wp:syntaxhighlighter/code -->";
}
$new_content = substr_replace( $new_content, $block, $open_pos, $close_pos + strlen( $close ) - $open_pos );
$changed = true;
$pos = $open_pos + strlen( $block );
}
}
if ( $changed ) {
$wpdb->update( $wpdb->posts, ['post_content' => $new_content], ['ID' => $post_id], ['%s'], ['%d'] );
$converted_count++;
} else {
$skipped_count++;
}
}
$sendback = add_query_arg( ['converted_count' => $converted_count, 'skipped_count' => $skipped_count], $sendback );
return $sendback;
}
add_filter( 'handle_bulk_actions-edit-post', 'handle_bulk_operation_convert_shortcode', 10, 3 );
function bulk_operation_convert_shortcode_notices() {
if ( isset( $_REQUEST['converted_count'] ) || isset( $_REQUEST['skipped_count'] ) ) {
$converted = intval( $_REQUEST['converted_count'] ?? 0 );
$skipped = intval( $_REQUEST['skipped_count'] ?? 0 );
echo '<div class="notice notice-success is-dismissible"><p>' .
sprintf( '转换完成!%d 篇文章已更新,%d 篇文章无需更新。', $converted, $skipped ) .
'</p></div>';
}
}
add_action( 'admin_notices', 'bulk_operation_convert_shortcode_notices' );
更新文件。
第三步:在文章列表页执行批量转换
1. 刷新 WordPress 后台的“所有文章”页面。
2. 勾选你需要转换的文章(可以多选)。(如图13)
3. 在顶部的“批量操作”下拉菜单中,选择“转换短代码为区块”。
4. 点击“应用”按钮。

系统会自动处理,完成后会在页面顶部显示转换结果(例如:转换完成!1 篇文章已更新,0 篇文章无需更新。)。
第四步:排坑记录:反斜杠丢失与 `>` 双重转义
在实现批量转换的过程中,我遇到了三个典型的“陷阱”,花了不少时间才彻底解决。记录下来,希望能帮助遇到类似问题的读者。
坑一:反斜杠 `\` 神秘失踪
我的测试文章中有一段 PowerShell 路径:`PS E:\wwwroot\object> bash test.sh`。执行批量转换后,反斜杠全部消失了,变成了 `PS E:wwwrootobject> bash test.sh`。
经过反复排查,原因出在两个方面:
1. 正则表达式:使用 `preg_replace_callback` 配合 `(.*?)` 捕获代码内容时,PHP 的正则引擎会对反斜杠进行转义处理。如果原始内容中包含 `\w` 这类组合,可能会被解释为转义序列而丢失。
2. WordPress 自动转义:使用 `wp_update_post` 保存文章时,WordPress 会自动调用 `wp_slash` 对内容中的反斜杠再次转义,导致保存后变成双反斜杠或直接丢失。
解决方案:放弃正则表达式,改用 `strpos` + `substr_replace` 进行字符串查找替换。同时,使用 `$wpdb->update` 直接写入数据库,完全绕过 `wp_update_post` 的自动转义机制。
坑二:`>` 变成 `>`
代码块中常见的 `>`(表示大于号)在转换后变成了 `>`,浏览器显示为 `>` 文本而不是 `>`。
原因是:原始文章内容中的 `>` 是 HTML 实体,在保存到数据库时,WordPress 或浏览器会将 `&` 字符转义为 `&`,从而导致双重转义。
解决方案:在生成 Gutenberg 区块之前,对捕获到的代码内容执行一次 `html_entity_decode`,将所有 HTML 实体还原为原始字符。这样保存后,`>` 就变成了 `>`,浏览器能正确显示为 `>`。
最终代码要点:
– 字符串查找替换(避免正则反斜杠问题)
– `html_entity_decode`(解决实体双重转义)
– `$wpdb->update`(绕过 WordPress 自动斜杠处理)
经过以上修正,批量转换后的代码块格式完整,反斜杠和特殊符号均能正确保留。
坑三:批量转换后的代码块格式符合预期,但是在网页中的一些代码块中会显示 `wp-block-syntaxhighlighter-code`。在代码编辑器中查看,未发现问题。如图16

在 可视化编辑器 中查看 。提示:区块包含未预料的或无效的内容。点击 尝试恢复,恢复后的代码与原始代码不一致。
原始代码如下:
<?php
use yii\db\Migration;
/**
* Class m180620_105204_update_table_options_to_log
*/
class m180620_105204_update_table_options_to_log extends Migration
{
/**
* {@inheritdoc}
*/
public function safeUp()
{
$tableOptions = null;
if ($this->db->driverName === 'mysql') {
// http://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci
$tableOptions = 'CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB';
$this->execute('ALTER TABLE {{%user}} ROW_FORMAT=DYNAMIC');
$this->execute('ALTER TABLE {{%user}} CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci');
$this->execute('ALTER TABLE {{%log}} ROW_FORMAT=DYNAMIC');
$this->execute('ALTER TABLE {{%log}} CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci');
}
$this->addCommentOnTable('{{%user}}', '用户', $tableOptions);
$this->addCommentOnTable('{{%log}}', '日志', $tableOptions);
}
/**
* {@inheritdoc}
*/
public function safeDown()
{
echo "m180620_105204_update_table_options_to_log cannot be reverted.\n";
return false;
}
/*
// Use up()/down() to run migration code without a transaction.
public function up()
{
}
public function down()
{
echo "m180620_105204_update_table_options_to_log cannot be reverted.\n";
return false;
}
*/
}
恢复后的代码如下,一些代码丢失:
db->driverName === 'mysql') {
// http://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci
$tableOptions = 'CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB';
$this->execute('ALTER TABLE {{%user}} ROW_FORMAT=DYNAMIC');
$this->execute('ALTER TABLE {{%user}} CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci');
$this->execute('ALTER TABLE {{%log}} ROW_FORMAT=DYNAMIC');
$this->execute('ALTER TABLE {{%log}} CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci');
}
$this->addCommentOnTable('{{%user}}', '用户', $tableOptions);
$this->addCommentOnTable('{{%log}}', '日志', $tableOptions);
}
/**
* {@inheritdoc}
*/
public function safeDown()
{
echo "m180620_105204_update_table_options_to_log cannot be reverted.\n";
return false;
}
/*
// Use up()/down() to run migration code without a transaction.
public function up()
{
}
public function down()
{
echo "m180620_105204_update_table_options_to_log cannot be reverted.\n";
return false;
}
*/
}
临时解决方案为 从网页源代码中复制原始代码至 可视化编辑器 中,保存。
第五步:移除临时代码
批量转换完成后,请务必回到 functions.php,删除刚才添加的所有代码。这样做有两个好处:
– 避免日后误操作;
– 保持主题文件的整洁。
第六步:重新生成翻译文章
如果你使用了 Polylang + AutoPoly:
1. 删除之前因短代码问题导致的英文文章(或将其移至回收站)。
2. 重新执行翻译(AutoPoly 会自动根据新的中文文章内容生成英文版本)。
3. 此时你会发现,英文文章中的代码块格式与中文完全一致,不会再出现转义或错乱问题。
五、支持的语言列表
代码中已包含以下语言(可根据需要自行增删):
bash, shell, cpp, c, css, go, golang, java, js, javascript, perl, pl, php, plain, text, ps, powershell, python, ruby, sql, xml, xhtml, html, yaml, lua
注意:plain 和 text 会转换为不指定 language 属性的纯文本块;golang 自动映射为 go,javascript 映射为 js,pl 映射为 perl。
六、常见问题
Q1:执行批量操作后,文章内容没有变化?
可能文章本身没有使用短代码,或者短代码的语言标签不在支持列表中。可以检查代码中的 $languages 变量,确保已包含你使用的所有标签。
Q2:转换后代码块的语法高亮失效?
请确保你仍然安装了 SyntaxHighlighter Evolved 插件,并且版本支持 Gutenberg 区块。该插件从 3.x 版本开始已经兼容区块编辑器。
Q3:我的代码块是 code 这种无语言标签的形式,怎么办?
你可以将代码中的语言列表改为 code,然后在替换回调中统一处理为不带 language 属性的区块。
Q4:能不能只替换特定分类下的文章?
可以。在第三步“勾选文章”时,你可以先通过分类筛选,然后全选当前分页,再执行批量操作。
七、总结
通过 PHP 函数 + WordPress 原生批量操作,我们实现了对经典编辑器中 SyntaxHighlighter 短代码的精准、批量转换。整个过程无需安装额外插件,安全可控,完美解决了多语言翻译时代码块格式错乱的问题。