XML作为一种通用的数据交换格式,依靠一套严格的语法规则来保证结构可被机器稳定解析。其中最容易让初学者和接口开发者头疼的,就是特殊字符的处理。当我们把业务数据直接塞进XML节点时,某些在普通文本里毫无问题的符号,会瞬间破坏整份文档的合法性。

一、哪些字符在XML中属于特殊字符
XML规范明确规定,有五个字符在文本内容中具有特殊含义,不能直接原样书写。它们分别是:小于号(<)、大于号(>)、和号(&)、单引号(')以及双引号(")。其中小于号和和号最为危险,因为解析器在扫描文档时,一旦看到<就会认为后面要开始一个标签,看到&就会认为后面是一个实体引用的开头。
大于号单独出现在文本里时,多数解析器可以容忍,但规范上仍建议转义,因为在某些上下文(比如CDATA结束标记附近)可能产生歧义。单引号和双引号只有在作为属性值分隔符时才必须转义,在普通元素文本中写引号通常没问题,但统一转义能让文档更健壮。下面用一个简单的对照表说明:
| 原始字符 | 预定义实体 | 常见使用场景 |
|---|---|---|
| < | < | 文本中出现比较运算 a < b |
| > | > | 文本中出现 a > b |
| & | & | 公司名 XYZ & Co |
| ' | ' | 属性值内的缩写 don't |
| " | " | 属性值内需要保持引号 |
很多开发者在拼接SQL语句或前端模板时,直接把含特殊符号的字符串放进XML节点,结果解析阶段就抛出错误。理解这张表是避免问题的第一步。
二、使用预定义实体进行转义
处理特殊字符最直接的方式,就是用XML预定义实体替换。这种方法兼容所有XML解析器,而且可读性尚可。比如我们要在节点中表达“收入 > 支出 & 利润 < 零”这样的文本,就必须写成对应的实体形式。
下面是一段Java代码,演示如何手动将字符串中的特殊字符替换为实体,然后再拼入XML:
public class XmlEscapeDemo {
// 将文本中的特殊字符转义为XML预定义实体
public static String escapeXml(String input) {
if (input == null) {
return null;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < input.length(); i++) {
char c = input.charAt(i);
switch (c) {
case '&':
sb.append("&");
break;
case '<':
sb.append("<");
break;
case '>':
sb.append(">");
break;
case ''':
sb.append("'");
break;
case '"':
sb.append(""");
break;
default:
sb.append(c);
}
}
return sb.toString();
}
public static void main(String[] args) {
String raw = "判断条件: a < b & c > d";
String safe = escapeXml(raw);
System.out.println("<expr>" + safe + "</expr>");
}
}
这种写法的优点是逻辑透明、不依赖第三方库,适合对安全性要求极高的嵌入式环境。缺点是如果文本量很大,频繁拼接字符串会带来性能开销,而且人工维护容易漏掉某个字符。实际项目中,更推荐直接使用标准库,例如Java的org.apache.commons.text.StringEscapeUtils或者内置的javax.xml.parsers相关工具。
需要注意,转义只针对文本内容以及属性值内的特殊符号,标签名和属性名本身不能包含这些字符。也就是说,你不能写一个叫<a&b>的标签,只能写<ab>然后在里面放转义后的文本。
三、CDATA区段的使用与限制
当一段文本里充满各种符号,比如一段JavaScript代码、SQL脚本或者正则表达,逐个转义既麻烦又影响可读性,这时可以用CDATA区段。CDATA的全称是Character Data,它的语法是<![CDATA[ 内容 ]]>,在方括号内部的任何字符(除了结尾的]]>序列)都会被解析器当作纯文本,不会做标签或实体解析。
例如下面这段XML,把一段带大量符号的SQL放在CDATA里,就完全不需要转义:
<query>
<![CDATA[
SELECT * FROM users
WHERE age > 18 AND name <> 'test'
AND dept IN ('a', 'b&c')
]]>
</query>
CDATA的好处是清晰直观,特别适合存放代码块或复杂表达式。但它有几个限制:首先,CDATA不能嵌套,也就是内部不能再出现<![CDATA[;其次,文本内容里如果天然包含]]>,就必须拆开处理,比如写成]]><![CDATA[这样的拼接;最后,并非所有场景都支持CDATA,例如某些严格定义的Web Service契约可能只允许简单类型。
从解析性能看,CDATA区和普通转义文本在多数解析器里开销相近,选择哪种主要看可维护性。如果文本来自用户输入且含不可控符号,用CDATA包裹更安全;如果是简短的属性值,转义实体更紧凑。
四、生成与解析时的注意事项
在编写程序生成XML时,永远不要相信原始字符串是“干净”的。无论是后端接口返回报文,还是配置文件导出,都应统一走XML库的序列化方法,让底层自动处理特殊字符。以Python为例,使用标准库xml.etree.ElementTree时,调用text赋值会自动转义:
import xml.etree.ElementTree as ET
root = ET.Element("root")
# 直接赋值含特殊字符的文本,ET会自动转义
root.text = "价格 < 100 & 库存 > 0"
tree = ET.ElementTree(root)
tree.write("out.xml", encoding="utf-8")
# 输出中文本会变成 价格 < 100 & 库存 > 0
在解析端,只要文档是合法的,标准解析器会反向把实体和CDATA还原成原始字符串,业务代码无需关心当初是转义还是CDATA。但如果你用正则或字符串切割等野路子去读XML,就会踩到特殊字符的坑,因此务必使用正规解析器。
还有一个常见误区是认为HTML和XML处理方式一样。HTML容错率高,浏览器会猜测标签边界;但XML解析是“遇错即停”,哪怕一个未转义的&都会导致整个文档加载失败。因此写XML时对待特殊字符必须严谨。
五、总结与实践建议
面对XML特殊字符问题,核心原则是:把数据交给XML库处理,不要手工拼字符串。如果必须手工构造,小于号和和号必须转义,大于号与引号按需转义;大段含符号的内容优先用CDATA提升可读性。在接口调试中,遇到解析异常先检查报错位置附近是否有裸写的<或&,这能解决绝大多数乱码与解析失败问题。
掌握这些细节后,无论是写配置文件、做跨系统报文交换,还是处理历史遗留的XML数据,都能更从容地保证文档合法、数据准确。