良构(well-formed)是 XML 解析器对文档语法合规性的判定:满足第 1 章全部语法规则——单一根、标签闭合、嵌套不交叉、属性引号、实体转义、声明位置正确。不满足,任何 XML 工具都不会处理这份文档,一步都过不去。本节把良构检查项整理成清单,并训练"看报错定位病灶"的能力。
解析器开工前逐项核对的正是这些:
| 检查项 | 典型病灶 | 报错关键词 |
|---|---|---|
| 有且仅有一个根元素 | 顶层并列两个元素 | content not allowed in prolog / junk |
| 标签全部闭合 | 结束标签漏斜杠 | expected ... to close |
| 嵌套不交叉 | p 内开 b、p 外关 b | mismatched tag |
| 大小写一致 | <Stock> 配 </stock> |
mismatched tag |
| 属性值加引号 | id=1001 |
attribute value not quoted |
| 特殊字符转义 | 裸 &、裸 < |
not well-formed 实体相关 |
| 声明位置 | 声明前有空行或 BOM | XML declaration allowed only at start |
值得强调的是"没有容错模式"。HTML 浏览器有悠久的纠错传统——标签没关会猜,属性没引号会补。XML 从设计上拒绝了这条路:宁可拒收,不可猜错。做数据交换时这是优点(契约严格);从 HTML 转来的开发者常觉得苛刻,但正是这份苛刻保证了任何解析器对同一份文档建出同一棵树。
这份严格性有明确的工程回报。想象一条流水线上有五个系统依次处理同一份报文:若解析器可以各自"猜",五棵树可能五种样子,每个系统都要为自己那棵怪树写补丁代码;解析器统一拒绝猜测后,五棵树必然同构,所有系统共享同一套处理逻辑。确定性的树换来的是整个生态的兼容性,这是 XML 设计中最值得体会的一笔交易。也正因此,良构检查不是"可选的规范洁癖",而是文档进入机器世界的最低通行证——过不了这一关,后面的有效校验、转换、查询统统无从谈起。
训练一:
报错:The matching tag for <item> was not found (line 7) 文档片段: 3 <line sku="A"> 4 <name>螺栓</name> 5 <qty>200<Qty> 6 </line>
报错在 line 7 才喊出来,病灶在 line 5——<Qty> 少了斜杠,解析器把 Qty> 又当开始标签吞进去,一路错位到 line 7 才对不上。教训:报错行号常是"发现处"而非"病灶处",往前找未闭合标签。
训练二:
报错:Content is not allowed in prolog
prolog 指声明之前的位置。九成原因是文档开头混进了不可见字符——从网页复制带回的全角空格、编辑器加的 BOM、或声明前手滑敲了个回车。排查方法是用能显示不可见字符的编辑器看第一行,或按十六进制检查文件头三字节。
训练三:
报错:The entity name must immediately follow the & in the entity reference
内容里有裸 &。常见于把带查询参数的 URL 直接塞进 XML——与符号加参数名会被当成实体开头。修法是 &:
<link>http://a.example.org/x?u=1&p=2</link> <!-- 解析后得到的真实值中 & 还原为与符号,转义只存在于文本形态 -->
URL 里的查询参数分隔符是裸与符号的高发区,凡把链接写进 XML 的场合都要过一遍这道手续。同理还有正文中引用商标名带与符号、数学不等式,都属此类。
一份文档可以完美通过良构检查,同时是业务垃圾:
<?xml version="1.0" encoding="UTF-8"?> <order> <id>O-1001</id> <qty>二百件</qty> <id>O-1002</id> </order>
语法挑不出毛病:标签全闭合、嵌套正确。但 qty 该是数字、id 不该出现两次——这些要求写在语法之外,写在契约里。这就是第二道质检"有效"的领地,3.2 节用一份事故文档展开。
💡 排错顺序论:先修良构(语法级),良构过了再修有效(契约级)。两级错误混着修,改一处报一处新错,永远在追着解析器跑。
问:为什么同样的文档在 A 工具能打开、B 工具报错? 先怀疑两件事:一是 B 工具校验更严格(比如开启了校验模式而 A 只是宽松浏览);二是编码处理不同——A 恰好按文档实际编码打开,B 按声明或默认编码打开。把两边的报错原文与文件头字节摆在一起对比,答案通常自己浮出来。
问:良构错误会"部分成功"吗? 不会。XML 解析是原子判定:要么整份文档建树成功,要么报错且不提供任何树。这与 HTML 的增量渲染截然不同,也意味着不能指望"先把能读的部分读出来"——上游发来坏文档时,唯一正确动作是拒收并回报错误,而不是带病解析。
日常自查建议养成三个习惯。其一,编辑器开着实时校验写文档,错误在产生的那一刻就消灭。其二,提交前跑一遍命令行良构检查,把"编辑器没报"的侥幸堵死(不同解析器严格度有差异)。其三,接手陌生文档先跑良构再干活——上一手留下的坑,越早发现成本越低。这三个习惯的成本加起来不到一分钟,却能挡掉大部分半夜被叫起来排查的灾难。
&,解析后自动还原。最后补一个"工具间不一致"的辨析。同一份文档,浏览器里显示正常、xmllint 却报错,初学者常怀疑工具坏了。多数情况的解释是:浏览器只把 XML 当展示对象,宽容度高;命令行工具按规范严格判定。另一个方向的不一致也存在——某些编辑器默认把 --recover 之类的修复选项打开,静默纠正了小错误,让你以为文档本来就没问题。结论是把最严格的那个工具当裁判:编辑器的绿灯只能算参考,命令行良构检查通过才算数。CI 里固定用同一个严格工具,本地与线上判定一致,"我这能跑你那不能"的扯皮就从根上消失。
良构这关过了,文档才拿到"接受进一步审查"的资格。下一节看看只过第一关、栽在第二关的真实事故长什么样。