这不是YAML规范的错,但是
摘要
这篇文章讨论了对YAML的常见批评,比如其庞大的规范、不一致的库实现以及隐式类型问题,并分享了作者在项目中的YAML使用个人经历。
<p><a href="https://lobste.rs/s/bsvc7a/it_s_not_yaml_spec_s_fault">评论</a></p>
查看缓存全文
缓存时间: 2026/09/10 18:16
# 这不是YAML规范的错,但是……
来源:https://slugcat.systems/post/26-09-10-yaml-spec/
2026年9月10日 星期四
我又看到一篇博客文章在抨击YAML了。作者一边高调重复“挪威问题”,一边却散布错误信息。于是我亲自调查了一番,以下是我的发现。
## YAML的问题
(https://slugcat.systems/post/26-09-10-yaml-spec/#the-problems-with-yaml)
> YAML™(读音与“camel”押韵)是一种人类友好、跨语言、基于Unicode的数据序列化语言,围绕动态编程语言的常见原生数据类型设计。它广泛适用于从配置文件、网络消息传递到对象持久化以及数据审计和可视化等多种编程需求。
多年来,YAML在许多场景中变得非常流行。根据你的编程经验,你可能曾在配置文件或其他地方见过它。在此期间,它也招致了大量批评。主要存在三大问题,且它们相互影响:
- 规范异常庞大
- 库实现参差不齐
- 隐式类型转换(“挪威问题”)
其中一些批评是合理的,但也有些是误解。至少我最初是这样认为的。让我们仔细看看。
## 我的YAML使用经历
(https://slugcat.systems/post/26-09-10-yaml-spec/#my-own-experiences-with-yaml)
在我多年的编程生涯中,曾在多个项目中使用过YAML、TOML、JSON等格式。我对这些格式都有些看法,但这里重点谈谈YAML。
我九年前第一个真正使用YAML的项目是自定义的Python Discord机器人。这大概是我第一个不参与Space Station 131而独立完成的“大型”项目,因此许多事情都要自己处理。我最终为配置文件使用了YAML。当时我并未遇到YAML常见的那些问题,但我知道这只是运气好。我只用了`yaml.safe_load()`,没有其他复杂操作。
但这并不意味着YAML的使用完全没有问题。我的配置文件工作方式有点笨拙——我实际上用了两个文件:`config.yml`和`override.yml`。因为我需要始终加载`config.yml`来提供基本结构和默认值,然后将`override.yml`合并进去,形成Python代码访问的嵌套数据结构。但这难道是YAML的错吗?不,不是。我在使用`pickle`文件持久化数据时遇到了其他序列化问题,比如“`defaultdict`实例难以序列化,因为它们本质上存储了lambda表达式”。
现在回想起来,我意识到这里的共同根源是Python,或者更准确地说,是动态语言本身。在Python这样的动态语言中,要正确实现“对象与序列化格式之间的往返转换”几乎是不可能的。
总之,下一个项目。**Space Station 14**使用YAML存储所有“原型”(以及其他一些内容),即实体、配方等大约200多种数据定义。该项目最初实际上使用XML(https://github.com/ss13remake/ss13remake/blob/1f41f4b7649249b099fe1844b140ad97a87f22a2/SS3d_server/JobDefinitions.xml),但当我接手时,决定将其切换为YAML。总体而言,这是一次“巨大成功”。我们遇到的最大问题是,新贡献者有时难以意识到YAML对空白字符敏感,从而导致低级的语法错误。这很烦人,但并非致命问题。
如何避免“挪威问题”?秘诀在于:虽然具体实现细节一直在变化(目前已是“serv**3**”版本,实际上可能已经可以算作4.0了),但我们始终用自己的代码执行实际的对象反序列化逻辑。也就是说,我们使用库提供的图结构对象(`YamlMappingNode`、`YamlScalarNode`等),并自行解析。当我们不需要布尔值时,不会将“`no`”反序列化为布尔值——只有当我们读取的字段需要布尔值时,才会进行布尔值反序列化。这显而易见。
当然,这才是正确的对象序列化方式。你应该针对程序代码中定义的模型进行序列化。这就是我们在Space Station 14避免混乱的方式,也是我当初在Discord机器人中本应避免混乱的方式。所以,当看到那些声称“YAML很差”因为它可能错误反序列化“`no`”的帖子时,我脑海里只想说:“这完全是动态类型语言使用者技能不足的问题。”
但这真的是事实吗?如果我错了怎么办?如果*我们*用错了YAML,而规范要求了这种行为呢?那我可就大错特错了!所以……让我们查查规范吧!
## 规范怎么说
(https://slugcat.systems/post/26-09-10-yaml-spec/#what-does-the-spec-say)
根据YAML官方网站(https://yaml.org/),YAML有几个重要修订版:1.0(2004年1月)、1.1(2005年1月)、1.2(2009年7月)。
让我们先看看1.0版本(https://yaml.org/spec/1.0/)怎么说。如果你开始深入研究,很快就会发现规范对于类型转换和解析的具体细节描述得相当简略。第2.4节(https://yaml.org/spec/1.0/#id2490971)这样写道:
> 在YAML中,普通(无引号)标量根据应用程序被赋予隐式类型。本规范中的示例使用了YAML标签仓库(https://yaml.org/spec/1.0/#tag-repository)中的类型,包括整数、浮点数、时间戳、空值、布尔值和字符串值。
点击链接,我们看到:
> 以下是三个强制核心标签的描述。YAML要求支持`seq`(https://yaml.org/spec/1.0/#type-seq)、`map`(https://yaml.org/spec/1.0/#type-map)和`str`(https://yaml.org/spec/1.0/#type-str)标签。YAML还提供了一组非强制性的通用标签,可在YAML标签仓库(地址为https://yaml.org/spec/type.html)中找到。这些标签代表了大多数编程语言中的原生数据类型,或在各种应用中都很有用。因此,强烈建议应用程序在适当的时候使用它们,以提高YAML系统之间的互操作性。
最后一个链接已失效,但它可能指向类似YAML 1.1版本的整数语言无关类型(https://yaml.org/type/)之类的内容。例如,查看整数类型(https://yaml.org/type/int.html):
> 解析和验证:有效值必须匹配以下正则表达式,该表达式也可用于隐式标签解析:
> `[-+]?0b[0-1_]*#(二进制)`
> `|[-+]?0[0-7_]*#(八进制)`
> `|[-+]?(0|[1-9][0-9_]*)#(十进制)`
> `|[-+]?0x[0-9a-fA-F_]*#(十六进制)`
> `|[-+]?[1-9][0-9_]*(:[0-5]?[0-9])+#(六十进制)`
如果我们进一步搜索主规范中关于隐式类型的内容,会在第3.3.2节(https://yaml.org/spec/1.0/#id2559057)看到:
> 普通标量样式允许无引号的值表示数字、日期或其他类型化数据,而带引号的值则被视为通用字符串。通过这个例外,处理器可以将普通标量与一组正则表达式匹配,从而在没有显式[sic]标签的情况下自动解析这些类型。
提醒一下:YAML规范确实遵循RFC 2119(https://www.ietf.org/rfc/rfc2119.txt),其中“可能”等关键词具有特定含义。你*可能*注意到规范中使用了“可能”以及其他模棱两可的措辞,如“取决于应用程序”。确实:我对YAML 1.0的理解是,这完全由应用程序决定。YAML 1.1似乎没有太大变化,只是让相关语言变得更复杂。同样在第3.3.2节(https://yaml.org/spec/1.1/#tag%20resolution/):
> 标签解析是特定于应用程序的,因此YAML处理器应提供一种机制,允许应用程序指定标签解析规则。[…]
直到YAML 1.2,语言才变得不那么模棱两可。现在有了真正的“模式”概念,其中包含隐式标签解析规则,而“核心模式”是“推荐”的默认选项:
> *核心模式*是JSON模式(https://yaml.org/spec/1.2.0/#schema/JSON/)的扩展,允许以更易于人类阅读的方式呈现相同类型。这是推荐的默认模式,除非另有指示,YAML处理器应使用它。同样强烈建议其他模式应基于它构建。
但等等!YAML 1.2的模式没有六十进制(六十进制)或yes/no布尔值!因此显然“挪威问题”并非YAML 1.2推荐所导致!
看,我之前已经读过YAML规范这部分好几次了。我看到大量的“可能”和“取决于应用程序”,心想:“好吧,所以PyYAML、Ruby和其他库只是选择了一个有缺陷的默认设置。”鉴于这些库都将不安全加载作为默认选项,很容易得出这种简化结论,这显然表明它们最初的设计就不够完善。但真的仅此而已吗?我们可以检查旧的草案规范!也许它们能提供更多信息。
2001年12月的规范草案首次明确记录了隐式类型转换(https://yaml.org/spec/history/2001-12-10.html#trans-implicit),并且根据我的理解,它*始终处于激活状态*!有趣!跳过大约一年,2002年10月的草案中,规则似乎再次改变(https://yaml.org/spec/history/2002-10-31.html#trans-implicit),现在变成了“取决于应用程序”。因此看来在草案制定过程中,他们实际上*改变了想法*!
## 检查邮件列表
(https://slugcat.systems/post/26-09-10-yaml-spec/#checking-the-mailing-list)
“为什么”不是你在草案规范中能得到的答案,更不用说有变更日志了。要找出所有规范措辞的来源,我们只能查看背后的讨论。YAML的情况主要是在2000年代初期的一个邮件列表中进行的。
感谢上帝,该列表的存档至今仍可在SourceForge上访问(https://sourceforge.net/p/yaml/mailman/yaml-core/)。但令人恼火的是,SourceForge上的列表没有公共存档下载功能(仅项目管理员可用),它按月划分并分页。这意味着所有相关邮件(数千封)分布在大量的浏览器标签页中。如果没有将这些邮件在合适的邮件客户端中打开的能力,深入研究根本不可能。
因此我用Python抓取了SourceForge的网站。如果他们不希望我抓取,就应该负责任地提供易于访问的公共存档。公共查看器只提供用户名、纯文本消息内容和时间值(出于隐私原因没有电子邮件地址),但这足以让我将内容导出为`.mbox`文件并在Thunderbird中加载。我写的脚本在这里(https://slugcat.systems/post/26-09-10-yaml-spec/import.py)和这里(https://slugcat.systems/post/26-09-10-yaml-spec/to%20mbox.py),如果你需要的话。我抓取的mbox文件在这里(https://slugcat.systems/post/26-09-10-yaml-spec/messages.7z)。
[展示邮件列表邮件的Thunderbird截图]
没错,邮件量很大
有数千封邮件,我只看了百分之几。我主要寻找与隐式类型转换和标签解析相关的信息:它是如何产生的?规范作者的意图是什么?当然,通过关键词在数百封邮件中搜索不可避免地让我读到了更多内容。
这一切的核心是几个有动力的人在做他们想做的事。有荒唐事,有各种出于兴趣出现的新面孔。有长时间的辩论。有一次规范网站离线,因为服务器托管在秘鲁。这是我第一次如此“深入调查”这类事情,虽然古老的邮件列表对我来说很陌生,但其氛围仍然让我感到有些熟悉。
## 旁注:YAML为什么“这样”?
(https://slugcat.systems/post/26-09-10-yaml-spec/#aside-why-is-yaml-like-that)
在继续之前,我必须说明:我生于2000年,而这些人在我学会走路之前就在讨论隐式类型规则了。我并非亲历者,因此只能对当时的大致背景进行推断。
YAML起源于千禧年之交的XML“热潮”周期。很多人和企业认为XML是神奇的未来技术,因为*数据互操作性*。我们将所有数据放入XML,现在我们有了一堆神奇的工具:XSD、XPath、XSLT,天知道还有哪些以‘X’开头的缩写。见鬼,有些公司甚至销售*硬件中间件*,其唯一功能就是基于更多XML来验证和转换XML!这种语言被用于*一切*:配置文件、序列化状态、数据库、RPC协议等。
但是,XML很奇怪,它是一种*标记语言*。与HTML比较:如果你从这个网页中移除所有*标记*……它仍然具有一定的连贯性,至少对人类来说。但对典型用法的XML这样做呢?它会完全失去意义。我们真的在标记东西吗?
```
1313 UDP 1313 192.168.50.3 1 RobustToolbox UDP 0
```
此外,如果你曾尝试为任何东西设计XML格式,你可能不太确定某个内容应该是标签名、属性还是文本内容。我确信许多人写过关于这个主题的、观点强烈的指南,但事实是这真的不直观。而且,看看上面例子中的重复量。
你可能已经意识到,JSON或YAML没有这些问题。如果你对其中任何一种略有了解,就能轻松想象上面例子用它们表示会是什么样子!YAML的设计初衷就是为了支持XML的所有用例,这意味着它在设计时考虑了相关的“工具箱”。数据可移植性、序列化、配置文件,几乎一切。许多人有截然不同的用例,这反映在我读到的一些邮件中。
为什么YAML规范比JSON复杂得多?因为,嗯,它*想*复杂。规范中充斥着关于“YAML处理器”应如何工作的描述。在邮件列表中,有很多关于“YPATH”、“YAML模式”、“YAML-RPC”等的零星提及和想法。*人们想用YAML实现XML能做的一切。*
当然,XML热潮周期过去了,对这些YAML等价物的需求也随之消失。我提到的那些雄心勃勃的目标大多从未实现。如今,人们主要将YAML作为编写配置文件时比JSON更友好的替代品。至于这是好事还是坏事,留给你判断。
我有很多
相似文章
YAML:问题一大堆
一篇讽刺文章,指出YAML在DevOps和编程环境中的各种缺陷和怪癖,例如解析错误和Kubernetes等工具中的配置错误。
为YAML辩护
Posit的一篇博文为YAML辩护,反驳当前普遍认为TOML更优的共识,回顾了配置格式的历史,并指出YAML的规范与工具已经演进,解决了过去的批评。
在 Rust 中“尊重原格式”地修补 YAML
本文评估了多款 Rust 库,旨在找出能够在编程修改 YAML 文件时保留原始格式与注释的最佳工具。
YAML?那是挪威问题
探讨了臭名昭著的 YAML '挪威问题',即国家代码 'NO' 被解析为布尔值 false,追溯其历史并提供如加引号等解决方案。
你讨厌XML吗?(2010)
一篇2010年的反思性博客文章,探讨了开发者讨厌XML的原因,追溯了炒作和反弹的周期,并讨论了使用XML进行数据建模和互操作性所面临的挑战。