XML在数据交换、配置文件、接口报文等场景中随处可见,但很多解析失败的报错,追根溯源并不是业务逻辑的问题,而是文档本身压根不符合XML的基本语法。XML规范里把符合基本语法规则的文档称为良构的(Well-Formed),这是XML世界里最低也是最硬性的门槛:一份文档可以没有任何DTD或Schema约束,但它只要是XML,就必须良构,否则解析器会直接拒绝处理。理解良构规则,是保证XML有效性的第一步。

良构到底是什么意思
XML规范(W3C的Extensible Markup Language 1.0)对文档提出了两个层次的要求。第一个层次是良构,即文档在纯语法层面符合XML的基本规则,好比一篇文章至少要做到句子通顺、标点配对。第二个层次才是有效(Valid),指文档在良构的基础上,还符合某种结构约束,例如DTD或XML Schema定义的元素顺序、出现次数、数据类型等。
换句话说,良构是必要条件,不是充分条件。一份文档必须先良构,才有资格讨论是否有效。解析器对这两者的态度也不同:遇到非良构的文档,解析器会直接报致命错误(fatal error)并停止解析;而对一份良构但不符合Schema的文档,是否报错取决于解析器的配置,很多解析器默认只做良构检查。
日常开发中最常见的误解,就是把HTML的容错习惯带到XML里。HTML解析器非常宽容,标签没闭合、属性没引号往往都能自动纠正,而XML解析器是严格模式,差一个字符都不行。这也是为什么一段能正常显示的HTML片段,直接塞进XML里几乎必然解析失败。
良构XML必须满足的核心语法规则
规则一:有且仅有一个根元素,所有其他元素都必须嵌套在这个根元素内部。下面的写法就是典型的非良构文档,因为出现了两个顶层元素:
<user>张三</user> <user>李四</user>
正确做法是给它们套一个共同的父元素,例如<users>作为根节点包裹全部内容。注意XML声明之前可以有注释和处理指令,但根元素只能有一个。
规则二:所有标签必须正确闭合。XML区分三种标签形式:开始标签与结束标签必须严格配对,例如<name>...</name>;空元素可以写成自闭合形式<img/>,也可以写成<img></img>。关键在于嵌套顺序不能交叉,<b><i>文本</i></b>是合法的,而<b><i>文本</b></i>这种交叉嵌套则直接报错。
规则三:标签名区分大小写,开始标签和结束标签必须完全一致。<User>和</user>会被视为两个不同的标签,解析器会报找不到匹配结束标签的错误。这一点和HTML不同,HTML中<BR>和<br>被视为同一个标签。
规则四:属性值必须用引号包裹,单引号双引号都可以,但不能省略。此外同一个元素上属性不能重名,属性值中的特殊字符也要转义:
<!-- 错误:属性没加引号 --> <item id=1001/> <!-- 正确写法 --> <item id="1001" type='normal'/>
规则五:特殊字符必须转义。XML预定义了五个实体:<代表小于号,>代表大于号,&代表与符号,'代表单引号,"代表双引号。凡是文本内容或属性值中出现这五个字符,都必须用对应实体替代,否则与标签语法本身冲突。最常见的就是内容里直接写了与符号,例如“AT&T”必须写成“AT&T”。如果有一大段包含大量特殊字符的内容,也可以用CDATA区段包裹,写成<![CDATA[...]]>的形式,CDATA内部的文本会被解析器原样保留。
规则六:名称的合法字符。元素名和属性名必须以字母或下划线开头,后面可以跟字母、数字、连字符、点号,但不能以数字或连字符开头,也不能包含空格。此外名称不能以xml(不区分大小写)开头,这是保留前缀。
如何检测文档是否良构
检测良构性不需要额外配置任何约束文件,任何XML解析器都能做。命令行下最方便的是xmllint,这是libxml2自带的工具,Linux和macOS一般自带,Windows也可以下载安装:
xmllint --noout config.xml
如果没有任何输出,说明文档是良构的;如果有问题,它会精确报出行号和错误原因,例如“Opening and ending tag mismatch”就对应标签不匹配,“xmlParseEntityRef: no name”多半是没转义的与符号。noout参数表示不输出解析结果,只做检查。
在Java中,直接用DOM或SAX解析一遍即可完成良构检查,解析过程抛出的SAXException就是非良构的信号。在浏览器里打开XML文件也能快速判断,非良构的文档浏览器会直接显示黄色的语法错误提示。编程生成XML时,建议优先使用XML库提供的序列化接口,让库去处理转义和闭合,而不是手工拼接字符串,手工拼接几乎必然在特殊字符上翻车。
良构与有效的区别
这两个概念经常被混用,但它们描述的是完全不同的检查层次。可以用一个表格来对比:
| 对比项 | 良构 Well-Formed | 有效 Valid |
|---|---|---|
| 检查层次 | XML基本语法规则 | 文档结构符合DTD或Schema约束 |
| 是否需要约束文件 | 不需要 | 需要 |
| 不满足时的行为 | 解析器报致命错误,停止解析 | 是否报错取决于解析器配置 |
| 检查方式 | xmllint --noout | xmllint --schema 或 --valid |
举例来说,一份订单XML要求item下必须有price子元素,这是Schema层面的约束。如果price缺失,文档仍然是良构的,解析器能正常读完,但它是无效的。而如果标签没闭合,文档连良构都达不到,根本没有进入有效性检查的资格。做接口联调时,如果对端报格式错误,先确认良构性,再排查Schema,排查顺序不要颠倒。
总结一下,良构是XML的语法底线:单一根元素、标签成对闭合、属性带引号、特殊字符转义、名称合法。把这几条规则内化成习惯,再配合工具自动检查,绝大多数XML解析报错都能在源头避免,也为后续基于Schema的结构校验打下基础。