XML Formatter:HTML 实体、字符引用与二次转义
&、&、HTML 的 和 CDATA 都不是同一种表示。XML parser 读取实体后通常得到字符 &,serializer 再输出时会重新转义;如果把已经序列化的 & 当成普通数据再次转义,就会得到 &。本文只讨论实体表示和二次转义。
一、问题概述:同一个字符有源码表示和解析值
XML 文本 A & B 的解析值是 A & B;如果业务数据本来就包含六个字符 &,它被 XML 转义后应变成 &,因为这时 & 是数据而不是实体语法。判断是否二次转义,必须先明确当前字符串处于解析值还是序列化文本层。
二、最小复现:解析一次再序列化一次
下面用纯函数展示实体编码层次。它没有调用某个 XML 库,目的是让输入值和输出源码分开可见。
function escapeXmlText(value: string): string {
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
}
const parsedValue = "A & B";
const literalText = "A & B";
console.log(escapeXmlText(parsedValue)); // A & B
console.log(escapeXmlText(literalText)); // A &amp; B三、根因:二次转义发生在错误的层边界
常见链路是 parser 将实体解码为字符,formatter/serializer 再对字符编码一次,这是正确的;错误链路是业务代码先手工生成 &,随后 serializer 把它当普通字符再次编码。HTML 实体表还包含不属于 XML 预定义实体的名称,不能直接复制到 XML。
四、推荐方案:内部保存解析值,最后只序列化一次
内部模型保存实际字符,不保存带 & 的 XML 源码片段。由一个明确的 serializer 负责文本/属性转义;如果必须接收已序列化片段,应使用不同的类型或拒绝混入,不能让同一个 string 同时代表两层数据。
五、完整代码:区分原始值、实体文本和 CDATA
以下代码只允许 serializer 接收解析值;CDATA 分支不做实体转义,但仍需处理 ]]> 边界。HTML 不会被当作 XML 合法实体自动保留。
type XmlText = { kind: "text"; value: string } | { kind: "cdata"; value: string };
function serializeXmlText(value: XmlText): string {
if (value.kind === "cdata") return "<![CDATA[" + value.value.replaceAll("]]>", "]]]]><![CDATA[>") + "]" + "]>";
return value.value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
}
console.log(serializeXmlText({ kind: "text", value: "A & B" })); // A & B
console.log(serializeXmlText({ kind: "text", value: "&" })); // &amp;
console.log(serializeXmlText({ kind: "cdata", value: "A & B" })); // <![CDATA[A & B]]>六、常见错误方案
把 HTML 实体 直接写入 XML,可能遇到未声明实体;把 & 当作解析值再调用 escape,会产生二次转义;把 CDATA 内容也做实体替换,则破坏了 CDATA 的用途。手工替换 & 的顺序错误,还可能把新生成的实体再次匹配。
七、边界条件:属性、数字引用与外部实体
属性和文本都需要转义,但可转义字符集合和引号边界不同。&、& 是数字字符引用,解析后与 & 的字符值可能相同,源码表示却不同。外部实体、DTD 和安全策略属于 parser 配置,不应由 formatter 擅自展开。
八、如何验证没有二次转义
分别测试原始 &、字面量 &、数字字符引用、HTML 、属性值和 CDATA。对每种输入记录解析后的字符,再序列化并重新解析,断言字符值和节点类型正确;不要只对比一次输出字符串。
九、FAQ
问:看到 &amp; 一定是 bug 吗?答:如果业务值本来就是 & 六个字符,它可能是正确的;要看输入层语义。
问:HTML 实体能在 XML 中使用吗?答:只有 XML 已声明的实体可直接使用,HTML 实体名称不能默认照搬。
问:CDATA 是否完全不需要转义?答:普通实体不需要,但 ]]> 必须拆分,且 XML 禁止某些控制字符。
十、总结
XML formatter 应在解析值与序列化文本之间只做一次转义。区分 XML 预定义实体、数字字符引用、HTML 实体和 CDATA,避免把已编码片段当普通值重复编码,并用解析—序列化—再解析验证字符值。
来源与延伸阅读
技术审校所依据的规范与权威参考资料。
相关文章
XML Formatter 自闭合标签缩进:Token、标签类型与缩进栈
用受限 token 化和缩进栈说明 XML formatter 如何区分开始、结束、自闭合标签及注释,并明确不覆盖完整 XML 规范。
踩坑避坑XML Formatter:空白文本节点污染与 mixed content
区分纯字符串格式化和重新解析 DOM,说明缩进空白何时会成为文本节点,以及 mixed content 为什么不能随意插入换行。
错误排查XML 转 JSON:单节点与多节点的数组形状推断
解释 XML 只有一个子节点和出现多个同名节点时的 JSON 形状差异,比较始终数组、schema 驱动和后处理三种策略。
继续阅读
可打开关联的浏览器工具,使用自己的样本验证文中的处理流程。
打开关联工具