
这段时间我们团队在把Confluence上的产品文档整体迁到自研内容平台编辑器换成了CKEditor。原以为只是数据的搬运结果第一批导入就在界面上出现大面积“乱码”。最典型的还不是网上教程里常说的“锟斤拷”而是另一种更让人头疼的情形编码从头到尾都是UTF-8中文既没变方块也没变问号可文字排列错乱段落挤成一团表格边界消失有些代码块直接丢行。折腾两天后我才意识到从Confluence到CKEditor的“乱码”绝大多数时候不是字符编码问题而是HTML结构假设不一致造成的。这篇文章就从现象分类开始逐步拆解Confluence导出HTML、CKEditor过滤规则、以及最终的清洗方案。准备做文档平台迁移的朋友可以直接把这篇当成踩坑手册来用。1. 先分清乱码的三种长相避免第一轮排查跑偏1.1 真正的字符编码乱码锟斤拷与双重编码先说最常见的经典乱码。从Confluence页面复制内容到CKEditor如果中间环节的字符集处理出错你会看到这几类现象中文变成“锟斤拷”“锘挎嫄”这类完全不认识的汉字组合英文数字正常中文变成一串“”甚至直接消失出现“锓—这种拉丁字母加符号的组合看着像法语乱入了。这些乱码的根源几乎都在“字节流解释错了一层”。Confluence是Java应用页面输出默认是UTF-8CKEditor跑在浏览器里正常情况下也用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拿到之后按自己的过滤规则处理识别不了的标签直接删除但删除的是整个宏区块内部正文被错误拼接于是读者看到的就是“乱码”。判断技巧先看源码用CtrlU查看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:nametip ac:schema-version1 ac:rich-text-attribute ac:nametext p这里是提示框内容需要注意xxx/p /ac:rich-text-attribute ac:parameter ac:nametitle提示/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里彼此之间不再有/pp边界显示自然就成一整块。这时候你往文本里硬塞br /是没用的因为根因是块级标签被抹平了。清洗的核心不是补换行而是先把Confluence宏标签降级成标准块级标签——div、table、p、pre——再交给CKEditor处理。3. CKEditor的内容加工从HTML到可编辑DOM要闯三道关3.1 第一道关allowedContent过滤白名单CKEditor 4的ACFAllowed Content Filter机制会根据allowedContent配置检查每一个标签、属性和样式。默认配置对标准HTML很友好但对XML命名空间或自定义前缀标签是零容忍。Confluence导出HTML开头常带这一段html xmlns:achttp://www.atlassian.com/schema/confluence/4/ac/ xmlns:rihttp://www.atlassian.com/schema/confluence/4/ri/这里的xmlns声明本身不影响CKEditor但它声明的ac:和ri:标签在ACF机制里没有任何对应规则于是被丢弃。丢弃后再处理子节点嵌套结构已经破损。处理方式有三档临时关闭ACFconfig.allowedContent true能快速看到全貌但不适合生产环境等于把内容安全过滤全关了给CKEditor添加自定义允许规则config.extraAllowedContent ac[*]这种做法只解决“不被删除”不解决“被识别成什么样的块级元素”最推荐在导入前由服务端清洗工具把ac:标签全部替换成标准HTMLCKEditor只接“干净”的内容。我们在项目里选的是第三档。不为别的因为批量迁移的稳定性比在线编辑器的“求生欲”更关键。你希望编辑器过滤的是用户从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这里给出一版我实际用过的清洗逻辑核心分四步去掉XML命名空间声明保持纯HTML展开文本宏保留内部富文本内容清理表格宏把它转成标准table统一实体解码与空白处理避免双重转义。一个可运行的最小处理版本如下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( rac:rich-text-attribute[^]*(.*?)/ac:rich-text-attribute, inner, re.S ) if rich: return clean_confluence_html(rich.group(1)) # 其次取ac:parameter里的纯文本 params re.findall( rac:parameter[^]*(.*?)/ac:parameter, inner, re.S ) return .join(params) raw re.sub( rac:structured-macro.*?/ac:structured-macro, macro_repl, raw, flagsre.S ) # 3. 删除ri:相关标签同时保留其内部文本或href属性 raw re.sub(rri:[^]*, , 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响应头要明确charsetutf-8编辑器提交保存时后端不要用strip_tags把标签全部剥掉要做allowlist过滤保留标准标签即可。如果做到了这里编码层面的乱码早被防御住了结构层面的乱码也已被清洗脚本降级剩下的就只是个别的页面级微调。5. 批量迁移中的验证清单与踩坑点5.1 用含表格、代码块、宏的页面做冒烟测试迁移后别只随机看两个页面。我建议列一个冒烟页面清单专门挑结构复杂的页面含TOC目录宏的页、含三线表的页、含代码块的页、含提示宏的页、含子页面列表宏的页。每个页面导入后查四件事编码是否正常、段落是否分界、表格是否保持、代码是否完整。下面是我当时用的记录表页面类型编码段落表格代码块TOC宏页正常正常--三线表页正常正常错位-代码块页正常正常-完整提示宏页正常挤成一团--如果发现提示宏那行显示“挤成一团”回去看清洗脚本对ac:nametip的宏处理逻辑大概率是宏内部文本没有被降级成p。我后来在脚本里加了一条规则对ac:rich-text-attribute先把内部的p和br /保留再把宏标签本身替换成div classconfluence-macro才算真正解决。5.2 图片、附件链接与锚点的二次校验Confluence的附件链接常常是ri:attachment ri:filenamexxx.png /这种形式。清洗脚本把ri:标签删掉后图片链接就断了页面只占位不加载。如果是自研平台需要把附件从Confluence的下载地址转存到对象存储然后在清洗脚本里把ri:attachment替换成img src新地址。这部分很多人会漏。漏掉的后果是文字不乱了图片全挂了用户还是会觉得“迁移出问题”。我们在清洗脚本里加了一个附件映射表批量下载附件、重命名、上传到OSS然后把映射关系注入到清洗结果里。锚点也一样Confluence导出的标题带id属性清洗时必须保留否则目录跳转全部失效。5.3 分批迁移、灰度确认比什么都重要最后一条经验分批迁移。一次导入10个页面在CKEditor实例里切换源代码视图和可视化视图反复对比确认无误后再跑下一批。不要一次性导入上千篇否则出了问题连定位都难。个人体会这种“平台型迁移”最怕的不是技术难度而是“脚本跑通就没问题”的错觉。脚本没有报错不代表页面显示正确。一定要把冒烟页面测试当成正式流程而不是临时加测。我后来还加了一个自动检查任务导入完成后用无头浏览器逐个打开页面比对“字符数、段落数、表格数、代码块数”这四个指标跟Confluence源站数据做差值超过阈值就自动标记为异常。靠这个才敢把最后一批文档放心切过去。