☰
Confluence迁移CKEditor乱码难题:从编码到HTML结构清洗方案
2026/10/5 2:58:29 网站建设 项目流程

这段时间我们团队在把Confluence上的产品文档整体迁到自研内容平台,编辑器换成了CKEditor。原以为只是数据的搬运,结果第一批导入就在界面上出现大面积“乱码”。最典型的还不是网上教程里常说的“锟斤拷”,而是另一种更让人头疼的情形:编码从头到尾都是UTF-8,中文既没变方块也没变问号,可文字排列错乱,段落挤成一团,表格边界消失,有些代码块直接丢行。折腾两天后我才意识到,从Confluence到CKEditor的“乱码”,绝大多数时候不是字符编码问题,而是HTML结构假设不一致造成的。

这篇文章就从现象分类开始,逐步拆解Confluence导出HTML、CKEditor过滤规则、以及最终的清洗方案。准备做文档平台迁移的朋友,可以直接把这篇当成踩坑手册来用。

1. 先分清乱码的三种长相,避免第一轮排查跑偏

1.1 真正的字符编码乱码:锟斤拷与双重编码

先说最常见的经典乱码。从Confluence页面复制内容到CKEditor,如果中间环节的字符集处理出错,你会看到这几类现象:

  • 中文变成“锟斤拷”“锘挎嫄”这类完全不认识的汉字组合;
  • 英文数字正常,中文变成一串“?”,甚至直接消失;
  • 出现“锓—这种拉丁字母加符号的组合,看着像法语乱入了。

这些乱码的根源几乎都在“字节流解释错了一层”。Confluence是Java应用,页面输出默认是UTF-8;CKEditor跑在浏览器里,正常情况下也用UTF-8。问题往往出在中间那层:代理、上传接口、文件编辑器、数据库连接,只要有一个环节用GBK/GB2312去解码UTF-8的字节流,中文就会变成“锟斤拷”。更隐蔽的是双重编码:UTF-8的字节先被当成Latin-1解码成“é”,再被转回UTF-8存库,于是你无论怎么调页面charset都修不好。

排查方法很简单:把Confluence导出的HTML用Notepad++或VS Code打开,看右下角编码是不是UTF-8;再用浏览器直接打开该HTML文件,如果浏览器显示正常,那就说明源文件本身没问题,问题出在导入链路的某一个环节。这时可以逐级检查HTTP响应头的Content-Type、后端框架的CharacterEncodingFilter、数据库连接串里的characterEncoding参数,以及数据库表字段的collation。我见过最离谱的一次,是文件上传组件把文件名编码成GBK,导致整个上传请求的body被容器用GBK提前解了一遍,后面怎么改代码都没用。

1.2 结构性乱码:文字挤在一起、表格散架、代码块消失

第二种“乱码”最迷惑人,因为它根本不是编码问题。表现是:

  • 整篇文档读起来像一坨泥,段落之间的空行全部消失,每行之间没有换行;
  • 表格边框还在,但单元格内容被揉成一团,跨行跨列错位;
  • 列表项目符号消失,只剩一串带着缩进的文字;
  • 代码块变成普通段落,部分代码行直接丢失;
  • 原本的提示框、目录、子页面列表,变成一块空白或残缺文本。

我在排查时用浏览器直接打开导出HTML,页面显示完全正常;用CKEditor实例setData后,内容就是上面这副鬼样子。这说明问题出在“HTML解释器对标签的理解”上。Confluence导出的HTML不是纯净的HTML,里面混着大量宏标签和自定义命名空间,CKEditor拿到之后按自己的过滤规则处理,识别不了的标签直接删除,但删除的是整个宏区块,内部正文被错误拼接,于是读者看到的就是“乱码”。

判断技巧:先看源码,用Ctrl+U查看Confluence导出HTML的源代码,搜索ac:前缀的标签。只要看到ac:structured-macro、ac:parameter、ri:attachment这些字样,基本可以断定问题不是编码,而是结构清洗不到位。

1.3 文字编码没问题,为什么还会显示“方块”

还有第三种比较容易误判的情况:显示成方块。如果产品文档里包含emoji、生僻字、特殊符号,而CKEditor所在系统的字体不支持,就会显示成方块。这种方块和乱码不一样——你刷新、复制、切到源码视图,它都是正常的字符,只是视觉上显示不出来。排查时把这段文字复制到系统自带记事本里看,如果正常,那就是字体问题,要么换字体,要么改CSS的font-family。虽然这类问题不常见,但建议排查顺序里先排除它,免得后面绕圈子。

提示:遇到乱码先别急着改编码。先用浏览器直接打开源HTML确认文件本身是否正常,再判断是解释者的问题。这一步能帮你省掉至少半天时间。

2. Confluence导出的HTML,其实是“披着HTML外衣的XHTML”

2.1 存储宏让导出HTML带了一堆“私货”

Confluence内部有一套自己的内容存储格式,官方叫Storage Format,本质是XHTML加上大量扩展标签。你在页面上看到的“提示框”“代码块”“附件”“目录”“子页面列表”,在存储层并不是标准HTML,而是ac:前缀的宏标签包裹。导出HTML时,Confluence会根据导出方式决定保留宏标签还是渲染成标准HTML。但在我实际遇到的情况里,很多导出渠道出来的是“半成品”:既保留了宏标签,又渲染了一部分内容,结果两边不靠。

举例来说,一个简单的“提示宏”,导出HTML里可能是这样的:

<ac:structured-macro ac:name="tip" ac:schema-version="1"> <ac:rich-text-attribute ac:name="text"> <p>这里是提示框内容,需要注意xxx</p> </ac:rich-text-attribute> <ac:parameter ac:name="title">提示</ac:parameter> </ac:structured-macro>

这段代码在Confluence系统里能正常显示,是因为Confluence渲染引擎认识ac:前缀。CKEditor只认W3C标准标签,ac:structured-macro、ac:parameter、ri:page统统不在白名单里。默认配置下,CKEditor会把这些标签视作不安全的陌生内容,直接过滤掉。过滤后的结果,取决于CKEditor对未知标签的处理策略:有的标签被剥离但内部文本保留;有的标签连内部内容一起被丢掉;还有的标签被丢弃后,内部的HTML结构被“打平”,导致段落边界消失。这就是为什么文档看起来“错乱”。

2.2 全角符号、实体与不间断空格

另一个“乱码”来源是实体字符。Confluence为了精确排版,页面里大量使用&nbsp;、&#39;、&quot;这类HTML实体。CKEditor处理时,会先把实体解码成真实字符,再通过自己的输出规则转回实体。这本是正常流程,但如果导入链路中出现两次实体解码,就会变成“双重解码”。

比如&nbsp;被解码成不换行空格\u00A0,后端存储时又把它转义成&nbsp;,前端展示时再解码一次,最终出现一些奇怪的字符组合。另外,Confluence的导出HTML里还经常出现全角空格、全角括号、波浪号等特殊符号,在转移过程中被数据库或HTTP层做了“规范化”,结果看起来就像乱码。清洗时建议统一策略:入库前只保留标准文本,入库后按需转义,别让两边各做一遍。

2.3 换行丢失的真相不是“换行”,而是块级标签被抹平

很多人抱怨Confluence导入CKEditor后,段落挤成一团,第一反应就是“换行丢了”。实际上,换行不是被删掉的,而是分隔段落的块级边界被抹掉了。

Confluence文档里,页面结构往往由宏来划分。一个“警告宏”内部可能有标题、两段文字、一个列表。当宏标签被CKEditor过滤掉之后,标题、两段文字、列表可能被塞进同一个<p>里,彼此之间不再有</p><p>边界,显示自然就成一整块。这时候你往文本里硬塞<br />是没用的,因为根因是块级标签被抹平了。清洗的核心不是补换行,而是先把Confluence宏标签降级成标准块级标签——div、table、p、pre——再交给CKEditor处理。

3. CKEditor的内容加工:从HTML到可编辑DOM要闯三道关

3.1 第一道关:allowedContent过滤白名单

CKEditor 4的ACF(Allowed Content Filter)机制,会根据allowedContent配置检查每一个标签、属性和样式。默认配置对标准HTML很友好,但对XML命名空间或自定义前缀标签是零容忍。Confluence导出HTML开头常带这一段:

<html xmlns:ac="http://www.atlassian.com/schema/confluence/4/ac/" xmlns:ri="http://www.atlassian.com/schema/confluence/4/ri/">

这里的xmlns声明本身不影响CKEditor,但它声明的ac:和ri:标签在ACF机制里没有任何对应规则,于是被丢弃。丢弃后再处理子节点,嵌套结构已经破损。

处理方式有三档:

  • 临时关闭ACF:config.allowedContent = true,能快速看到全貌,但不适合生产环境,等于把内容安全过滤全关了;
  • 给CKEditor添加自定义允许规则:config.extraAllowedContent = 'ac[*]',这种做法只解决“不被删除”,不解决“被识别成什么样的块级元素”;
  • 最推荐:在导入前由服务端清洗工具把ac:标签全部替换成标准HTML,CKEditor只接“干净”的内容。

我们在项目里选的是第三档。不为别的,因为批量迁移的稳定性比在线编辑器的“求生欲”更关键。你希望编辑器过滤的是用户从Word粘贴进来的脏样式,而不是系统已经清洗过的历史文档。

3.2 第二道关:dataProcessor的toHtml流程

CKEditor从外部拿到HTML,要经过CKEDITOR.htmlDataProcessor.toHtml完成“浏览器解析→规范化→过滤→可编辑”。如果你的导入流程是直接把HTML字符串setData,等于把整篇内容交给toHtml去重构。这一步会做大量自动修正:

  • 把连续文本塞进<p>;
  • 把<div>处理成段落边界;
  • 识别有效表格并修正无效嵌套。

这些自动修正对正常网页是友好的,但对“已经残疾”的Confluence导出HTML就是二次破坏。同一个HTML,浏览器打开看得清清楚楚,一进CKEditor就错乱,原因就在这里:浏览器只负责渲染,CKEditor要“理解并结构化”。它需要知道哪些是段落、哪些是列表、哪些是表格,一旦源HTML里的语义结构是奇怪的宏标签而不是标准块级标签,它就只能用猜的,一猜就错。

3.3 第三道关:为什么复制粘贴时的表现不一样

在排查过程中,我们还发现一个有意思的现象:从Confluence网页复制内容粘贴到CKEditor,基本正常;用导出HTML文件导入,就乱成一片。一开始以为是粘贴流程和setData流程的过滤规则不同,后来深入比对才发现,问题在于源数据本身不一样。

从Confluence页面复制时,浏览器复制的是Word等富文本编辑器通常走的路径——经过系统剪贴板,拿到的是当前渲染好的标准HTML DOM片段,里面没有宏标签。而导出HTML文件里,很多宏可能还是存储态的ac:标签,并没有被渲染引擎替换成标准标签。所以不是CKEditor对粘贴更宽容,而是你粘贴进去的东西本来就比较干净。这个认知很重要:不要用“复制粘贴正常”来反推“导入也应该正常”,两个场景的数据源完全不是一回事。

4. 清洗Confluence导出HTML的完整实操方案

4.1 源头尽量取“View模式渲染后的DOM”

如果只是迁移几个页面,最省力的办法是打开Confluence页面,用浏览器DevTools把外层容器中渲染后的DOM直接复制出来。这时候拿到的HTML已经是标准标签,宏已经被Confluence渲染成了div、table、pre,乱码问题几乎消失。缺点是会丢失附件相对路径、锚点ID等元信息,需要额外处理。

如果是几十上百篇文档,不能靠人工复制,就必须走导出API或批处理脚本。Confluence有REST API可以取到body.storage.value,但那是存储格式,需要自己清洗。取body.view.value会拿到渲染后的HTML,但那个HTML里也有Confluence自己的包装结构和内联样式,清洗成本同样不低。我的建议是:小批量用View DOM,批量用API获取后走清洗脚本,两条路都要配一个验证环节。

4.2 Python清洗脚本:把宏标签降级成标准HTML

这里给出一版我实际用过的清洗逻辑,核心分四步:

  1. 去掉XML命名空间声明,保持纯HTML;
  2. 展开文本宏,保留内部富文本内容;
  3. 清理表格宏,把它转成标准<table>;
  4. 统一实体解码与空白处理,避免双重转义。

一个可运行的最小处理版本如下:

import re import html def clean_confluence_html(raw): # 1. 去掉xmlns声明 raw = re.sub(r'\sxmlns:(ac|ri)="[^"]*"', '', raw) # 2. 将宏标签剥离,保留内部富文本 def macro_repl(m): inner = m.group(0) # 优先取ac:rich-text-attribute内部的HTML rich = re.search( r'<ac:rich-text-attribute[^>]*>(.*?)</ac:rich-text-attribute>', inner, re.S ) if rich: return clean_confluence_html(rich.group(1)) # 其次取ac:parameter里的纯文本 params = re.findall( r'<ac:parameter[^>]*>(.*?)</ac:parameter>', inner, re.S ) return ''.join(params) raw = re.sub( r'<ac:structured-macro.*?</ac:structured-macro>', macro_repl, raw, flags=re.S ) # 3. 删除ri:相关标签,同时保留其内部文本或href属性 raw = re.sub(r'<ri:[^>]*>', '', raw) raw = re.sub(r'</ri:[^>]*>', '', raw) # 4. 实体解码 raw = html.unescape(raw) # 5. 清理标签间的多余空白,保留换行 raw = re.sub(r'>\s+<', '>\n<', raw) return raw

这段脚本很粗糙,对复杂嵌套宏需要递归处理:比如宏里套宏、宏里套表格,单纯正则很难一次到位。但思路是对的:先降级,后解码,绝不直接往编辑器里倒原始HTML。实际落地时,我把这个脚本包装成了一个FastAPI接口,后台任务批量跑,日志记录每个页面的清洗结果和剩余未知标签数量。

4.3 CKEditor端配置配合:进得去,也要存得住

清洗不是终点,CKEditor侧还要做两个配置。

一是允许来源里合理的标准标签。可以在初始化时加:

config.extraAllowedContent = [ 'p', 'div', 'pre', 'table[border]', 'code', 'span{color}', 'ul;ol;li', 'h1;h2;h3;h4;h5;h6', 'img[src,alt,title]' ].join(';');

二是把编辑器输出格式收敛,方便对接后端存储:

config.enterMode = CKEDITOR.ENTER_P; config.shiftEnterMode = CKEDITOR.ENTER_BR; config.forcePasteAsPlainText = false;

不推荐关闭ACF。生产环境没人愿意看到一个不设防的富文本编辑器。如果你担心历史数据里残留某些标签被过滤,宁可回源库再清洗一次,也不要牺牲编辑器的安全边界。

4.4 数据库和HTTP层:最后的编码闭环

清洗后的HTML要入库。这里千万别忽视最后三道编码防线:

  • MySQL/MariaDB表字段的collation要用utf8mb4_unicode_ci,不要用utf8,否则4字节的emoji会变成问号;
  • 后端读取后输出到CKEditor时,HTTP响应头要明确charset=utf-8;
  • 编辑器提交保存时,后端不要用strip_tags把标签全部剥掉,要做allowlist过滤,保留标准标签即可。

如果做到了这里,编码层面的乱码早被防御住了,结构层面的乱码也已被清洗脚本降级,剩下的就只是个别的页面级微调。

5. 批量迁移中的验证清单与踩坑点

5.1 用含表格、代码块、宏的页面做冒烟测试

迁移后别只随机看两个页面。我建议列一个冒烟页面清单,专门挑结构复杂的页面:含TOC目录宏的页、含三线表的页、含代码块的页、含提示宏的页、含子页面列表宏的页。每个页面导入后查四件事:编码是否正常、段落是否分界、表格是否保持、代码是否完整。

下面是我当时用的记录表:

页面类型编码段落表格代码块
TOC宏页正常正常--
三线表页正常正常错位-
代码块页正常正常-完整
提示宏页正常挤成一团--

如果发现提示宏那行显示“挤成一团”,回去看清洗脚本对ac:name="tip"的宏处理逻辑,大概率是宏内部文本没有被降级成<p>。我后来在脚本里加了一条规则:对ac:rich-text-attribute,先把内部的<p>和<br />保留,再把宏标签本身替换成<div class="confluence-macro">,才算真正解决。

5.2 图片、附件链接与锚点的二次校验

Confluence的附件链接常常是<ri:attachment ri:filename="xxx.png" />这种形式。清洗脚本把ri:标签删掉后,图片链接就断了,页面只占位不加载。如果是自研平台,需要把附件从Confluence的下载地址转存到对象存储,然后在清洗脚本里把ri:attachment替换成<img src="新地址">。

这部分很多人会漏。漏掉的后果是:文字不乱了,图片全挂了,用户还是会觉得“迁移出问题”。我们在清洗脚本里加了一个附件映射表,批量下载附件、重命名、上传到OSS,然后把映射关系注入到清洗结果里。锚点也一样,Confluence导出的标题带id属性,清洗时必须保留,否则目录跳转全部失效。

5.3 分批迁移、灰度确认,比什么都重要

最后一条经验:分批迁移。一次导入10个页面,在CKEditor实例里切换源代码视图和可视化视图反复对比,确认无误后再跑下一批。不要一次性导入上千篇,否则出了问题连定位都难。

个人体会:这种“平台型迁移”最怕的不是技术难度,而是“脚本跑通就没问题”的错觉。脚本没有报错,不代表页面显示正确。一定要把冒烟页面测试当成正式流程,而不是临时加测。我后来还加了一个自动检查任务:导入完成后,用无头浏览器逐个打开页面,比对“字符数、段落数、表格数、代码块数”这四个指标,跟Confluence源站数据做差值,超过阈值就自动标记为异常。靠这个,才敢把最后一批文档放心切过去。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询