为YAML辩护

Lobsters Hottest 新闻

摘要

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) 设计哲学很直接。对于绝大多数

相似文章

YAML?那是挪威问题

Hacker News Top

探讨了臭名昭著的 YAML '挪威问题',即国家代码 'NO' 被解析为布尔值 false,追溯其历史并提供如加引号等解决方案。