跳至正文
老丹的足迹 —— 代码写给机器,游记写给自己,感悟写给时间
老丹的足迹 老丹的足迹
老丹的足迹 老丹的足迹
  • 首页
  • 示例页面
  • 首页
  • 示例页面
老丹的足迹 老丹的足迹
老丹的足迹 老丹的足迹
  • 首页
  • 示例页面
  • 首页
  • 示例页面

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 与另外两种常见格式的差异:

特性YAMLJSONXML
可读性极高,像大纲较高,但括号多较低,标签多
语法简洁度极简中等冗长
注释支持✅ 原生❌ 不支持✅ 支持
数据复用(锚点/引用)✅ 支持❌ 不支持❌ 不支持
典型用途配置文件、基础设施即代码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 决定层级关系的唯一依据,理解它至关重要。

基本规则:

  1. 只能用空格,绝对不能用 Tab。YAML 规范明确禁止 Tab 作为缩进字符。
  2. 缩进量不限,2 个空格是社区惯例,4 个也完全可以。
  3. 同一层级的所有元素必须严格左对齐,差一个空格即报错。

正确示例:

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 并不难,它的核心语法可以用半天学完。难的是在实际项目中避开那些暗坑,而这份指南已经把最常踩的坑都标记出来了。写配置文件的时候,回来看一眼,能省下不少调试时间。

三条最实用的建议:

  1. 永远只用空格缩进——这是新手上路最常犯的错误,也是最容易查出来的错误。
  2. 任何有歧义的值都加上引号——避免自动类型推断带来的意外。尤其是 yes、no、on、off、true、false、null,以及以特殊字符开头的字符串。
  3. 永远用安全加载(safe_load)——处理任何外部输入的 YAML 时,不要用 load(),只用 safe_load()。
作者

老丹

关注我
其他文章
上一个

Netlink 深度剖析:从内核实现到应用开发的完整图景

下一个

从地图到导航:深入理解Linux路由系统

关于博主

    老丹是一名C/C++后台开发工程师,信奉“无抽象不设计,无性能不生产”。

  • 技术栈:Modern C++、Linux环境编程、多线程/并发、网络编程等。
  • 信条:能用constexpr解决的问题绝不拖到运行时,能靠RAII避免的泄漏绝不写析构。
  • 正在填坑:从解封装到渲染的C++全链路实现,正在驯服FFmpeg与H.264/H.265。
  • 输出原则:这里的每一段代码都经过-Wall -Wextra -Werror -O2的洗礼。

近期文章

  • Linux系统的安全基石:深入理解可插拔认证模块(PAM) 2026年7月27日
  • vsftpd 完全指南:从核心原理到Docker容器化部署 2026年7月27日
  • 互联网的”导航”安全卫士:深入解读DNSSEC 2026年7月27日
  • Ubuntu DNS 配置完全指南 2026年7月27日
  • 在 Ubuntu 中使用 Certbot 的操作指南 2026年7月27日

文章分类

  • C/C++开发 (13)
  • Docker容器 (3)
  • Linux工具包 (10)
  • Linux服务配置 (33)
  • Linux系统 (10)
  • OpenWrt路由 (2)
  • Shell脚本 (3)
  • 安防技术 (4)
  • 数据安全 (30)
  • 网络协议 (17)
  • 计算机理论 (22)
联系我们:📍 地址:中国·广东省深圳市   |   ✉️ 邮箱:support@tanglinux.com   |   💬 QQ:870866607
版权所有:老丹的足迹粤ICP备2026061170号-1       公安备案图标 粤公网安备44030002013274号