YAML:不只是另一种标记语言
一、它是什么
YAML 是一个递归缩写,全称 YAML Ain’t Markup Language(YAML 不是标记语言)。这个命名本身就是一份宣言——它宣布自己与 HTML、XML 那类用来”标记文档结构”的语言划清界限,而将自己定位为一种数据序列化语言。
通俗地说,它的工作是把程序内存里的数据(列表、字典、字符串、数字)变成一段可存储、可传输、更重要的是人可以直接看懂的文本,反过来也能把这段文本还原成程序数据。
YAML 诞生于 2001 年,最初的名字是 “Yet Another Markup Language”(另一种标记语言),后来改名为现在的版本,以更准确地表达它的本质。当前的标准是 2009 年发布的 YAML 1.2,在 2021 年进行了修订更新。
二、它好在哪里
YAML 的设计围绕一个核心目标:让人类轻松读写,同时让机器高效解析。这个目标衍生出了它最显著的特点。
第一,依赖缩进表达层级。它不使用成对的开闭标签或花括号,而是像写文章大纲一样,用缩进来表示包含关系。这让它看起来干净、直观,一眼就能看出结构。
第二,语法简洁,冗余度低。字符串通常不需要加引号,列表用短横线加空格即可,字典用冒号加空格即可。同样的数据,YAML 写出来的字符数往往远少于 JSON 或 XML。
第三,原生支持注释。这是 YAML 对比 JSON 的一个决定性优势。你可以在配置文件里随处写下 # 注释,说明每一段配置的用途,这对维护和协作至关重要。
第四,具备高级复用能力。通过锚点(&)和引用(*),YAML 可以在文件内部定义一处数据,然后在多个位置复用,避免重复书写。这在大型配置中能显著减少冗余。
第五,它是 JSON 的超集。任何合法的 JSON 文件,都是合法的 YAML 文件。这意味着你可以在 YAML 中混用 JSON 语法,在需要的时候随时切换。
下图对比了 YAML 与另外两种常见格式的差异:
| 特性 | YAML | JSON | XML |
|---|---|---|---|
| 可读性 | 极高,像大纲 | 较高,但括号多 | 较低,标签多 |
| 语法简洁度 | 极简 | 中等 | 冗长 |
| 注释支持 | ✅ 原生 | ❌ 不支持 | ✅ 支持 |
| 数据复用(锚点/引用) | ✅ 支持 | ❌ 不支持 | ❌ 不支持 |
| 典型用途 | 配置文件、基础设施即代码 | API 数据传输 | 文档标记、复杂消息 |
| 解析速度 | 较慢(缩进解析开销) | 快 | 慢 |
三、语法全解
YAML 的语法可以拆解为四个层次:标量(单个值)、序列(列表)、映射(键值对)以及文档结构(文件组织)。这三种基本结构嵌套组合,就能表达任意复杂的数据。下面逐一详细拆解。
3.1 标量(Scalars)
标量是最基础的数据单元,不可再分。它涵盖了我们日常编程中使用的所有基本数据类型。
3.1.1 字符串
YAML 对字符串的处理极为灵活,这也是它最容易让人困惑的地方。它提供了三种书写方式:
无引号字符串(最常用):
name: 张伟
greeting: Hello World
大多数情况下,字符串不需要任何引号。但以下情况必须加引号:
- 字符串以
[、{、*、&、!、%、@、`等特殊字符开头 - 字符串包含冒号加空格(
:),如url: http://example.com会被误认为键值对 - 字符串是
yes、no、on、off、true、false、null等会被自动类型推断的值
单引号字符串:
literal: 'Hello\nWorld' # 输出:Hello\nWorld,\n 不会被转义
单引号内的所有内容原样输出,唯一的转义是连续两个单引号 '' 表示一个单引号字符。
双引号字符串:
escaped: "Hello\nWorld" # 输出:Hello(换行)World
双引号支持标准的 C 风格转义序列:\n(换行)、\t(制表符)、\r(回车)、\\(反斜杠)、\"(双引号)等。
多行字符串(这是重点):
YAML 提供了四种多行字符串模式,区别在于如何处理换行符:
# 1. 保留所有换行(竖线)
keep: |
第一行
第二行
第三行
# 解析结果:"第一行\n第二行\n第三行\n"(末尾保留一个换行)
# 2. 折叠换行为空格(大于号)
fold: >
这是一段很长的话
它会被合并成一行
但段落间可以空行分隔
# 解析结果:"这是一段很长的话 它会被合并成一行 但段落间可以空行分隔\n"
# 3. 去掉末尾换行(竖线减号)
strip: |-
第一行
第二行
# 解析结果:"第一行\n第二行"(末尾没有换行)
# 4. 保留并追加换行(竖线加号)
append: |+
第一行
# 解析结果:"第一行\n\n"(原有的换行 + 额外追加一个)
缩进控制:多行文本的缩进量以第一行非空内容的缩进为基准,额外的缩进会被保留:
code: |
def hello():
print("Hello")
# 解析结果:" def hello():\n print(\"Hello\")\n"
3.1.2 数字
YAML 支持多种数字格式:
integer: 100 # 十进制整数
negative: -50 # 负数
float: 3.14159 # 浮点数
scientific: 1.5e3 # 科学计数法(1500)
octal: 0o10 # 八进制(= 8),注意是 0o 前缀
hex: 0x1A # 十六进制(= 26)
infinity: .inf # 正无穷
negative_inf: -.inf # 负无穷
not_a_number: .nan # 非数字
重要陷阱:
- 不带
0o前缀的010,在 YAML 1.1 中被解析为八进制(十进制的 8),在 YAML 1.2 中解析为十进制 10。跨版本时需要小心。 1.0被解析为浮点数而不是整数。如果需要整数,用!!int 1显式标记。- 极长数字可能丢失精度,建议转为字符串:
"12345678901234567890"
3.1.3 布尔值
YAML 接受多种写法来表示真和假:
# 真值
enabled: true
on: on
yes: yes
active: Y
ok: OK
# 假值
disabled: false
off: off
no: no
inactive: N
not_ok: OFF
最常踩的坑:yes、no、on、off 会被自动转换为布尔值。如果你需要它们作为字符串,必须加引号:
answer: "no" # 字符串 "no",而不是布尔值 false
status: "on" # 字符串 "on",而不是布尔值 true
3.1.4 空值
两种写法等价:
empty: null
nothing: ~
也可以写成键存在但值为空:
blank: # 值直接留空,解析为 null
3.1.5 时间与日期
YAML 原生支持 ISO 8601 格式的日期和时间:
date: 2026-07-19
datetime: 2026-07-19T14:30:00+08:00
timestamp: 2026-07-19 14:30:00
解析器会自动将其转为对应语言的时间对象。如果不需要这种自动转换,加引号即可。
3.1.6 显式类型标签(!!)
当自动类型推断不符合预期时,可以用 !! 强制指定类型:
age: !!int "30" # 强制转整数
score: !!float "98" # 强制转浮点
flag: !!bool "yes" # 强制转布尔
binary: !!binary SGVsbG8gV29ybGQ= # Base64 解码为二进制
timestamp: !!timestamp 2026-07-19
常用类型标签:
!!int、!!float、!!bool、!!str、!!null!!map(映射)、!!seq(序列)!!binary(Base64 编码的二进制)!!timestamp(时间戳)
3.2 序列(Sequences)
序列即列表或数组,用短横线 - 加一个空格表示每个元素。
块格式(推荐):
fruits:
- 苹果
- 香蕉
- 橙子
行内格式(紧凑):
fruits: [苹果, 香蕉, 橙子]
嵌套序列:
matrix:
- [1, 2, 3]
- [4, 5, 6]
- [7, 8, 9]
序列中的复杂元素(每个元素可以是一个映射或其他结构):
employees:
- name: 张三
age: 30
department: 技术部
- name: 李四
age: 25
department: 产品部
3.3 映射(Mappings)
映射即键值对字典,用冒号 : 加一个空格分隔键和值。
块格式:
person:
name: 张伟
age: 28
address:
city: 深圳
district: 南山
行内格式:
person: {name: 张伟, age: 28, address: {city: 深圳, district: 南山}}
复杂键:键可以是任意数据类型,而非仅仅是字符串。这在某些场景(如数学运算或索引)中有用:
# 键为数字
1: 苹果
2: 香蕉
# 键为列表(用方括号显式标记)
[1, 2]: 坐标
流动序列中的键值对:
- {name: 张三, age: 30}
- {name: 李四, age: 25}
3.4 缩进的完整规则
缩进是 YAML 决定层级关系的唯一依据,理解它至关重要。
基本规则:
- 只能用空格,绝对不能用 Tab。YAML 规范明确禁止 Tab 作为缩进字符。
- 缩进量不限,2 个空格是社区惯例,4 个也完全可以。
- 同一层级的所有元素必须严格左对齐,差一个空格即报错。
正确示例:
parent:
child1: value1
child2:
grandchild: value2
child3: value3
错误示例:
parent:
child1: value1
child2: value2 # 错:缩进为 3 个空格,而 child1 是 2 个空格
缩进的处理细节:
- 序列的
-算作内容的一部分,缩进计从-后第一个非空字符开始算起 - 多行字符串的缩进以首行非空内容为准
- 空行不影响缩进层级
3.5 注释
用 # 表示注释,从 # 到行尾的内容都被解析器忽略。
# 这是整个文件的注释
database:
host: localhost # 行内注释
port: 3306 # 默认 MySQL 端口
# 注释不能写在键值对的值中间
# 错误:name: 张#三
3.6 锚点(&)与别名(*)
这是 YAML 最强大的特性之一,用于在文件内部复用数据。
基本用法:
# 定义一个锚点
common_settings: &common
timeout: 30
retries: 3
protocol: http
# 引用锚点
service_a:
<<: *common # 合并所有键
host: a.example.com
port: 8080
service_b:
<<: *common
host: b.example.com
port: 9090
timeout: 60 # 覆盖 common 中的 timeout
解析后的 service_a:
service_a:
timeout: 30
retries: 3
protocol: http
host: a.example.com
port: 8080
引用列表:
default_ports: &ports
- 80
- 443
web:
ports: *ports # 复制整个列表
引用映射中的部分键:
defaults: &defaults
host: localhost
port: 8080
debug: true
# 只引用部分键,而不是全部合并
custom:
<<: *defaults
port: 9090 # 覆盖
# 结果:{host: localhost, port: 9090, debug: true}
注意:<<:(合并键)并非 YAML 1.2 官方标准的一部分,而是大多数解析器(如 PyYAML、Go-yaml、SnakeYAML)的扩展。在纯标准模式下,应使用 * 引用整个映射而非合并。
3.7 多文档(—)
一个文件中可以包含多个独立的 YAML 文档,用 --- 分隔。每个文档解析为独立的根对象。
---
# 第一个文档
name: 张三
age: 30
---
# 第二个文档
name: 李四
age: 25
---
# 第三个文档
cities:
- 北京
- 上海
如果文件开头没有 ---,解析器会从第一行开始解析。--- 通常用于明确指示文档开始,在 Kubernetes 配置中极为常见。
可选的前言:在第一个 --- 之前可以写入非内容行(如编辑器配置),解析器会忽略:
# vim: set ts=2 sw=2:
---
apiVersion: v1
kind: Pod
3.8 结束标记(…)
可选地用 ... 标记文档结束,用于明确指示文档边界:
---
name: 张三
...
---
name: 李四
...
在只有一个文档时很少使用。
3.9 指令(Directives)
指令用于告诉解析器如何处理文档,以 % 开头:
%YAML 1.2
---
# 这个文档使用 YAML 1.2 标准
version: 1.0
%TAG 指令用于定义标签缩写,在大型项目中偶尔使用:
%TAG !m! !my-tags/
---
my_tag: !m!tag_value
四、解析器原理(选读)
理解 YAML 的解析过程有助于理解错误信息和性能特性。
YAML 解析器的工作分为三个阶段:
1. 词法分析(Lexer):将字符流切分成 Token。Token 包括:键名、冒号、短横线、缩进空格数、引号、各种字面量等。
2. 语法分析(Parser):根据缩进栈和 Token 序列构建抽象语法树(AST)。核心算法是维护一个缩进级别的栈:
- 遇到新行,计算该行的缩进空格数
- 若缩进 > 栈顶:入栈,开始新的层级
- 若缩进 = 栈顶:保持当前层级
- 若缩进 < 栈顶:出栈直到匹配
3. 类型推断(Composer):遍历 AST,将每个标量的内容按规则推断类型,同时应用 !! 显式类型标签,最终生成与编程语言对应的数据结构。
五、它的使用场景
YAML 在云原生和 DevOps 领域占据了统治地位。可以说,如果不了解 YAML,就几乎无法在现代云计算环境中开展工作。
- 容器编排:Kubernetes 的全部资源定义(Pod、Service、Deployment、Ingress、ConfigMap、Secret 等)都通过 YAML 文件描述。这是 YAML 最重要的应用场景。
- 容器应用编排:Docker Compose 使用 YAML 定义多容器应用的服务、网络和卷。
- 基础设施即代码:Ansible 的 Playbook 和 Terraform 的配置文件都采用 YAML 格式。
- CI/CD 流水线:GitLab CI(
.gitlab-ci.yml)、CircleCI、GitHub Actions、Jenkins(部分插件)等主流工具都以 YAML 定义构建、测试和部署流程。 - 应用程序配置:许多现代应用框架(如 Spring Boot 的
application.yml、Django 的配置扩展、Ruby on Rails)将其作为首选配置格式。
六、它的局限与注意事项
YAML 并非万能,它有明确的能力边界,理解这些边界能帮你在合适的时候做出正确选择。
性能问题:由于需要解析缩进来确定层级,YAML 的解析速度比 JSON 慢,内存消耗也更大。对于超长的列表(几十万行),YAML 解析器容易出现内存溢出,此时应换用 JSON 或其他流式格式。在 Kubernetes 中,超大 YAML 文件(如综合了数千个资源的大型 Manifest)也可能导致解析缓慢。
类型陷阱:自动类型推断虽然方便,但也埋下了不少隐患。yes/no 变为布尔值、012 在 YAML 1.1 中变为八进制、1.0 变为浮点数——这些情况如果不加注意,会导致难以追踪的 bug。最佳实践是:任何可能有歧义的值,都加上引号强制转为字符串。
安全风险:YAML 的某些解析器(如 PyYAML 的 yaml.load() 函数)支持反序列化任意 Python 对象,这可能被恶意构造的 YAML 文件利用来执行任意代码。在处理不可信来源的 YAML 数据时,必须使用解析器的安全加载函数(如 yaml.safe_load())。
跨库差异:不同语言、不同解析器对 YAML 高级特性的支持并不完全一致:
- 合并键
<<:Python PyYAML ✅ | Go-yaml ✅ | JavaScript js-yaml ✅ | Java SnakeYAML ✅(但需要配置) - 锚点全局性:有些库锚点仅限当前文档,有些跨文档
- 自定义标签:各库实现方式不同
如果你需要编写跨语言通用的 YAML,建议只使用基础语法(映射、序列、标量),避免依赖锚点、合并键等扩展特性。
空格敏感:缩进差一个空格即报错,这种严格性在编辑时容易出错。建议使用支持 YAML 语法高亮和缩进线显示的编辑器,并启用”显示不可见字符”功能。
七、什么时候用 YAML,什么时候不用
| 场景 | 推荐 | 理由 |
|---|---|---|
| 人类手写的配置文件(CI/CD、Docker Compose) | ✅ 用 YAML | 可读性最高,支持注释 |
| 机器间传输的数据(API 响应、微服务通信) | ❌ 用 JSON | 解析更快,无歧义,无安全风险 |
| 复杂嵌套且需要严格校验(如 OpenAPI 定义) | ⚠️ 两者皆可 | YAML 可读,但 JSON 更不易出错 |
| 超大配置文件(几十万行) | ❌ 避免 YAML | 缩进解析开销大,易内存溢出 |
| 需要在代码中动态生成配置 | ⚠️ 先生成 JSON 再转 YAML | 避免手动拼接字符串带来的格式错误 |
| 配置中包含大量注释和说明 | ✅ YAML | 注释是 YAML 的核心优势 |
| 需要配置复用(相同片段多处使用) | ✅ YAML | 锚点和引用能有效减少重复 |
八、总结
YAML 是一种以人为本的数据序列化语言。它用简洁的缩进替代了复杂的括号和标签,用注释和锚点提供了 JSON 所不具备的可维护性和复用能力,因此在现代云原生和配置管理领域成为了事实上的标准。
它的优势在于可读性和表达力,代价在于解析性能和类型明确性。理解这一点,就能在合适的场景发挥它的长处,同时避开它的陷阱。
掌握 YAML 并不难,它的核心语法可以用半天学完。难的是在实际项目中避开那些暗坑,而这份指南已经把最常踩的坑都标记出来了。写配置文件的时候,回来看一眼,能省下不少调试时间。
三条最实用的建议:
- 永远只用空格缩进——这是新手上路最常犯的错误,也是最容易查出来的错误。
- 任何有歧义的值都加上引号——避免自动类型推断带来的意外。尤其是
yes、no、on、off、true、false、null,以及以特殊字符开头的字符串。 - 永远用安全加载(safe_load)——处理任何外部输入的 YAML 时,不要用
load(),只用safe_load()。