做后端、做数据交换、做配置系统的朋友,对 XML 应该再熟悉不过。这几年 JSON 和 YAML 在轻量接口场景里占了主流,但 XML 从来就没有真正退场——配置文件、行业数据协议、文档标记、图形描述、办公文档的底层结构,到处都有它的身影。我见过不少同事在接手 XML 相关任务时,遇到中文乱码、特殊字符炸结构、大文件解析把内存撑爆这类问题,往往卡上半天才搞清楚。这篇内容不打算讲教科书式的概念,而是把 XML 从基础语法、约束校验、解析选型到实战建模完整走一遍,穿插我踩过的坑和总结的经验,帮你把这块基本功补扎实。
实际工作里,XML 有四个高频场景最适合它:一是系统间交换结构复杂、带约束的正式数据;二是配置文件和规则描述;三是语义化文档的标记与排版;四是需要长期存档、可追溯的数据源。如果你是后端工程师、数据工程师,或者正在做系统集成、报文对接的工作,这一篇几乎就是为你准备的。新手也可以放心阅读,遇到专业术语我会尽量用通俗话解释。
1. XML 解决什么问题,为什么值得系统性学一遍
1.1 XML 诞生的核心目标
XML 是 W3C 在 1998 年发布的可扩展标记语言。它的诞生源于一个很朴素的需求:让数据在交换时不丢失结构,同时又能让人类直接读懂。与 HTML 的固定标签不同,XML 的标签完全由使用者自己定义,这意味着它能适配任意领域。
XML 的三个核心目标值得记住:
- 文档可读:它是一份文本文件,打开就能看到标签和内容;
- 结构可校验:可以声明字段顺序、出现次数、取值范围,接收方在解析前就能验明正身;
- 数据可交换:独立于操作系统、编程语言和数据库,只要遵守规范就能互相读懂。
和数据库表格不同,XML 是树状结构,天然适合表达一对多、多层嵌套的数据关系。比如一个订单包含多个商品,商品又有各自的规格和价格,用树形标记比用多张外键关联的表更直观,也更适合在没有数据库的系统之间传递。
1.2 XML 与 JSON、YAML 的定位差异
很多人问:现在 JSON 不是更流行吗,为什么还要学 XML?我的回答是:两者解决的问题不一样。JSON 强在"轻量、近乎直接对应编程语言的数据结构",XML 强在"它是可验证、可扩展的文档与消息格式"。
我整理过一个对比表格,方便你快速判断一个场景该不该选 XML:
| 对比维度 | XML | JSON | YAML |
|---|---|---|---|
| 结构约束 | 支持 DTD/XSD 严格校验 | 无原生校验,依赖外部 Schema | 无原生校验 |
| 语义标记 | 支持混合内容,可标记文本片段 | 不适合标记文本 | 不适合标记文本 |
| 注释 | 原生支持 | 不支持 | 支持 |
| 二进制数据 | 通过 Base64 内嵌 | Base64 | Base64 |
| 解析性能 | 相对较重 | 轻量 | 轻量 |
| 工具链 | XPath/XSLT/XQuery 等 | 较少 | 较少 |
从这张表能看出来,XML 的"重"不是缺陷,而是特性。如果只是前后端异步接口传数据,JSON 确实够了;但如果要传输一份包含格式、校验规则、注释、多语言内容的正式文档,或者要对接行业数据协议,XML 的校验能力和转换能力是 JSON 很难替代的。
1.3 哪些场景继续在用 XML,哪些场景不该选
继续选 XML 的场景有这些:
- 系统间正式报文:订单、单据、工单、回执等,通常配备 XSD,接收方能自动校验完整性;
- 配置类文件:很多中间件和构建工具的配置格式仍是 XML 结构,改错一个标签整个服务起不来;
- 开放文档标准:比如 SVG 图形、Web 订阅源、各种文档规范底层都是 XML;
- 需要保留注释、格式化、语义混排的存档内容。
不该选 XML 的场景也很明确:日志输出、极简接口、内存态对象直接传输、对性能极其敏感的链路。这些场合用 JSON 或二进制序列化更合适。技术选型不是追新,而是看谁匹配当前的约束条件。
2. XML 语法基础:每个细节都是隐患
2.1 合法的 XML 文档从 XML 声明开始
一个结构完整的 XML 文件,第一行通常是 XML 声明:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>这里有几个容易忽略的点。声明必须出现在文件最开头,前面不能有任何字符,包括空格和换行也不能有。有些人保存文件时带了 UTF-8 BOM,部分解析器可以容忍,但严谨的服务端程序可能直接把 BOM 当成正文报错,所以我建议源码保存一律用 UTF-8 无 BOM。
encoding字段必须和文件实际编码一致。很多中文乱码问题的根源就是:声明写了 UTF-8,文件实际存成了 GBK,或者反过来。带上standalone="yes"表示文档不依赖外部 DTD 声明,能避免一些外部实体加载问题,这个后面会详细说。
2.2 标签命名规则
XML 的命名规则比大多数编程语言的变量名要宽松,但依然有硬性要求:
- 名称可以包含字母、数字、下划线、连字符、点号、冒号;
- 不能以数字或标点开头;
- 不能以
xml开头,这里的大小写变体如XML、Xml也不允许; - 冒号只在命名空间场景下使用,普通元素不要随便加冒号。
我实际踩过的一个坑是:元素名里用了带连字符的字段,比如<order-status>,这个合法,但在某些编程语言里映射成变量名时很别扭,比如 Java 里就变成了getOrder-status(),调用公司内部框架时还要额外写注解映射。设计 XML 结构时,我会提前考虑下游语言的可读性,优先用驼峰或者下划线,少用连字符。
2.3 属性还是子元素:设计取舍
同一个数据,可以写成属性,也可以写成子元素:
<user id="1001" name="张三" /><user> <id>1001</id> <name>张三</name> </user>我的经验原则是:元数据用属性,内容数据用子元素。
- 属性适合放唯一标识、枚举类型、简短且没有层级的值,比如
id、status、code; - 子元素适合放业务内容、长文本、可重复值、未来可能扩展成复杂结构的数据。
为什么强调这一点?因为属性很难做结构扩展。今天<user name="张三"/>没问题,明天想把 name 拆成姓和名,在这个属性里就做不了,只能引入子元素,牵一发而动全身。如果一开始就把用户姓名等可能演化的数据设计成子元素,双方协议兼容性会好很多。属性还有一个限制:同名的重复属性在 XML 里是直接违法的,子元素可以出现多次,属性不能。
2.4 特殊字符与 CDATA 处理
XML 里五个字符必须转义,否则会破坏文档结构:
| 原始字符 | 转义实体 | 含义 |
|---|---|---|
< | < | 小于号 |
> | > | 大于号 |
& | & | 和号 |
" | " | 双引号 |
' | ' | 单引号 |
一段包含大量特殊字符的内容,比如 SQL 语句、正则表达式、模板代码,如果逐个转义,既繁琐又容易漏。此时用 CDATA 区更省心:
<query> <![CDATA[ SELECT * FROM users WHERE name = 'A & B' AND id > 100 ]]> </query>CDATA 区里的内容会原样交给解析器,不需要转义,但要注意它内部不能直接出现字符串]]>,如果业务数据里确实有,必须手动拆开 CDATA 段拼接。这个字符序列极罕见,但一旦出现会让解析器直接报错。
2.5 命名空间:多词表混用的前提
命名空间是 XML 里最让新手头疼、却又是最核心的概念之一。它的作用是在同一个文档中混合多套词表,避免元素名冲突。
举个常见的例子:
<root xmlns="http://example.com/ns/order" xmlns:custom="http://example.com/ns/custom"> <order id="1001" /> <custom:remark>加急处理</custom:remark> </root>xmlns="http://example.com/ns/order"声明了默认命名空间,不带前缀的元素都属于这个命名空间;xmlns:custom="..."声明了带前缀的命名空间,所以<custom:remark>属于另一套词表。命名空间的值看起来像 URL,但它只是一个唯一标识字符串,不要求真实可访问。
在实际解析中,很多问题出在忘了开启命名空间感知。比如 Python 的xml.etree.ElementTree默认会处理命名空间,但取标签时要用完整路径;Java 的DocumentBuilderFactory默认也可以开启namespaceAware。如果不开,带前缀的元素名会被当成"前缀:本地名"这样一个整体字符串,取出来的结果和预期完全错位。我后面写实战解析时会再强调一遍。
3. 约束校验:DTD 和 XSD,怎么选
3.1 DTD:轻量但能力有限
DTD(Document Type Definition)是 XML 自带的约束语言。它可以直接内嵌在文档里,也可以写成独立文件。一个简单的 DTD 长这样:
<!DOCTYPE note [ <!ELEMENT note (to, from, heading, body)> <!ELEMENT to (#PCDATA)> <!ELEMENT from (#PCDATA)> <!ELEMENT heading (#PCDATA)> <!ELEMENT body (#PCDATA)> <!ATTLIST note date CDATA #IMPLIED> ]>它声明了note元素必须恰好包含四个子元素,顺序不能乱,date属性是可选字符串。DTD 的优点是轻量、写起来快,但缺点同样明显:几乎没有数据类型的概念,不能约束"价格是数字且最多两位小数"这种需求;不支持命名空间;语法和 XML 本身不一致,学习成本反而可能更高。
现在的建议很明确:新项目不要选 DTD。只有在维护遗留系统,或者学习和了解 HTML/XHTML 领域的文档类型时,才需要去读它。
3.2 XSD:正规军的核心力量
XSD(XML Schema Definition)是 XML Schema 的现代标准,它有几十种内置数据类型,支持复杂类型、枚举、正则、唯一性约束、自定义类型扩展。同一条约束用 XSD 写出来会更接近编程语言中的类型系统。
一个和上面等价的 XSD 片段:
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="note"> <xs:complexType> <xs:sequence> <xs:element name="to" type="xs:string"/> <xs:element name="from" type="xs:string"/> <xs:element name="heading" type="xs:string"/> <xs:element name="body" type="xs:string"/> </xs:sequence> <xs:attribute name="date" type="xs:date" use="optional"/> </xs:complexType> </xs:element> </xs:schema>这里定义了几个关键点:
sequence表示子元素顺序固定;type表示字段类型,字符串用xs:string,日期用xs:date,金额可以用xs:decimal;use="optional"表示属性可选,默认是required时就必须提供。
XSD 还有一个很实用的能力:通过pattern做正则约束。比如手机号字段想限定为数字加短横线,可以写成:
<xs:element name="mobile" type="phoneType"/> <xs:simpleType name="phoneType"> <xs:restriction base="xs:string"> <xs:pattern value="[0-9]{3}-[0-9]{8}"/> </xs:restriction> </xs:simpleType>3.3 实操建议:约束别只写在代码里
我强烈建议生产系统中的 XML 报文把这个约束做进接入层,而不是只靠业务代码里一堆if...else判断。接入层做 XSD 校验的好处非常明显:数据格式问题在入口就被拦截,业务代码拿到的数据基本是合法数据,NullPointerException、字段缺失、金额为负这类问题会少很多。
校验工具上,Java 生态可以用javax.xml.validation.Validator;Python 环境可以借助 lxml 的etree.XMLSchema类。不过要注意,不是所有解析器都内置 XSD 校验能力,标准库里的xml.etree.ElementTree不提供 XSD 校验,至少得是 lxml 这类增强库才行。如果环境受限,也可以把校验单独做成一个服务,批量导入前统一跑一遍。
4. XML 解析方案全对比
4.1 DOM:直接但吃内存
DOM(Document Object Model)做的事情很直白:把整个 XML 文档读进内存,构建一棵完整的节点树,然后你就可以随机访问任意节点、修改结构、再序列化回文件。
优点是好理解,写起来像操作一棵普通树;缺点是内存开销大。一个几百 MB 的 XML 文件,DOM 模型连同属性、文本、命名空间信息一起加载,内存占用往往是原文件的 5 到 10 倍。我见过同事解析一个数据报表 XML,文件只有 30 MB,服务跑起来内存却飙到 300 MB,最后 OOM 重启。所以 DOM 只适合文档体积小、需要随机读写的场景,比如配置文件、小型数据交换报文。
4.2 SAX:流式推模型
SAX(Simple API for XML)是事件驱动模型。解析器从头到尾扫一遍文档,遇到元素开始、元素结束、文本、注释等节点时触发对应的事件回调。因为是流式处理,内存里不需要保存整棵节点树,无论文件多大,内存占用基本恒定。
SAX 的代价是编程模型回调化。你没法"回到上一个节点",只能自己去维护状态。想从一个大文件里抽出一个订单,往往要写一个状态机,靠布尔变量标记"当前处于客户节点"这种逻辑。代码写起来别扭,调试也要费一番功夫。如果只是想读取数据而不修改结构,且文件特别大,SAX 是可以用命换内存的方案。
4.3 StAX:流式拉模型
StAX(Streaming API for XML)是介于 DOM 和 SAX 之间的方案。它也是流式读取,但方向反过来了:不是解析器主动推事件给你,而是你在循环里主动去拉下一个事件类型。代码上更像 Java 的Iterator,维护状态相对容易一些。
典型的读取循环长这个样子:
while (reader.hasNext()) { int event = reader.next(); if (event == XMLStreamConstants.START_ELEMENT) { if ("order".equals(reader.getLocalName())) { String id = reader.getAttributeValue(null, "id"); // 处理逻辑 } } }这种"自己拉数据"的模型让流程控制很清楚,一旦想跳过某个大节点,可以直接next()继续走。在需要流式处理又想要可控逻辑的场景里,StAX 是比 SAX 更舒适的选项。
4.4 解析方案选型对照
| 解析方式 | 内存占用 | 实现难度 | 适用场景 |
|---|---|---|---|
| DOM | 高,整个文档入内存 | 低,树形操作直观 | 小文件、需要随机读写、修改结构 |
| SAX | 低,流式扫描 | 高,回调状态维护复杂 | 超大文件、只读、顺序遍历 |
| StAX | 低,流式拉取 | 中,循环驱动 | 大文件、需要条件控制跳过的只读场景 |
实际开发中,我通常的选型习惯是:百万字段以内读配置用 DOM,几 GB 的报表和日志用 SAX 或 StAX。如果项目里已经引入了 lxml,它同时支持类 DOM 的ElementTree API和 iterparse 流式处理,可以在同一套代码体系里做取舍,相当方便。
5. 实战:一个跨系统订单交换 XML 的设计与实现
5.1 场景与要求
假设现在有两家公司,A 公司需要把一批订单数据传给 B 公司。接收方要求:
- 一次请求中可携带多个订单;
- 每个订单必须包含客户信息、商品明细;
- 商品数量为整数,金额保留两位小数;
- 日期使用 ISO 8601 格式,带时区;
- B 公司收到报文后要做结构校验,不合法就整批拒绝。
这个场景用 JSON 也能传,但加上"结构校验、正式留痕"两个要求后,用 XML 加 XSD 会更平整。
5.2 设计 XML 文档结构
先定义原始 XML 报文:
<orderBatch xmlns="http://example.com/order" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" batchId="B2024110001" timestamp="2024-11-08T10:30:00+08:00"> <order id="O20241100001"> <customer> <name>智测贸易有限公司</name> <mobile>13900000000</mobile> </customer> <items> <item sku="SKU-A1001"> <name>工业传感器</name> <quantity>2</quantity> <price>399.00</price> </item> <item sku="SKU-B2002"> <name>数据采集器</name> <quantity>1</quantity> <price>8990.00</price> </item> </items> </order> </orderBatch>这里把batchId和timestamp放到批量节点上,id和sku放属性,其余业务数据放子元素,遵循之前讲的"元数据用属性、内容用子元素"原则。
5.3 编写 XSD 约束
针对上面的结构,只给出关键的 XSD 片段:
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://example.com/order" xmlns="http://example.com/order" elementFormDefault="qualified"> <xs:element name="orderBatch"> <xs:complexType> <xs:sequence> <xs:element name="order" maxOccurs="unbounded" type="orderType"/> </xs:sequence> <xs:attribute name="batchId" type="xs:string" use="required"/> <xs:attribute name="timestamp" type="xs:dateTime" use="required"/> </xs:complexType> </xs:element> <xs:complexType name="orderType"> <xs:sequence> <xs:element name="customer"> <xs:complexType> <xs:sequence> <xs:element name="name" type="xs:string"/> <xs:element name="mobile" type="mobileType"/> </xs:sequence> </xs:complexType> </xs:element> <xs:element name="items"> <xs:complexType> <xs:sequence> <xs:element name="item" maxOccurs="unbounded"> <xs:complexType> <xs:sequence> <xs:element name="name" type="xs:string"/> <xs:element name="quantity" type="xs:int"/> <xs:element name="price"> <xs:simpleType> <xs:restriction base="xs:decimal"> <xs:fractionDigits value="2"/> </xs:restriction> </xs:simpleType> </xs:element> </xs:sequence> <xs:attribute name="sku" type="xs:string" use="required"/> </xs:complexType> </xs:element> </xs:sequence> </xs:complexType> </xs:element> </xs:sequence> <xs:attribute name="id" type="xs:string" use="required"/> </xs:complexType> <xs:simpleType name="mobileType"> <xs:restriction base="xs:string"> <xs:pattern value="1[0-9]{10}"/> </xs:restriction> </xs:simpleType> </xs:schema>这里有两个细节值得注意。targetNamespace和elementFormDefault="qualified"配合使用后,所有子元素都必须带命名空间前缀或者属于默认命名空间,校验起来更严格,不会把别的文档里同名的元素误判成合法数据。金额字段用xs:decimal并且限定位数为 2,未来即使有人传399.9也会被拒绝,因为399.9不是两位小数的合法型值,必须写成399.90。
5.4 用代码实现生成与解析
生成端用 Python 标准库示例:
import xml.etree.ElementTree as ET def build_order_batch(batch_id, timestamp, orders): ns = "http://example.com/order" ET.register_namespace("", ns) root = ET.Element( f"{{{ns}}}orderBatch", attrib={"batchId": batch_id, "timestamp": timestamp}, ) for order in orders: order_el = ET.SubElement(root, f"{{{ns}}}order", attrib={"id": order["id"]}) customer_el = ET.SubElement(order_el, f"{{{ns}}}customer") ET.SubElement(customer_el, f"{{{ns}}}name").text = order["customer"]["name"] ET.SubElement(customer_el, f"{{{ns}}}mobile").text = order["customer"]["mobile"] items_el = ET.SubElement(order_el, f"{{{ns}}}items") for item in order["items"]: item_el = ET.SubElement( items_el, f"{{{ns}}}item", attrib={"sku": item["sku"]} ) ET.SubElement(item_el, f"{{{ns}}}name").text = item["name"] ET.SubElement(item_el, f"{{{ns}}}quantity").text = str(item["quantity"]) ET.SubElement(item_el, f"{{{ns}}}price").text = f"{item['price']:.2f}" return ET.tostring(root, encoding="utf-8", xml_declaration=True)解析端同样可以用 ElementTree:
import xml.etree.ElementTree as ET def parse_order_batch(xml_text): ns = {"o": "http://example.com/order"} root = ET.fromstring(xml_text) batch_id = root.attrib["batchId"] orders = [] for order_el in root.findall("o:order", ns): customer = order_el.find("o:customer", ns) items_el = order_el.find("o:items", ns) items = [] for item_el in items_el.findall("o:item", ns): items.append({ "sku": item_el.attrib["sku"], "name": item_el.find("o:name", ns).text, "quantity": int(item_el.find("o:quantity", ns).text), "price": item_el.find("o:price", ns).text, }) orders.append({ "id": order_el.attrib["id"], "customer": { "name": customer.find("o:name", ns).text, "mobile": customer.find("o:mobile", ns).text, }, "items": items, }) return batch_id, orders注意findall里使用的命名空间前缀,必须和ns字典中的键一致。ElementTree 在带命名空间的 XML 中,会把标签内部名称变成{命名空间}本地名这样的格式,用o:order这种写法只是语法糖,最终合成为{http://example.com/order}order。
5.5 这个实战里的关键细节
金额字段全程不要用浮点数处理。399.00这类值,解析成float后可能变成399.0,序列化时很容易丢失后面的零。我的做法是直接以字符串形式保留,入库时再转成数据库的DECIMAL类型,运算交给数据库做,避免二进制浮点误差。
日期时间字段同样是重灾区。报文里写了带时区的+08:00,解析端必须按 ISO 8601 转换,不能直接当成无时区的本地时间。Python 里可以用标准库datetime.fromisoformat转换,但不同 Python 版本对时区标志的解析能力不一致,稳妥点是统一先把字符串转成 UTC 存储,展示时再转目标时区。
6. 实践中的常见问题与排查技巧
6.1 中文乱码
中文乱码十有八九是编码声明与实际编码不一致。最简单的处理办法就是全局统一 UTF-8:文件保存用 UTF-8 无 BOM,内容本身不含 BOM,XML 声明也写 UTF-8。收到对方传来的 XML 时,优先读取二进制签名和前几百字节,通过声明判断再用对应编码解码。
某些老系统还在用 GBK 生成 XML,这种报文不能硬转 UTF-8。正确做法是先用原始编码读入,再做转码。Python 中可以用bytes.decode("gbk")得到字符串,再重新编码为 UTF-8 发送给下游。不要试图用chardet猜编码来规避问题,猜中的概率在正式环境里不够高。
6.2 特殊字符与注入问题
用字符串拼接生成 XML 是高风险操作。如果用户输入的内容里恰好有<或者&,拼接出的文档结构会直接被破坏。更严重的是,如果内容里包含]]>或伪造的 XML 声明,可能注入出非预期结构。
防御手段只有一条:永远使用解析器自带的序列化能力,不要手工拼接。ElementTree 的text赋值、Java 的Document节点创建,解析器都会自动做转义。假设你的代码里出现了类似<tag>" + content + "</tag>的写法,不用怀疑,这就是一个等待爆发的隐患。
6.3 外部实体与 XXE 攻击
XML 规范允许通过 DTD 引用外部实体,比如:
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>如果一个解析器没有禁掉外部实体解析,攻击者就能通过构造报文读取服务器上的本地文件,这就是 XXE(XML 外部实体攻击)。生产环境必须明确关闭外部实体加载。
Python 的 lxml 解析时可以这样限制:
from lxml import etree parser = etree.XMLParser( resolve_entities=False, no_network=True, load_dtd=False, ) root = etree.fromstring(xml_data, parser=parser)Java 的DocumentBuilderFactory可以设置:
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); factory.setFeature("http://xml.org/sax/features/external-general-entities", false); factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);强烈建议默认就禁用 DTD 和外部实体,除非业务确实需要 DTD 校验。哪怕不需要 XXE 这个角度,减少 DTD 处理也能显著降低解析复杂度。
6.4 大文件解析性能问题
大 XML 用 DOM 是慢性自杀。我参与过一个日志分析任务,单文件 2 GB,一开始用了 DOM 解析,加载阶段就直接内存溢出。后来换成流式迭代,分批处理节点,整机内存占用从 2 GB 降到 100 MB 以内。
Python 里可以用iterparse来做增量解析:
import xml.etree.ElementTree as ET for event, elem in ET.iterparse("large.xml", events=("end",)): if elem.tag == "order": process_order(elem) elem.clear()elem.clear()是关键,它在处理后释放已用过的子树,让内存峰值稳定。不调用 clear,即使用了流式接口,整个树越积越大,效果和 DOM 一样糟糕。
6.5 日期时间与数字精度问题
日期时间的坑主要在时区和格式。XML 原生类型xs:dateTime默认要求格式为2024-11-08T10:30:00,带时区时允许追加+08:00或Z。解析时应先把时区对齐,再应用到业务逻辑中,不能直接把带时区的字符串当成发送方本地时间存储。
数字精度上,大整数和小数都容易出问题。xs:integer是无上限的,但很多语言原生整数类型有上限,比如 Java 的int只有 32 位。解析 JSON 时老生常谈的长整数精度问题,在 XML 里一样存在。传入大 ID 时,必须确认目标语言能承载它的范围。
6.6 空白与格式化问题
XML 规范中,元素内的空白符(空格、换行、制表符)在没有语义标记时可能被保留或忽略,各解析器行为不完全一致。带格式化的 XML 文件经常有一堆缩进空格,如果它混在数据文本里,比如:
<description> 这是一个产品描述 包含换行 </description>解析出来的text会带着换行和缩进。如果你的上下游系统对文本有严格校验,最好在 XML 结构上明确这些空白是否有意义,或者让发送方使用 CDATA 与普通文本区分开来,接收方做解析时再对无意义空白做剥离。
7. 一些个人体会
做了这么久的数据交换和系统集成,我对 XML 最大的感受是:它没有过时,只是从"默认方案"变成了"特定方案"。很多人觉得 XML 难,其实是难在语法松散、同一个问题有多种写法,所以才更需要用 XSD、命名空间、流式解析这些工具把它牢牢约束住。
我个人的学习顺序建议是:先把语法和命名空间吃透,接着动手写 XSD,把类型、枚举、正则约束都试一遍,然后拿一个大文件练流式解析,最后把生成、校验、解析、错误处理串成一条完整的链路。这样走一遍之后,你再去看 JSON、YAML 或者其他标记格式,会发现很多设计思想都是相通的。
最后分享一个日常调试技巧:拿到一个 XML 文件,先在命令行跑一遍xmllint --noout 文件.xml,确认格式正确后再进入业务代码排查;想快速从大 XML 里抽少数节点,直接写 XPath 比手写遍历快得多。工具用得越熟练,踩坑的概率就越低。