如何比较两个 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 响应里粘贴一个片段。代价是格式噪音, 我们接下来就处理它。如果你常驻终端, libxml2xmllint 是值得掌握的工具。

最快得到干净比较结果的步骤

每当有人扔给我两个配置文件并问"有什么不一样?"时,我就是这么做的。 大约十五秒就能完成。

  1. 打开 XML 比较工具
  2. 把原始版本粘贴到左侧,新版本粘贴到右侧。
  3. 点击两侧的 Format,让它们使用相同的缩进。
  4. 扫视真正的差异。绿色表示新增,红色表示删除,而改动的值会各显示一次。
  5. 忽略那些只是属性重新排序或空白字符不同的行。

第三步就是诀窍的大部分。一旦两份文档使用相同的缩进, 剩下被高亮出来的就只有真正的改动了。我们的 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,第一行看起来就变了, 因为 idrole 交换了位置。 把两侧都格式化、按含义比较之后,真实情况其实很简单:

真正改动的内容
节点改动前改动后改动
@roleeditoradmin已修改
seats35已修改
teamplatform已新增
@id77无改动(只是移动了位置)
nameAda LovelaceAda 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 看到 ns1u 不同,就会标记出一个 并不存在的改动。解决办法是按命名空间 URI 而非前缀来比较, 而这正是规范化所做的事。 XML 命名空间 规范可以作为参考,用来终结关于这件事的争论。

需要留意的常见陷阱

陷阱为什么会出问题解决办法
字符编码一个 UTF-8 文件和一个 UTF-16 文件可以承载相同的文本,逐字节却不同归一化编码;XML 声明会写明编码
实体引用同一个字符可能以 &amp; 或字面的 & 两种形式出现做规范化,它会一致地解析实体
CDATA vs 转义文本<![CDATA[a<b]]>a&lt;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:useru:user 是同一个元素。要正确比较,应按 URI 而非前缀做归一化。最简单的办法是用 xmllint --c14n 对两份文档做规范化,它会一致地重写命名空间绑定,然后再对结果做 diff。纯文本 diff 靠自己做不到这一点。
我能比较很大的 XML 文件而不让页面卡死吗?
可以,但有限度。按行模式的 diff 在数千行的文件上依然很快,因为它先比较整行而不是每个字符。非常大的文件(好几 MB)更适合用 xmllint 或 git diff 这类命令行工具处理,它们会流式读取数据。只要是你能在浏览器里舒服滚动浏览的内容,在线 diff 就是更快的选择。
XML 的文本 diff 和结构化 diff 有什么区别?
文本 diff 逐行比较文件,就像比较两篇文章一样。结构化 diff 理解 XML 树,所以它知道重新排序的属性不算改动,还能按路径报告被插入的元素。只要两侧都做了格式化,文本 diff 就更快,而且对大多数审查场景已经足够好。结构化 diff 则在程序需要应用改动、或者你希望忽略元素顺序时才重要。

准备好试试了吗?把你的文件粘贴进 XML 比较工具,看看改动了什么。