如何比较两个 XML 文件并查看改动内容
比较两个 XML 文件最快的方法,是把两者都粘贴进一个并排显示的 diff 工具, 用同样的方式美化排版,然后阅读它高亮出的行。比较本身是简单的部分, 让人栽跟头的是噪音:重新排序的属性、标签之间的空白字符,以及命名空间前缀, 都会让两个含义相同的文件看起来毫无共同之处。
本指南会讲解如何得到干净、可信的 XML diff。我们会看看两份等价的文档 为什么在字面上渐行渐远,有哪些方法值得了解,以及一个你可以跟着做的完整实例。 如果你只想要工具,我们的 XML 比较页面可以在浏览器里完成这一切。
为什么 XML 文件比看上去更难比较
XML 有严格的语法(参见 W3C XML 规范), 但它在文本排布上给了书写者很大余地。两份文档可以描述完全相同的数据, 逐字节看却并不一致。纯文本 diff 对此毫无理解,于是会把这些全部标记出来。
有一个关键事实需要牢记:在 XML 中,元素上属性的顺序并不重要。
XML 信息集
把属性视为一个无序集合。所以
<user id="7" role="admin"/> 和
<user role="admin" id="7"/> 承载着相同的信息,
尽管按行 diff 会把它们涂成红色和绿色。而元素的顺序,则通常是重要的。
| 你在 diff 里看到的 | 是真正的改动吗? | 该怎么做 |
|---|---|---|
| 属性顺序不同 | 不是,属性顺序不重要 | 对两侧做规范化 |
| 2 空格缩进 vs 4 空格缩进 | 不是 | 用同样方式美化两侧排版 |
| 元素之间的空白字符 | 通常不是 | 格式化,或去除无关紧要的空白 |
<br/> vs <br></br> | 不是,是同一个空元素 | 对两侧做规范化 |
| 同一个 URI 用了不同的命名空间前缀 | 不是,前缀只是任意的标签 | 按命名空间 URI 比较,而非按前缀 |
| 子元素顺序不同 | 通常是,元素顺序有意义 | 需要排查,这很可能是真改动 |
最后一行是需要留意的。属性顺序是自由的,但在大多数 schema 中, 子元素的顺序是文档的一部分。如果你想了解解析器是如何看待这一切的, MDN 上有一份扎实的参考文档,讲解 用 DOMParser 解析 XML。
比较 XML 的四种方法,以及各自适用的场合
没有哪一种方法是绝对最好的。这取决于文件放在哪里,以及你想弄清楚什么。 下面是几种常见选项的对比。
| 方法 | 适合 | 投入 | 理解 XML 吗? |
|---|---|---|---|
| 用肉眼看 | 很小的文件,一两个元素 | 低 | 不,你就是解析器 |
| 在线 diff 工具 | 快速检查,随处粘贴 | 低 | 配合美化排版,可以 |
命令行(xmllint) | 磁盘上的文件、脚本、规范化形式 | 中 | 可以,配合 --c14n |
IDE 或 git diff | 已在仓库中的文件 | 已提交的话很低 | 默认按行比较 |
对大多数人来说,浏览器工具在速度上胜出:无需安装,
而且你可以直接从配置文件或 SOAP 响应里粘贴一个片段。代价是格式噪音,
我们接下来就处理它。如果你常驻终端,
libxml2
的 xmllint 是值得掌握的工具。
最快得到干净比较结果的步骤
每当有人扔给我两个配置文件并问"有什么不一样?"时,我就是这么做的。 大约十五秒就能完成。
- 打开 XML 比较工具。
- 把原始版本粘贴到左侧,新版本粘贴到右侧。
- 点击两侧的 Format,让它们使用相同的缩进。
- 扫视真正的差异。绿色表示新增,红色表示删除,而改动的值会各显示一次。
- 忽略那些只是属性重新排序或空白字符不同的行。
第三步就是诀窍的大部分。一旦两份文档使用相同的缩进, 剩下被高亮出来的就只有真正的改动了。我们的 diff 引擎构建在 Google 的 diff-match-patch 之上,它会先逐行比较,所以即使文件很长也依然很快。
一个完整实例
假设你正在审查一处对服务配置的改动。这是改动前:
<user id="7" role="editor">
<name>Ada Lovelace</name>
<active>true</active>
<seats>3</seats>
</user>
这是同事交给你的改动后版本:
<user role="admin" id="7">
<name>Ada Lovelace</name>
<active>true</active>
<seats>5</seats>
<team>platform</team>
</user>
把它们丢进原始的按行 diff,第一行看起来就变了,
因为 id 和 role 交换了位置。
把两侧都格式化、按含义比较之后,真实情况其实很简单:
| 节点 | 改动前 | 改动后 | 改动 |
|---|---|---|---|
@role | editor | admin | 已修改 |
seats | 3 | 5 | 已修改 |
team | — | platform | 已新增 |
@id | 7 | 7 | 无改动(只是移动了位置) |
name | Ada Lovelace | Ada Lovelace | 无改动 |
三处真实修改:角色提升、席位数变化,以及一个新增的 team 元素。
属性交换只是噪音。从 editor 提升到 admin
正是你在审查时想要抓住的那类改动,而当它被埋在一行被 diff 误报的内容之下时,
很容易被漏掉。
规范化 XML:忽略噪音的正规做法
格式化两侧能解决缩进问题,但针对这个问题其实有一个专门的标准。 规范化 XML 由 W3C 在 Canonical XML 1.1 中定义,它把文档重写成单一的归一化形式:属性排序、空元素展开、 标签内空白字符归一化,并把默认属性显式写出。两份等价的文档会产出 完全相同的规范化输出。这相当于 XML 版的"排序 JSON 键"。
xmllint --c14n old.xml > old.c14n.xml
xmllint --c14n new.xml > new.c14n.xml
diff old.c14n.xml new.c14n.xml
现在 diff 只会报告真正改变的内容,因为两个文件都以相同的方式
被归一化了。如果你只想要可读的缩进、而非严格的规范化形式,
xmllint --format file.xml 会做美化排版,
这就是在浏览器里点击 Format 的终端等价操作。
命名空间:让所有人困惑的那部分
XML 命名空间让两份文档可以用不同的前缀标签使用同一套词汇表。
绑定到某个 URI 的 <ns1:user> 和绑定到同一个 URI 的
<u:user> 是同一个元素;前缀只是一个本地的昵称。
文本 diff 看到 ns1 和 u 不同,就会标记出一个
并不存在的改动。解决办法是按命名空间 URI 而非前缀来比较,
而这正是规范化所做的事。
XML 命名空间
规范可以作为参考,用来终结关于这件事的争论。
需要留意的常见陷阱
| 陷阱 | 为什么会出问题 | 解决办法 |
|---|---|---|
| 字符编码 | 一个 UTF-8 文件和一个 UTF-16 文件可以承载相同的文本,逐字节却不同 | 归一化编码;XML 声明会写明编码 |
| 实体引用 | 同一个字符可能以 & 或字面的 & 两种形式出现 | 做规范化,它会一致地解析实体 |
| CDATA vs 转义文本 | <![CDATA[a<b]]> 和 a<b 是相同的文本内容 | 比较解析后的值,而不是原始字节 |
| 有意义的空白字符 | 在 xml:space="preserve" 内部,空格是有意义的,不能去除 | 不要盲目裁剪;尊重 xml:space |
| 自闭合标签 | <x/> 和 <x></x> 是完全相同的 | 做规范化,让两者以相同方式呈现 |
文本 diff vs 结构化 diff
上面讲的全都是文本 diff:快速、直观,非常适合人来阅读一处改动。 结构化 diff 更进一步,它用 XML 树的语言描述改动: 这个属性变了、那个子元素在这个路径上被插入了。当需要由程序来应用这处改动, 或者当元素顺序确实无关紧要而你希望忽略它时,你才需要结构化 diff。 对日常审查来说,对两份已格式化文档做文本 diff 已经绰绰有余。
相关工具
XML 很少是你要打交道的唯一格式。如果你要比较 API 载荷, JSON 比较把同样的思路用在了 JSON 上。 带标记的页面在 HTML 比较页面上更容易阅读, 而环境设置则在 配置比较工具里对齐得很好。
常见问题
- 在线比较 XML 文件会把它们上传到什么地方吗?
- 在 comparetext.org 上,diff 在你的浏览器里运行。两个 XML 文件由你自己机器上的 JavaScript 完成比较,所以除非你明确点击保存或分享,否则不会有任何内容被发送到服务器。这让它可以安全地用于配置文件、SOAP 消息,以及其他你不愿粘贴到某个每敲一次键就上传一次的网站上的数据。
- 为什么我的两个 XML 文件每一行都显示为不同?
- 几乎总是格式问题,而不是真正的改动。可能一个文件是压缩过的、或用制表符缩进,另一个用两个空格;也可能是属性的顺序不同。点击两侧的 Format 让它们使用相同缩进。这之后,diff 通常会缩小到少数几处真正改变的值。如果需要更严格的归一化,可以先用 xmllint --c14n 对两个文件做规范化。
- 比较 XML 时属性顺序重要吗?
- 不重要。在 XML 中,元素上的属性是一个无序集合,所以
<a x="1" y="2"/>和<a y="2" x="1"/>是等价的。纯文本 diff 不知道这一点,会把重新排序标记为改动。规范化 XML 会把属性排成固定顺序,所以在比较前对两侧做规范化,这个误报就会消失。相比之下,元素顺序通常是有意义的。 - 我该如何在比较 XML 时忽略命名空间前缀?
- 命名空间前缀只是命名空间 URI 的本地标签,所以绑定到同一个 URI 的
ns1:user和u:user是同一个元素。要正确比较,应按 URI 而非前缀做归一化。最简单的办法是用 xmllint --c14n 对两份文档做规范化,它会一致地重写命名空间绑定,然后再对结果做 diff。纯文本 diff 靠自己做不到这一点。 - 我能比较很大的 XML 文件而不让页面卡死吗?
- 可以,但有限度。按行模式的 diff 在数千行的文件上依然很快,因为它先比较整行而不是每个字符。非常大的文件(好几 MB)更适合用 xmllint 或 git diff 这类命令行工具处理,它们会流式读取数据。只要是你能在浏览器里舒服滚动浏览的内容,在线 diff 就是更快的选择。
- XML 的文本 diff 和结构化 diff 有什么区别?
- 文本 diff 逐行比较文件,就像比较两篇文章一样。结构化 diff 理解 XML 树,所以它知道重新排序的属性不算改动,还能按路径报告被插入的元素。只要两侧都做了格式化,文本 diff 就更快,而且对大多数审查场景已经足够好。结构化 diff 则在程序需要应用改动、或者你希望忽略元素顺序时才重要。
准备好试试了吗?把你的文件粘贴进 XML 比较工具,看看改动了什么。