UPTools UPTools
开发 主笔:工具匠

XML 报错「标签未闭合」?well-formed 的五条铁律

上周帮人调 SOAP 接口,对方报文回来一段压缩成一行的 XML,解析器直接抛 Unclosed tag。我对着那串字符看了十分钟,最后发现是某个 <ns2:Item> 在两百行外少写了一个斜杠。把报文丢进格式化工具一展开,第五行就看出来了。

XML 解析失败,十次有八次不是逻辑问题,是「格式不良好」。well-formed(格式良好)是 XML 规范里写死的五条硬规则,违反任何一条,解析器当场罢工。下面这五条,记住能少掉一大半头发。

第一条:只能有一个根元素

整份 XML 必须包在唯一一个根元素里,不能出现两个平级的顶层标签。

<!-- 错:两个根元素 -->
<user id="1">张三</user>
<user id="2">李四</user>
<!-- 对:外加一个根 -->
<users>
  <user id="1">张三</user>
  <user id="2">李四</user>
</users>

这条最容易被忽略的场景是从接口拼接数据,循环输出时忘了外层包一层 <list>,解析器看到第二个 <user> 就抛 junk after document element

第二条:所有标签必须闭合

两种闭合方式都行,成对闭合 <a></a> 或自闭合 <a/>,但不能只开不关。

<!-- 错:<note> 没闭合 -->
<note>
  <to>小明</to>
  <body>你好
</note>
<!-- 对 -->
<note>
  <to>小明</to>
  <body>你好</body>
</note>

自闭合写法在无内容元素上常见,比如 <br/><img src="x"/>。注意 HTML 里的 <br> 在 XML 里非法,必须加斜杠。SVG、Android Manifest 里写成 <View> 不带斜杠,解析器会一路吃下去,直到遇到一个意料之外的标签才报错,定位很费劲。

第三条:严格区分大小写,不能交叉嵌套

XML 区分大小写,<Name><name> 是两个标签,必须配对。标签也不能交叉,必须严格包含。

<!-- 错:交叉嵌套 -->
<a><b>内容</a></b>
<!-- 对:严格嵌套 -->
<a><b>内容</b></a>

这条踩坑的场景是从 SOAP、RSS 这种深层 XML 里找错。格式化展开后,缩进错位的那个位置基本就是交叉点。交叉嵌套的报错信息往往指向后一个闭合标签,别被带偏,真正的问题在前面。

第四条:属性值必须加引号

属性值可以用单引号或双引号,但不能省略。

<!-- 错:version 没加引号 -->
<config version=1.0 debug=true>
<!-- 对 -->
<config version="1.0" debug="true">

从 HTML 复制属性过来常踩这个坑。HTML 容忍 debug 这种裸属性,XML 一律报错。布尔值也得写成 debug="true",没有「省略值即 true」这种写法。

第五条:特殊字符必须用实体转义

文本内容或属性值里出现 &<>'",必须替换成实体。

字符实体
&&amp;
<&lt;
>&gt;
'&apos;
"&quot;

最坑的是 &<。比如想把一段说明写成 A & B,得写成 A &amp; B。再比如想把一段示例代码塞进 XML 里:

<!-- 错:< 被当成标签起点 -->
<example>if a < b then print()</example>
<!-- 对 -->
<example>if a &lt; b then print()</example>

解析器看到 < 就以为要开新标签,结果找不到匹配,报 not well-formed> 和引号在文本里通常不会触发报错,但规范要求转义,老老实实写更安全。

如果一整段都不想手动转义,用 CDATA 包起来:<![CDATA[ if a < b then print() ]]>,里面的内容解析器原样保留,HTML、SQL 片段常这么塞。

顺带分清:well-formed vs valid

well-formed 是「语法对」,只要满足上面五条就是格式良好。valid 是「语义对」,除了格式良好,还得符合 DTD 或 XSD 里定义的元素结构、属性类型、出现顺序等约束。

<!-- well-formed 但不一定 valid -->
<person>
  <age>张三</age>
  <name>28</name>
</person>

上面这段格式完全正确,解析器不会报错。但如果 XSD 规定 name 是字符串、age 是整数且 name 必须出现在 age 之前,那它就不 valid。日常开发里碰到的大多解析器只查 well-formed,valid 校验需要专门启用,比如 Java 的 SchemaFactory、Python 的 lxml 带 schema 参数。

命名空间一句话

如果遇到 <ns:Name><xs:Name> 这种带前缀的标签,那是 XML 命名空间机制,靠 xmlns 属性声明,用来区分不同来源的同名标签。它不改变 well-formed 的五条规则,前缀也要严格闭合,只是多了个声明。

实操:先格式化再排查

XML 出错时报错信息常指向某一列,手动数列数效率极低。把内容丢进 UPTools XML 格式化工具,工具会自动展开缩进并提示未闭合的标签位置,配合错误信息很快能定位。本地解析,不传数据。

三句话总结

XML 报错多半是 well-formed 没过:单根、必闭合、不交叉、属性加引号、特殊字符转义,五条里挑错。well-formed 只是语法对,valid 还要看 DTD/XSD 约束,日常解析器一般只查前者。出错时先格式化展开再读报错位置,比对着压缩字符串数字符快得多。

相关文章