JSON 时代的 XML:命名空间、校验与关键安全边界
XML 1.0 在 1998 年 2 月作为 W3C Recommendation 发布。它被设计为 SGML(1986 年的 ISO 标准)的简化子集,让解析器不必容忍 SGML 那种 tag-soup。“简化”这个说法今天回看挺好笑——namespaces、schemas、transforms、查询语言一层层往上堆。但底下那个核心仍然小、定义清晰,比任何想取代它的东西都更严格。
JSON 在 API 大战中赢得如此彻底,今天 XML 已经像出土文物。它不是。 XML 是你每天依赖的若干系统的底料——SAML SSO、SOAP 银行网关、政府税务电子申报 schema、RSS / Atom feed、.docx/.xlsx/.pptx 里的 OOXML、电子书的 EPUB、Android 资源、企业代码库长尾里的 Java 配置、仍在被渲染的 XHTML。XML 在这些场景里不可替代有共同点:它们需要一个可 schema 校验、有命名空间、可查询、可变换的文档模型,而 JSON 在格式本身里没这些特性。
这篇尝试用"每年要打两次交道的人实际上需要的方式"来解释 XML。
它到底是什么
XML 文档是元素的树。每个元素有名字、可选属性、可选子元素、可选文本内容。元素用开始标签、结束标签、自闭合标签写。属性是开始标签上的 name="value"。注释 <!-- ... -->。文本内容可以含字符引用(&、<、>、"、')和数字字符引用(A、A)。
<?xml version="1.0" encoding="UTF-8"?>
<order id="42" status="paid">
<customer>Alice</customer>
<items>
<item sku="ABC-123" qty="2"/>
</items>
</order>
格式本身大致五条规则:
- 每个开始标签都有匹配的结束标签(或自闭合)。
- 标签嵌套,不交叉。
- 属性值要用引号引起来。
- 恰好一个根元素。
- 文本中的保留字符要转义。
满足这些规则的文档是 well-formed(格式良好)。同时匹配某个声明 schema(DTD、XSD、RELAX NG)的 well-formed 文档是 valid(合法)。生产里多数 XML 是 well-formed 但未校验——schema 校验昂贵,多数管线跳过。
well-formed vs valid:被搞混最多的区分
这件事在运维上很重要。几乎所有 XML 解析器都会拒绝非 well-formed 文档(标签不匹配、& 未转义等);几乎所有 XML 解析器默认都不会对 schema 做校验,除非你显式要求。被 XML 烫过的人通常的意思是:"解析器接受的 well-formed XML 在语义上是错的,因为没人对 schema 校验过它。"
校验也是 XML 表达力闪光、JSON 贫瘠暴露的地方。XSD 能表达:
- "这个元素必须有恰好 1-N 个 X 类型子元素。"
- "这个属性必须匹配某正则。"
- "这个数字必须在 0 到 100 之间。"
- "这个元素只在某个兄弟取特定值时才允许出现。"
- "这个子树必须按某 key 唯一。"
JSON Schema 也能表达大部分,但 XSD 早 15 年、在每种企业语言里都有成熟工具、有标准化类型库。如果你在向遗留银行 / 税务 / 医疗管线发数据,schema 一定是 XSD 形态——这些行业在 JSON Schema 出现之前就完成了格式之战。
命名空间:第一次没人能凭直觉理解
真实 XML 文档常组合多种词汇——你的业务数据 + 签名元素 + 加密元素 + 元数据。为防止名字冲突(你的 <id> 和 W3C 签名规范的 <id>),XML 1.0 在 1999 修订中加入命名空间。
命名空间就是绑到一个前缀上的 URI。URI 只是一个标识符——它不必能解析、不会被抓取、在任何意义上都不是 URL。前缀只是该 URI 在文档内的简写。
<order xmlns="http://example.com/order/v1"
xmlns:sig="http://www.w3.org/2000/09/xmldsig#">
<customer>Alice</customer>
<sig:Signature>...</sig:Signature>
</order>
这里 <order> 和 <customer> 在 order 命名空间(无前缀 = 默认命名空间)。<sig:Signature> 在 W3C XML Signature 命名空间。知道 http://www.w3.org/2000/09/xmldsig# 的消费者就能确切知道 <sig:Signature> 的含义,不论生产者用了哪个前缀。
陷阱:前缀是任意的。同一份文档把 <sig:Signature> 改成 <dsig:Signature>、URI 绑定不变,两份文档等价。写 XPath 查询匹配 sig: 然后收到 dsig: 数据的人,被这件事困惑过几小时。永远按命名空间 URI 查询,不要按前缀。
实体扩展与 billion laughs 攻击
XML 支持实体(entity)——在 DTD 里声明的具名宏,原地展开。经典滥用:
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
<!-- ... &lol9; ... -->
]>
<lolz>&lol9;</lolz>
每层十倍展开,&lol9; 展成十亿个 lol。朴素解析器分配数 GB 内存、OOM。这是 billion laughs 攻击,2003 年起众所周知;任何现代解析器在小预算之外都关掉内部实体扩展。
更危险的变体是 XXE(XML External Entity)注入——实体指向外部 URL 或本地文件:
<!DOCTYPE x [
<!ENTITY exfil SYSTEM "file:///etc/passwd">
]>
<x>&exfil;</x>
会解析外部实体的解析器会读 /etc/passwd 内联进文档,攻击者可外带。XXE 打过 PayPal、Facebook、银行、政府——每隔几个月就有新 XXE CVE 出现,因为有人在某处把默认改回去了。现代解析器默认关闭外部实体解析,但显式确认值得:
- Java:
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)。 - Python:用
defusedxml替代xml.etree。 - Go:
encoding/xml完全不处理外部实体(好)。 - C#:
XmlReaderSettings.DtdProcessing = DtdProcessing.Prohibit。
如果你在解析不可信 XML 又说不出哪些实体扩展特性已关闭,默认认为你有漏洞。
编码:声明、BOM 与字节本身
顶部那行 <?xml version="1.0" encoding="UTF-8"?> 是 XML 声明,encoding= 告诉解析器后面的字节怎么解码。
会出错的事:
- 声明写
UTF-8、文件其实是 Latin-1。解析 ASCII 字节没问题,到第一个非 ASCII 字符就以一种令人困惑的方式失败。 - 声明写
UTF-8而文件以 UTF-8 BOM(EF BB BF)开头。多数解析器能处理;少数会噎住。RFC 7303 允许 BOM。 - 声明写
UTF-16而文件无 BOM。解析器不知道字节序;规矩的拒绝,其他猜。 - 没有声明。默认是 UTF-8。有人按 Latin-1 假定写工具,产出非法字节。
"解析器卡在这个 XML 文件上"出乎意料常见的原因不是结构错,是编码不匹配。有疑问时 hex dump 前 8 字节,对照声明。
CDATA:基本不需要的逃生口
把含大量 < 与 & 的 HTML 或代码嵌入 XML 文本内容里很痛苦——每个尖括号和与号都要转义。CDATA 段允许字面量块:
<script><![CDATA[
if (x < 10 && y > 0) { return "ok"; }
]]></script>
<![CDATA[ ... ]]> 内几乎全部字面,除了结尾 ]]> 不能出现(要分两段)。CDATA 有时被当成"另一种字符串"——它不是。对解析器,结果与等价转义文本完全相同。别写"是不是 CDATA"分支的业务逻辑。
XPath、XSLT、XSD:可查询数据生态
JSON 在格式自身里没有等价于 XPath 的东西。XML 把 XPath 内置在模型里:
/order/items/item[@sku='ABC-123']/qty
这是一种导航 XML 树并返回节点集的查询语言。XPath 1.0(1999)在所有 XML 解析器中通用。XPath 2.0/3.x 加入丰富类型与函数库;XPath 3.x 是 XSLT 3 的依赖。
XSLT 是图灵完备的、把一份 XML 文档变换成另一份的语言。没写过 XSLT 的人觉得它令人遗憾;以写 XSLT 为职业的人要么退休了要么学会享受它。它仍然是把 XML 数据按规模渲染成 HTML 的标准方式(很多政府 PDF 底下是 XSLT)。
XSD(XML Schema Definition) 是校验用的 schema 语言。庞大繁杂,但事实标准。
这些就是 XML 在 JSON 触不到的地方反复出现的原因。带 WSDL 的 SOAP 服务可以静态校验、自动生成十几种语言的客户端桩、用 XPath 查询——全用现成工具。JSON 也能做等价工作,但你要把五种工具拼起来。
为什么 XML 输了(以及它没输的地方)
JSON 在 HTTP API 战胜 XML 因为:
- JavaScript 自带
JSON.parse;XML 要单独的库和 DOM API。 - JSON 的"啰嗦/信息"比更低。
{"x":1}vs<x>1</x>听起来计较,规模上来后主导带宽。 - JSON 没 schema、没命名空间、没变换、没校验——对多数 CRUD API 你不需要。
- XML 生态堆积了大量包袱(SOAP、WSDL、WS-*、XLink、XPointer、XHTML 2.0 那场惨剧),让"为你的 API 用 XML"变成"为你的 API 用这一摞十二头标准栈"。
XML 赢的地方:任何一个文档需要被 schema 校验、命名空间、签名、加密、变换、归档几十年 的场景。银行。医疗。政府。法律电子归档。SAML。SOAP。Office 文档里的 OOXML。EPUB。SVG(是的,SVG 是 XML)。RSS / Atom。Maven 生态。任何生命周期 30 年、生产者与消费者可能跨代的东西。
常见坑
- 文本里的
<或&。 必须<与&。意外多的坏 XML 是printf而不是真序列化器生成的。 - 混淆 CDATA 与实体引用。 对解析器它们等同。
- 信任
xmlns前缀。 比较前永远解析到 URI。 - 忘了 XML 声明编码 而手写 Latin-1 文件。
- 用默认配置解析 XXE 易感输入。 现代默认通常安全,确认一下。
- 按字符串相等比较两份 XML 文档。 元素内容里的空白通常(但不总是)有意义。属性顺序永远无意义:
<a x="1" y="2"/>与<a y="2" x="1"/>等价。diff 前归一化(xmllint --c14n)。 - 用字符串拼接而非 DOM/SAX 修改文档。 你会把转义弄错产出非法 XML。
- 把 XML 当 JSON 而忽略属性。
<item sku="X">2</item>同时有属性和文本内容;JSON 风格映射到{item: 2}会丢 SKU。
什么时候用 XML vs JSON vs 别的
XML 当:你需要"schema 校验"作为格式的一部分;你在与已经说 XML 的 SOAP/SAML/政府/银行系统集成;你需要命名空间因为在组合多套词汇;你需要 XSLT 把数据变换成展示形态。
JSON 当:HTTP API;消费方是 Web 前端;你不需要格式自带命名空间或 schema 校验;你想要一个所有地方都最小的解析器。
Protobuf / Avro / Cap'n Proto 当:你需要线上效率、向后兼容的 schema 演化,并且生产者和消费者都在你掌控之下。
TOML / YAML 当:人类写的配置文件。
Markdown / 纯文本 当:是散文。
XML "过时"的名声是错的。它是专精的。需要 XML 独特能力的工作不会消失,JSON 也不会长出一个能追上 XSD 的、带命名空间支持的 schema 语言。2026 年遇到 XML 时不该幻想它是 JSON;要做的是用真正的解析器、对 schema 校验、把外部实体解析关掉。
主要参考资料
用于核对本文技术细节的标准与官方文档。
在浏览器里直接格式化与校验 XML
本站 XML 工具用真正的解析器在浏览器内格式化与校验,保留属性,可控缩进。当上游给你扔来一行的 XML、你需要先看懂再黏回去时很有用。数据不会离开浏览器。
打开 XML 工具相关文章
继续阅读同一主题领域的实践指南。
Node 生产 Dockerfile 里到底该有什么,不该有什么
网上大多数 Node Dockerfile 都把 node_modules 直接拷进镜像、用 root 运行、最后产出一个 900 MB 的层。本文只讲那几个真正影响构建时间、镜像体积和运行时安全的决定:基础镜像、多阶段构建、依赖层缓存、NODE_ENV 陷阱,以及为什么你的 docker-compose 不该照搬生产。
在用户之前发现缺失的翻译键和插值参数不匹配
缺失的翻译键会把原始键路径直接渲染给用户,插值参数不匹配会渲染出空串或崩溃。这两者在评审中都容易漏,因为开发者的语言包永远有全部键。本文讲如何结构化比较 locale JSON 文件、找出缺失键,并在发布前抓住参数不匹配。
能真正压测 UI 的 mock 数据(而不是只把页面填满)
大多数 mock 数据是同一行复制十遍、只换个 id。它填满页面,却什么都测不到。本文讲如何生成能压测布局边界、长名字、缺失字段、空状态,以及会破坏格式化代码的日期和数字格式的 mock 数据,并通过字段推断让一个 JSON 样本一步变成贴近真实的数据集。