如何比较两个 YAML 文件而不被缩进坑到

比较两个 YAML 文件最快的方法,是把两者都粘贴进一个并排显示的 diff 工具, 归一化缩进,然后阅读它高亮出的行。比较本身是简单的部分。 而 YAML 的噪音比一般格式更狡猾:一个多余的制表符、一个重新排序的键, 或者某人给某个值加上的引号,都会让两个加载后数据相同的文件看起来毫无共同之处。

本指南会讲解如何得到干净、可信的 YAML diff。我们会看看两个等价的文件 为什么在字面上渐行渐远,有哪些方法值得了解,以及一个你可以跟着做的完整实例。 如果你只想要工具,我们的 YAML 比较页面可以在浏览器里完成这一切。

为什么 YAML 文件比看上去更难比较

YAML 是一种对空白字符敏感的格式(参见 YAML 1.2.2 规范), 而这正是 diff 变得棘手的原因。缩进承载着含义, 但缩进的数量并不重要,只要它保持一致即可。 所以一个用两个空格缩进的文件和另一个用四个空格缩进的文件, 可以加载出完全相同的结构,而在文本 diff 眼里每一行都不一样。

有一个关键事实需要牢记:YAML 的映射(mapping)是一组键/值对, 和 JSON 对象一样,这些键的顺序并不改变数据。所以这段:

name: Ada Lovelace
role: editor

和这段:

role: editor
name: Ada Lovelace

会加载出相同的映射,尽管按行 diff 会把它们涂成红色和绿色。 而序列(sequence)中的元素则是有序的,所以重新排列一个列表 确实是真正的改动。

看着像改动,其实通常不是
你在 diff 里看到的是真正的改动吗?该怎么做
2 空格缩进 vs 4 空格缩进不是,只有一致性才重要把两侧重新格式化成相同宽度
映射的键顺序不同不是,映射是无序的对两侧都排序键
true vs "true"是,布尔值 vs 字符串需要排查,这是真改动
name: Ada vs name: "Ada"不是,是相同的字符串值统一引号风格
流式风格 [a, b] vs 块状列表不是,是相同的序列两侧统一选用一种风格
序列元素顺序不同是,序列是有序的需要排查,这是真改动

其中有两行是真正的陷阱。不带引号的 true 是布尔值; 加上引号就成了字符串 "true",这个区别造成过真实的线上故障。 但 name: Adaname: "Ada" 是相同的字符串。 如果你想深入了解解析器是如何处理这些的,规范中关于 核心 schema 的章节可以作为参考。

比较 YAML 的四种方法,以及各自适用的场合

没有哪一种方法是绝对最好的。这取决于文件放在哪里,以及你想弄清楚什么。 下面是几种常见选项的对比。

方法适合投入理解 YAML 吗?
用肉眼看很小的文件,一两个键不,你就是解析器
在线 diff 工具快速检查,随处粘贴配合重新格式化,可以
命令行(yq磁盘上的文件、脚本、排序键先排序的话,可以
IDE 或 git diff已在仓库中的文件已提交的话很低默认按行比较

对大多数人来说,浏览器工具在速度上胜出:无需安装, 而且你可以直接从 Kubernetes 清单或 CI 配置里粘贴一个片段。代价是格式噪音, 我们接下来就处理它。如果你常驻终端, yq 是值得学的工具,它可以像 jq 对 JSON 那样排序键。

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

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

  1. 打开 YAML 比较工具
  2. 把原始版本粘贴到左侧,新版本粘贴到右侧。
  3. 点击两侧的 Format,让它们使用相同的缩进。
  4. 开启 排序键,这样重新排序的映射键就不会再显示为改动。
  5. 阅读结果。绿色表示新增,红色表示删除,而改动的值会各显示一次。

第三步和第四步就是全部诀窍。一旦两个文件使用相同的缩进、键也排好序, 剩下被高亮出来的就只有真正的改动了。我们的 diff 引擎构建在 Google 的 diff-match-patch 之上,它会先逐行比较,所以即使文件很长也依然很快。

一个完整实例

假设你正在审查一处对用户记录的改动。这是改动前:

name: Ada Lovelace
role: editor
active: true
seats: 3

这是同事交给你的改动后版本:

active: true
name: Ada Lovelace
role: admin
seats: 5
team: platform

把它们丢进原始的按行 diff,看起来几乎每一行都动过,因为键的顺序不同。 把两侧重新格式化并排序之后,真实情况其实很简单:

真正改动的内容
改动前改动后改动
roleeditoradmin已修改
seats35已修改
teamplatform已新增
nameAda LovelaceAda Lovelace无改动
activetruetrue无改动(只是移动了位置)

三处真实修改:角色提升、席位数变化,以及一个新增的 team 键。 重新排序只是噪音。从 editor 提升到 admin 正是你在审查时想要抓住的那类改动,而当它被埋在误报之下时,很容易被漏掉。

在命令行上消除格式噪音

如果文件已经在磁盘上,同样的"重新格式化并排序"思路用两条简短的命令就能实现。 yq 可以排序键并重新输出一种规范形式, 这样之后再做普通的 diff 才诚实:

yq -P 'sort_keys(..)' old.yaml > old.sorted.yaml
yq -P 'sort_keys(..)' new.yaml > new.sorted.yaml
diff old.sorted.yaml new.sorted.yaml

现在 diff 只会报告真正改变的值,因为两个文件有着相同的缩进 和相同的键顺序。这就是在浏览器里点击 Format 和排序键的终端等价操作。

YAML 独有的那些陷阱

有几个 YAML 特性会造成让人意外的 diff。锚点和别名 (&name*name)让一个文件可以通过引用重复某个值, 而另一个文件把它完整写出;两者加载出的数据相同,读起来却截然不同。 臭名昭著的"挪威问题"是另一个:不带引号的 nooffyes 在较老的 YAML 1.1 解析器中会被解析成布尔值, 所以 country: NO 可能变成 false。 YAML 1.2 修正了 schema,但仍有大量工具沿用 1.1 的行为。 当某个值看起来像是类型变了时,这是首先要检查的地方。

需要留意的常见陷阱

陷阱为什么会出问题解决办法
用制表符缩进YAML 禁止用制表符缩进;文件可能根本无法解析先把制表符转换成空格
带引号 vs 不带引号的标量"true" 是字符串,true 是布尔值这可能是真正的改动,别不当回事
锚点和别名一个文件内联了某个值,另一个则引用它先解析锚点,再比较展开后的形式
挪威问题在 YAML 1.1 中 no 可能被解析成 false给有歧义的字符串加引号;检查解析器版本
行尾空白字符值后面看不见的空格会显示为改动比较前去除行尾空白

YAML 和 JSON 比看起来更接近

每一份 JSON 文档都是合法的 YAML,因为 YAML 1.2 是 JSON 的超集。 这一点在比较时很有用:如果缩进或锚点让你头疼, 就把两个文件都转成 JSON,用同样的方式格式化,然后对 JSON 做 diff。 包括 PyYAML 在内的许多解析器,都能把 YAML 往返转换成一个可以重新输出为 JSON 的普通数据结构, 这会剥离掉风格上的差异,只留下数据本身。

相关工具

YAML 很少单独出现。如果你要比较同一份数据的 JSON 形式, JSON 比较用的是同样的思路。 环境设置和 .env 文件在 配置比较页面上对齐得很好, 而审查两次 API 调用之间的改动,正是 API 响应 diff 的用武之地。

常见问题

在线比较 YAML 文件会把它们上传到什么地方吗?
在 comparetext.org 上,diff 在你的浏览器里运行。两个 YAML 文件由你自己机器上的 JavaScript 完成比较,所以除非你明确点击保存或分享,否则不会有任何内容被发送到服务器。这让它可以安全地用于 Kubernetes 清单、CI 配置,以及其他你不愿粘贴到某个每敲一次键就上传一次的网站上的数据。
为什么我的两个 YAML 文件每一行都显示为不同?
几乎总是格式问题,而不是真正的改动。可能一个文件用两个空格缩进、另一个用四个,也可能是映射键的顺序不同,或者一个用引号、另一个不用。把两侧重新格式化成相同缩进,再排序键,让顺序不再有影响。这之后,diff 通常会缩小到少数几处真正改变的值。
比较 YAML 时缩进宽度重要吗?
对含义不重要,只有一致性重要。YAML 用缩进来表示结构,但它并不在乎你用两个空格还是四个,只要文件内部每一层都保持一致即可。所以一个两空格的文件和一个四空格的文件可以承载完全相同的数据,在文本 diff 眼里却截然不同。把两者重新格式化成相同宽度就能消除这种虚假差异。不过,制表符是完全不允许用于缩进的。
我该如何在比较 YAML 时忽略键的顺序?
YAML 映射的键是无序的,所以两个文件即便键的顺序不同,只要键相同就承载着相同的数据。要让文本 diff 也认同这一点,比较前先对两侧的键排序。在浏览器里,使用排序键(规范化)选项。在命令行上,yq 用 sort_keys 就能做到。一旦两个文件的键都按相同顺序排好,就只有真正的值改动会显示出来。序列元素仍然是有序的,所以不要对它们排序。
YAML 中的"挪威问题"是什么?
这是一个经典的 YAML 陷阱:不带引号的值 NO 会被解析成布尔值 false 而不是字符串 "NO",于是挪威的国家代码就变成了 false。它出现在 YAML 1.1 解析器中,这类解析器把 yes、no、on 和 off 都当作布尔值。YAML 1.2 收紧了规则,但许多工具仍沿用 1.1 的行为。如果你的 diff 里某个值看起来从一个单词变成了 true 或 false,那么一个未加引号的、形似布尔值的字符串就是最可能的原因。给这个值加上引号即可解决。
我能比较很大的 YAML 文件而不让页面卡死吗?
可以,但有限度。按行模式的 diff 在数千行的文件上依然很快,因为它先比较整行而不是每个字符。非常大的文件(好几 MB)更适合用 yq 或 git diff 这类命令行工具处理,它们会流式读取数据。只要是你能在浏览器里舒服滚动浏览的内容,在线 diff 就是更快的选择。

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