XML 声明是文档可选的第一行,形如
<?xml version="1.0" encoding="UTF-8"?>,向解析器报告 XML 版本、字符编码与独立性。写它,解析器就知道按什么规则、什么编码读这份文档;不写,多数工具默认 UTF-8,但跨系统交换时等于把编码问题交给运气。承接第 1 章,本节解剖这枚"出厂标签"上的每一栏。
完整形式:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <!-- version:必填,目前只有 1.0 与 1.1 encoding:可选,默认 UTF-8 standalone:可选,yes 表示文档自包含、不依赖外部标记声明 -->
version 栏。1.1 是个小众修订(主要改换行与 Unicode 处理),绝大多数场景用 1.0,写别的值解析器直接报错。encoding 栏。声明文档本体用什么编码存储。standalone 栏。yes 表示文档不依赖外部 DTD 里的标记声明就能成立;引用外部 DTD 时应写 no 或省略。三个属性里真正高频出事的只有 encoding——另两个写对一次就不用再管,encoding 却要面对"文件实际编码与声明不符"这种随时可能发生的错位,所以本节的案例也集中在它身上。
三条位置铁律:声明必须是文档第一个东西,前面连一个空行都不能有;<?xml 中间不能有空格;属性顺序上 version 必须第一。
为什么位置如此严苛?因为解析器在读到声明之前必须先知道"用什么编码读这份文件"——这是个先有鸡还是先有蛋的难题,XML 的解法是规定声明只能用最保守的字符(ASCII 子集)书写、且必须出现在最前面,解析器先按 ASCII 盲读第一个标签,从 encoding 栏得知真实编码后再切换。理解了这个设计动机,"声明前不能有任何字节"就不再是死记的规矩,而是机制的自然推论。同理也解释了为什么声明里不能写中文注释、为什么它长得像处理指令却不是(2.4 节会回到这个话题)。
背景:某项目对接国企老系统,对方按规范发来 XML 报文,我方收到的中文全是乱码。操作:打开报文,第一行写着——
<?xml version="1.0" encoding="GBK"?> <notify> <title>账单已生成</title> </notify>
排查过程分两步。第一步,用十六进制查看器看文件头:没有 EF BB BF(UTF-8 BOM),中文字节是 GBK 双字节区间,说明文件本体确实是 GBK。第二步,检查我方解析代码——读取时按平台默认编码(Linux 上是 UTF-8)打开了流。结果:编码声明是"写给解析器看的说明书",但解析器根本没读它,而是硬编码了自己猜的编码。
修复方式有两种:让读取端真正尊重声明(多数解析器在以字节流方式打开时会自动识别声明并选择编码);或双方约定统一 UTF-8。我们选了后者,并补了三条工程约定:文件统一 UTF-8、不加 BOM、声明里显式写 encoding。变式:如果对方发来的文件声明 UTF-8 但实际是 GBK,责任在发送方——声明是契约的一部分,写错就是违约。再变式:双方都是自己人时,"全司统一 UTF-8"一条就能消灭大半编码问题,比逐接口排查便宜一个量级——编码问题的最优解常常在规范层而非代码层。
<!-- 加了 BOM 的文件,某些老解析器会把 BOM 当内容,报"声明前有字符" --> <!-- 排查口诀:先看头三字节,再看第一行声明,最后看读取端代码 -->
standalone 这栏平时安静,出事时才显形:
<!-- 声明自包含,但内部 DOCTYPE 引用了外部 DTD —— 自相矛盾 --> <?xml version="1.0" standalone="yes"?> <!DOCTYPE inventory SYSTEM "inventory.dtd"> <inventory/> <!-- 校验器会警告:standalone 与实际依赖不一致 -->
判断标准很简单:文档是否需要加载外部文件才能确定"哪些是普通元素、属性默认值是什么"。日常业务报文大多自包含,写 yes 或干脆省略。
⚠️ 最常见的三个声明错误:前面有空行导致"处理指令不在开头";编辑器自动存成 UTF-16 但声明仍写 UTF-8;从网页复制时带进了不可见字符。三者症状相似——解析器在第一行报错,初学者却去正文里找毛病。
<?xml 无空格、前面零字节;补一段编码常识,把事故背后的原理补齐。计算机只存数字,文字靠"编码表"映射——GBK 用两个字节表一个汉字,UTF-8 用一到四个字节表全世界。同一串字节,按不同编码表解出来就是不同文字,这就是乱码的全部原理。XML 声明的 encoding 栏本质是文档自带的编码说明书:"我这些字节请按 GBK 查表"。理解这一层,三类经典乱码都能对号:全部变问号(解码失败被替换)、中文变奇怪符号(字节被按错误表解释)、偶发正常偶发乱(多字节字符被从中间截断)。而"BOM"是文件头三四个字节的编码签名,帮助部分工具猜编码,却可能被严格的解析器当成内容——签名与说明书打架时,以说明书为准是更可控的策略。
再补三个边界情况。其一,encoding 只认 IANA 注册名,写 utf8 多数解析器宽容,写 "中文编码" 直接报错;稳妥写法是大小写规范的 UTF-8、GBK、ISO-8859-1。其二,UTF-16 文档的声明必须带 BOM 才能让解析器盲读阶段判断字节序,这与"建议不加 BOM"不矛盾——那条建议针对 UTF-8。其三,把 XML 片段拼接进大文档时,片段自带的声明必须剥掉,否则中间出现 <?xml 又是"处理指令不在开头"报错,聚合网关处理多源报文时尤其容易踩中。
声明虽小,却是整份文档里唯一"写给机器的元信息",下一节的元素才是承载数据的骨架主体。