为YAML辩护
摘要
Posit的一篇博文为YAML辩护,反驳当前普遍认为TOML更优的共识,回顾了配置格式的历史,并指出YAML的规范与工具已经演进,解决了过去的批评。
<p><a href="https://lobste.rs/s/metnis/defense_yaml">评论</a></p>
查看缓存全文
缓存时间: 2026/06/08 03:18
# 为 YAML 辩护
来源:https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/
每个程序员对配置文件都有自己的看法。这些看法往往异常坚定,与事情本身的重要性成反比。过去几年,共识发生了变化:YAML 不好,TOML 好,热衷于 YAML 的用户可能只是孤陋寡闻。这篇博文持不同观点。我们打算从历史、规范以及 2026 年的工具生态出发,为 YAML 提出辩护。
长期以来,对 YAML 的批评都事出有因。这个格式确实因其真实存在的问题而引来批评,多年来那些令人意外的行为让哪怕最谨慎的用户也吃过亏。但规范进化了,工具也终于跟上了。要理解为什么当前的共识已经过时,我们需要追溯配置文件格式本身的谱系,因为这类争论以前就上演过。
## 配置文件格式简史
# (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#a-brief-history-of-configuration-formats)
INI 文件诞生于 20 世纪 80 年代早期,与 MS-DOS 和最初的 Windows 版本一同出现。1 (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#fn:1) 它是能工作的最简单形式:键值对,用方括号分组为节,用分号做注释。扁平的、可读的、人类可编辑的。对于那个时代的需求(比如配置设备驱动、指定字体路径、或设置应用程序偏好),它完全够用。它唯一真正的局限是结构性的:你无法嵌套超过一层,而且没有正式规范,这意味着每个解析器都实现自己的方言。但二十年来,这都没问题。
```
[boot]
shell=COMMAND.COM
device=HIMEM.SYS
[display]
resolution=640x480
colors=256
```
然后 XML 来了。20 世纪 90 年代末,企业软件世界广泛采用了尖括号。XML 可以表示任意层次结构。它有模式、命名空间、变换。它能自描述。有一段时间,争论似乎已经平息。但实践中 XML 配置文件变得很大。任何在 2003 年维护过 Java `web.xml` 或 Ant 构建文件的人都知道,仅仅为了修改一个数据库连接字符串,就要编辑数十个嵌套元素。这种冗长使得手工维护文件变得困难,而这正是配置文件所要求的。
```xml
<servlet>
<servlet-name>myServlet</servlet-name>
<servlet-class>com.example.MyServlet</servlet-class>
<init-param>
<param-name>database.url</param-name>
<param-value>jdbc:postgresql://localhost/mydb</param-value>
</init-param>
</servlet>
```
JSON 作为轻量级应对出现。道格拉斯·克罗克福特声称他是“发现”而非“发明”了这个格式2 (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#fn:2),它提供了 JavaScript 对象字面量的简单性:花括号、方括号、带引号的字符串,以及少量类型。JSON 在 2000 年代末和 2010 年代初取代了 XML 在 Web API 中的地位。但人们开始将其用于配置(而非机器间数据交换)时,其局限性变得明显。JSON 没有注释。没有多行字符串。尾随逗号是非法的。这些约束对于序列化格式是合理的,但对于人类必须编写和维护的文件来说,JSON 令人痛苦。根据克罗克福特本人的说法,从 JSON 规范中移除注释,是因为人们滥用注释来做解析指令。3 (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#fn:3)对于数据交换来说,这是正确的决定,但它留下了一个缺口。
YAML(2001)和 TOML(2013)各自出现填补了这个缺口,并且两者都明确将自己定位为对前者的反叛。YAML 提供了序列化语言的完整表现力(包括任意嵌套、多文档、引用和自定义类型),语法基于缩进而非括号。TOML 由 Tom Preston-Werner 在十二年后创建,是对 YAML 复杂性的反应:它旨在成为一种“标准化的 INI”,具有显式类型、明确的语义和正式规范。4 (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#fn:4)这个模式每代重复:前一个格式的过度之处成为新格式创建的理由。有趣的是,当前时刻,YAML 的问题并非其设计固有的。它们是特定规范版本和基于该版本的解析器的产物。
## 对 YAML 的批评(就其当时而言)
# (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#the-case-against-yaml-as-it-was)
对 YAML 的批评并非捏造。它们反映了真实程序员多年来真实的体验。最臭名昭著的问题是“挪威事件”,它已成为 YAML 隐式类型转换行为的代名词。在 YAML 1.1 中,裸标量 `NO` 被解释为布尔值 `false`。这意味着一个国家代码列表会悄无声息地将挪威变成谬误:
```
# 你写的内容:
countries:
- dk
- fi
- is
- no
- se
# YAML 1.1 解析后的结果:
["dk", "fi", "is", false, "se"]
```
同样的规则也适用于 `yes`、`on`、`off`、`y`、`n` 及其各种大小写形式。Ruud van Asseldonk 广为流传的“来自地狱的 YAML 文档”5 (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#fn:5) 列举了这些及其他问题:端口映射如 `22:22` 被解析为六十进制整数;版本号如 `10.23` 被解析为浮点数而非字符串;类似日期的值被解析为时间戳6 (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#fn:6);以 `!` 开头的标签可能在某些解析器中触发任意代码执行。
这不仅仅是一个国家代码问题。在数据科学和机器学习代码中,`n` 和 `y` 是常见的变量名:
```yaml
variables:
x: features
n: sample_size
y: target
```
在 YAML 1.1 的隐式布尔规则下,解析器可能将这些键解析为布尔值而非字符串:
```json
{"variables": {"x": "features", False: "sample_size", True: "target"}}
```
这些并非只有粗心者才会遇到的边缘情况。它们源于 YAML 1.1 规范设计哲学中的激进隐式类型转换,即解析器试图通过猜测未加引号的值类型来“提供帮助”。其初衷是可读性(你可以写 `true` 而不加引号,得到布尔值),但实际结果却是不可预测的行为。配置文件恰恰是最不能容忍意外行为的领域。它们很少被编辑,通常由非原文件作者的人操作,一次静默的解析错误可能在系统中潜伏数月而未被发现。
完整规范的复杂性加剧了问题。YAML 1.2.2 规范有十章,节编号深达四级。7 (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#fn:7) 表达多行字符串的方式有几十种。锚点和别名创建了一个引用系统,虽然强大,但概念负担远超大多数配置任务所需。而基于标签的对象反序列化的安全影响(Python 中的 `yaml.load()` 漏洞)已成为众所周知的攻击向量。8 (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#fn:8)所有这些批评都是合理的,而且它们尤其针对 YAML 1.1 及其构建的工具生态。
## TOML 做对了什么
# (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#what-toml-gets-right)
TOML 值得赞扬。对于扁平或浅层的配置结构,它简洁、可读且无歧义。任何见过 INI 文件的人都会熟悉其语法,但它增加了显式类型、正式规范以及对点分隔键嵌套表的支持。考虑一个 `pyproject.toml` 或 `Cargo.toml`。这些文件通常只有一两层深度,具有定义明确的节和可预测的内容。TOML 处理得非常好。字符串总是带引号,因此 `no` 是布尔值还是单词“no”没有歧义。整数就是整数,浮点数就是浮点数,日期是一等类型。注释工作得完全符合预期。对于这类问题,TOML 表现良好,其在 Python 打包生态系统(PEP 518)9 (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#fn:9) 和 Rust 社区(Cargo)中的采用是合理的。
TOML 还受益于实现的简单性。规范足够简短,一个有能力的程序员可以用一个周末就能编写一个合规的解析器。这意味着解析器生态庞大、测试充分且一致。没有 YAML 1.1/1.2 版本分裂的情况。TOML 1.0 无处不在,都是一致的。这些都是真正的优势。
## TOML 的薄弱之处
# (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#where-toml-strains)
当配置需要表达深度时,问题就开始了。TOML 处理嵌套结构依赖点分隔的节头(`[servers.alpha]`)或表数组(`[[products]]`),随着嵌套增加,这两种方式都变得难以阅读。这不是理论上的担忧:正是这个原因导致 PyTOML 解析器的作者 Martin Vejnár 最终放弃了自己的项目。当被问及他的库是否应该成为 pip 的依赖时,他拒绝了并解释说:“TOML 是一个糟糕的文件格式。第一眼看上去还行,对于真正非常琐碎的事情可能也不错。但一旦我开始使用它,并且配置模式变得更复杂,我发现语法丑陋且难以阅读。”10 (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#fn:10)
考虑一个中等复杂的配置。在 YAML 中,缩进一目了然地传达了层次结构:
```yaml
services:
web:
image: nginx:latest
environment:
DB_HOST: postgres
DB_PORT: 5432
resources:
limits:
memory: 512M
cpu: "0.5"
```
TOML 中的等价写法需要在每个节头中重复完整路径:
```toml
[services.web]
image = "nginx:latest"
[services.web.environment]
DB_HOST = "postgres"
DB_PORT = 5432
[services.web.resources.limits]
memory = "512M"
cpu = "0.5"
```
读者必须从一长串限定名称中在脑海中重建树形结构。StrictYAML 文档具体测量了这一点:等价的 TOML 文件表示相同数据大约要多用 50% 的字符,主要原因是重复的路径前缀。11 (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#fn:11)
还有意义性缩进本身的问题。Python 几十年前就证明了缩进作为结构不是弱点而是优势:它消除了视觉结构与语法结构不一致的那类错误。YAML 继承了这一特性。TOML 不要求缩进(尽管许多作者自愿添加缩进作为非解析的视觉辅助),这意味着键与其所属表之间的关系仅存在于节头中,而非文件的物理布局中。对于深度嵌套的配置,这使得 TOML 文件更难扫描,也更难放心编辑。
## YAML 1.2 改变了什么
# (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#what-yaml-12-changed)
YAML 1.2 规范于 2009 年发布,明确的修订版(1.2.2)于 2021 年 10 月完成。12 (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#fn:12) 它的变化直接针对上述批评。造成挪威问题的隐式类型转换已经消失。在 YAML 1.2 核心模式中,只有 `true` 和 `false`(及其大小写形式 `True`、`False`、`TRUE`、`FALSE`)被识别为布尔值。单词 `yes`、`no`、`on`、`off`、`y` 和 `n` 是普通字符串。六十进制数字字面量(`22:22` 问题)被完全移除。时间戳不再是核心类型,因此未加引号的 `2026-05-05` 在核心模式下是字符串,而非自动检测的日期。JSON 现在严格地是 YAML 1.2 的正确子集,这意味着任何有效的 JSON 文档作为 YAML 都能被解析相同。标签解析规则被收紧和明确。规范本身虽然仍然庞大,但写得更清晰,并在 GitHub 上开放维护。
简而言之,人们抱怨的 YAML 是 YAML 1.1。如今实际管理语言的规范是一个不同的、更安全、更可预测的文档。问题在于大多数人体验 YAML 并非通过规范,而是通过他们的解析器,而对大多数 Python 用户来说,那个解析器是 PyYAML,它实现了 YAML 1.1,并且自 2006 年以来核心语义没有改变。
## Python 的 YAML 解析器生态
# (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#the-python-yaml-parser-landscape)
PyYAML (https://github.com/yaml/pyyaml),由 Kirill Simonov 于 2006 年编写,是 Python 的事实标准 YAML 库。它包装了 LibYAML(一个 C 库)以追求性能,并提供了纯 Python 回退。它每周被下载数百万次,是无数包的依赖,并且它实现了 YAML 1.1。这最后一点是 Python 生态中大多数 YAML 抱怨的根源。当有人说“YAML 把我的国家代码解析成了布尔值”时,他们描述的是 PyYAML 的行为,而不是 YAML 的规范。
PyYAML 的 GitHub 仓库显示有超过 200 个打开的问题和 100 个打开的拉取请求。13 (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#fn:13) 该项目有人维护但进展缓慢,并且尚未实现向 YAML 1.2 语义的主版本升级。
`ruamel.yaml` (https://github.com/ruamel/yaml) 库,由 Anthon van der Neut 维护,提供了 YAML 1.2 支持,具有往返保留注释、流样式和键顺序的能力。14 (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#fn:14) 它被广泛使用,在需要注释保留或格式感知编辑的任务上比 PyYAML 强大得多。然而,它在其默认的往返模式下主要是纯 Python 实现,这使得它比 PyYAML 的 C 后端快路径慢得多。它的打包历史也较为复杂:命名空间包问题和依赖链有时会混淆部署管道。
StrictYAML (https://github.com/crdoconnor/strictyaml) 采用了一种完全不同的方法,实现了 YAML 的一个特意子集,移除了所有隐式类型转换、标签、锚点和流样式。15 (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#fn:15) 从哲学上讲,它更接近 TOML 而非完整 YAML:一种安全、简单的格式,恰好使用 YAML 的缩进语法。它是纯 Python 的,在其他语言中没有实现,也不追求规范合规性。
这个生态一直缺少的是一个快速、完全符合 1.2 规范、并且简单到可以作为 PyYAML 基本接口的替代品的库。
## 介绍 py-yaml12
# (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#introducing-py-yaml12)
`py-yaml12` (https://github.com/posit-dev/py-yaml12) 库是一个用于 Python 的 YAML 1.2 解析器和格式化器,用 Rust 实现以保证速度和正确性。它基于 `saphyr` crate16 (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#fn:16)(一个 Rust YAML 库),并暴露一个最小聚焦的 API:用于加载的 `parse_yaml()` 和 `read_yaml()`,以及用于序列化的 `format_yaml()` 和 `write_yaml()`。
### 简单
# (https://opensource.posit.co/blog/2026-05-21_in-defense-of-yaml/#simple)
设计哲学很直接。对于绝大多数
相似文章
在 Rust 中“尊重原格式”地修补 YAML
本文评估了多款 Rust 库,旨在找出能够在编程修改 YAML 文件时保留原始格式与注释的最佳工具。
YAML?那是挪威问题
探讨了臭名昭著的 YAML '挪威问题',即国家代码 'NO' 被解析为布尔值 false,追溯其历史并提供如加引号等解决方案。
@jarredsumner:在 bun 的 Rust 重写过程中我最喜欢的测试失败:TOML 和 YAML 解析器的栈溢出测试失败了,因为它现在可以比测试预期更深…
Jarred Sumner 分享了 Bun 的 Rust 重写过程中一个最喜欢的测试失败:TOML 和 YAML 解析器的栈溢出测试失败了,因为 Rust 实现能够处理比预期更深层次的嵌套。
新推出的面向代理的Markdown对象语言MOL
MOL(Markdown对象语言)是一种新的正式规范,用于解析基于markdown的配置和数据文件,其设计比JSON更适合人类和LLM使用。
10,000 Lines Later: When a Tool Became a Compiler - Rob Durst - Gleam Gathering 2026
Rob Durst 在 Gleam Gathering 2026 上分享了如何用 Gleam 将 YAML-to-Terraform 的配置工具重写为编译器,并从中体会到类型驱动设计和解码器模式的力量。