数据序列化导读(2)[YAML]
上一节讲了JSON, 这一节将介绍YAML。可以认为,YAML是JSON的超集,但是更加简单易用,适合人类阅读和书写。
1. 什么是YAML?
YAML是YAML Ain't Markup Language的递归缩写。Clark Evans在2001年率先提出了YAML,Ingy döt Net和Oren Oren Ben-Kiki参与了YAML的设计。最初,YAML被称为Yet Another Markup Language(另一种标签语言),但它后来又被改写为YAML Ain't Markup Language(YAML不是一种标签语言)的递归缩写,旨在强调YAML面向的是数据而非文档标签。
Wikipedia对YAML的解释:
- YAML (YAML Ain't Markup Language) is a human-readable data serialization language. It is commonly used for configuration files, but could be used in many applications where data is being stored (e.g. debugging output) or transmitted (e.g. document headers). YAML targets many of the same communications applications as XML but has a minimal syntax which intentionally breaks compatibility with SGML. It uses both Python-style indentation to indicate nesting, and a more compact format that uses [] for lists and {} for maps making YAML 1.2 a superset of JSON. YAML是一种适合人类阅读的数据序列化语言。通常用于写配置文件,但也可以用于存储数据(如调试输出)或传输数据(如文档头)。YAML的设计目标是适用于使用XML的通信应用,但它提供最小的语法,于是故意破坏了与SGML(标准广义标签语言)的兼容性。YAML既使用Python样式的缩进来表示嵌套,又使用更紧凑的格式--使用[]表示列表(即数组)和{}表示映射(即字典),这些使得YAML1.2成为了JSON的超集。
- Custom data types are allowed, but YAML natively encodes scalars (such as strings, integers, and floats), lists, and associative arrays (also known as hashes, maps, or dictionaries). These data types are based on the Perl programming language, though all commonly used high-level programming languages share very similar concepts. The colon-centered syntax, used to express key-value pairs, is inspired by electronic mail headers as defined in RFC 0822, and the document separator "---" is borrowed from MIME (RFC 2045)[not in citation given]. Escape sequences are reused from C, and whitespace wrapping for multi-line strings is inspired from HTML. Lists and hashes can contain nested lists and hashes, forming a tree structure; arbitrary graphs can be represented using YAML aliases (similar to XML in SOAP). YAML is intended to be read and written in streams, a feature inspired by SAX. YAML允许自定义数据类型,但是YAML原生态地支持标量(scalar)(例如:字符串,整数和浮点数),列表(list)和关联数组(associative array, 也叫做哈希表,映射或者字典)。这些数据类型基于Perl语言,虽然几乎所有的高级编程语言都有类似的概念。用于表示键/值对的冒号(:)语法起源于RFC0822中定义的电子邮件头。文档分割符(---)则源于MIME(RFC2045)[不是在引用中给出的]。转义字符序列则重用了C语言。多行字符串的空白包装是从HTML中激发出来的。列表(list)和哈希(hash)可以嵌套包含列表和哈希,形成树形结构。任意图可以用YAML别名表示(类似于SOAP中的XML)。YAML的读写采用流(stream)模式,其灵感来自于SAX。
2. YAML的基本语法规则
- 大小写敏感;
- 使用缩进代表层级关系;
- 缩进不能使用TAB,只能使用空格;
- 缩进对空格个数没有要求,只要相同层级的元素左侧对齐即可。(一般使用2个或4个空格, 跟Python风格类似,本文后面的例子默认使用4个空格);
- 注释使用'#'。(类似Shell和Python,注意JSON不支持注释)
3. YAML的三种数据结构
- 映射(map), 即字典(dict)
- 列表(list), 即数组(array)
- 纯量(scalar), 即最基本的、不可再分的值
注意:YAML跟JSON类似,其骨架也是基于字典(dict)和数组(array)构建出来的。而字典的"键/值对"的值的类型和数组元素的类型可以是以上三种数据结构。
3.1 映射(map)
映射(map), 即字典(dict),使用冒号结构,格式为key: value,冒号后面要加一个空格。例如:
- 使用缩进表示层级关系
foo: foo1: abc foo2: 0xFF foo3: true
- 还可以使用流式(flow)语法,即将所有键/值对写成一个行内对象,于是上面的例子可以改写为:
foo: {foo1: abc, foo2: 0xFF, foo3: true}
上面的例子转换成Python对象表示就是:
{'foo': {'foo1': 'abc', 'foo2': 0xFF, 'foo3': True}}
3.2 列表(list)
列表(list)即数组(array), 使用一个短横线加一个空格代表一个数组元素。例如:
- C - Python - Go
上面的例子转换成Python对象表示就是:
['C', 'Python', 'Go']
上面的数组包含了三个元素,每个元素是一个字符串。
也有这样的写法:
- - C - Python - Go
上面的例子转换成Python对象表示就是:
[['C', 'Python', 'Go']]
上面的数组只有一个元素,该元素是一个包含了三个元素的数组。
数组的元素可以是数组,也可以是字典;当然,对字典来说,每一个key的值既可以是纯量,也可以是字典,还可以是数组。例如:
Students: - name: John age: 23 city: Agra - name: Steve age: 28 city: Delhi
上面的例子转换成Python对象表示就是:
{'Students': [{'city': 'Agra', 'age': 23, 'name': 'John'}, {'city': 'Delhi', 'age': 28, 'name': 'Steve'}]}
显然,键'Students'的值是一个具有两个元素的数组,该数组的每一个元素都是一个字典。
3.3 纯量(scalar)
- 数值(整数和浮点数)
- 字符串
- 布尔值
- 空值
- 时间和日期
3.3.1 数值(number)
数值(number)包括整数(int)和浮点数(float)。 例如:
a: 10 b: 10.9
上面的例子转换成Python对象表示就是:
{'a': 10, 'b': 10.9}
3.3.2 字符串(string)
- 在YAML中,字符串默认不使用引号表示。 例如:
- Hello Beijing - Hello China - Hello World
上面的例子转换成Python对象表示就是:
[
'Hello Beijing',
'Hello China',
'Hello World'
]
- 如果字符串之中包含特殊字符,需要放在引号之中。 使用单引号和双引号都可以使用,只是双引号不会对特殊字符转义。 例如:
- "Hello\tChina" - 'Hello\tWorld'
上面的例子转换成Python对象表示就是:
[
'Hello\tChina',
'Hello\\tWorld'
]
- 如果单引号之中还有单引号,必须连续使用两个单引号转义。 例如:
- "I'm good" - 'I''m good'
上面的例子转换成Python对象表示就是:
[
"I'm good",
"I'm good"
]
- 字符串可以写成多行,从第二行开始,必须至少有一个单空格缩进。注意换行符会被转成空格。 例如:
foo: There is a napping house, where everyone is sleeping.
上面的例子转换成Python对象表示就是:
{'foo': 'There is a napping house, where everyone is sleeping.'}
- 多行字符串可以使用 | 保留换行符,也可以使用 > 删除换行符(即折叠换行)。 例如:
1 foo1: | 2 Be good, 3 do right. 4 foo2: > 5 Be good, 6 do right.
上面的例子转换成Python对象表示就是:
{'foo1': 'Be good,\ndo right.\n', 'foo2': 'Be good, do right.\n'}
- + 表示保留文字块末尾的换行, - 表示删除字符串末尾的换行。 例如:
1 foo1: | 2 abc 3 4 5 foo2: |+ 6 abc 7 8 9 foo3: |- 10 abc 11 12 13 #END#
上面的例子转换成Python对象表示就是:
1 { 2 'foo1': 'abc\n', 3 'foo2': 'abc\n\n\n', 4 'foo3': 'abc' 5 }
- 字符串之中还可以插入 HTML 标记。 例如:
1 foo: >- 2 <p> 3 Hello World, <br> 4 Hello China. <br> 5 </p>
上面的例子转换成Python对象表示就是:
{'foo': '<p> Hello World, <br> Hello China. <br> </p>'}
3.3.3 布尔值(boolean)
在YAML中,布尔值通常用true/false表示,也支持True/False, Yes/No, On/Off之类的表示方法。 例如:
1 T: [true, True, yes, Yes, on, On] 2 F: [false, False, no, No, off, Off]
上面的例子转换成Python对象表示就是:
1 { 2 'T': [True, True, True, True, True, True], 3 'F': [False, False, False, False, False, False] 4 }
对true/false, yes/no, on/off这些成对的表示boolean的反义词来说,第一个字母是可以大写的。
3.3.4 空值(null)
在YAML中,空值(null)通常用~表示。 例如:
1 a: ~ 2 b: null 3 c: Null 4 d: NULL
上面的例子转换成Python对象表示就是:
1 { 2 'a': None, 3 'c': None, 4 'b': None, 5 'd': None 6 }
3.3.5 时间和日期
- 日期必须使用ISO 8601格式,即YYYY-MM-DD;
- 时间使用ISO 8601格式,时间和日期之间使用T连接,最后使用+代表时区。
例如:
1 date: 1983-05-24 2 datetime: 1983-05-24T15:02:31+08:00
上面的例子转换成Python对象表示就是:
1 { 2 'date': datetime.date(1983, 5, 24), 3 'datetime': datetime.datetime(1983, 5, 24, 7, 2, 31) 4 }
4. 常用的特殊符号
4.1 文件开始符(---)和结束符(...)
多个YAML可以合并到同一个文件中,使用---表示一个文件的开始; ... 和 --- 配合使用,代表一个文件的结束。这类似与电子邮件的附件组织形式。 例如:
1 --- 2 name: foo1.mp3 3 size: 20480 4 ... 5 6 --- 7 name: foo2.mp3 8 size: 10240 9 ...
4.2 强制类型转换符(!!)
使用!!<type> <value>可以对value进行强制类型转换,跟C语言类似。 例如:
1 foo: 2 - !!str 123.1 3 - 123.1 4 - !!int '0xff' 5 - '0xff'
上面的例子转换成Python对象表示就是:
{'foo': ['123.1', 123.1, 255, '0xff']}
4.3 字符串删除换行符(>)和保留换行符(|)
符号(>)和(|)在前面介绍字符串的时候已经讲过了,它们在YAML字符串里可谓使用非常频繁。这里再举个例子以加深印象:
1 foo1: > 2 Beautiful is 3 better than ugly. 4 5 foo2: | 6 Explicit is 7 better than implicit.
上面的例子转换成Python对象表示就是:
1 { 2 'foo1': 'Beautiful is better than ugly.\n', 3 'foo2': 'Explicit is\nbetter than implicit.\n' 4 }
注意: > 和 | 都可以后面跟 + 或 - , 具体含义请参见3.3.2字符串。 这里给出一个完整的例子对照 >, >+, >- 和 |, |+, |- 的区别:
1 foo10: > 2 (> ) Beautiful is 3 better than ugly. 4 5 6 #1.0 /* end of demo > */ 7 8 foo11: >+ 9 (>+) Beautiful is 10 better than ugly. 11 12 13 #1.1 /* end of demo >+ */ 14 15 foo12: >- 16 (>-) Beautiful is 17 better than ugly. 18 19 20 #1.2 /* end of demo >- */ 21 22 foo20: | 23 (| ) Explicit is 24 better than implicit. 25 26 27 #2.0 /* end of demo | */ 28 29 foo21: |+ 30 (|+) Explicit is 31 better than implicit. 32 33 34 #2.1 /* end of demo |+ */ 35 36 foo22: |- 37 (|-) Explicit is 38 better than implicit. 39 40 41 #2.2 /* end of demo |- */
上面的例子转换成Python对象表示就是:
1 { 2 'foo10': '(> ) Beautiful is better than ugly.\n', 3 'foo11': '(>+) Beautiful is better than ugly.\n\n\n', 4 'foo12': '(>-) Beautiful is better than ugly.', 5 'foo20': '(| ) Explicit is\nbetter than implicit.\n', 6 'foo21': '(|+) Explicit is\nbetter than implicit.\n\n\n', 7 'foo22': '(|-) Explicit is\nbetter than implicit.' 8 }
由此可见, >- 能删除所有的换行符, |+ 能保留所有的换行符。
4.4 锚点定义(&),锚点引用(*)和内容合并(<<)
4.4.1 锚点定义(&)和锚点引用(*)
重复的内容可以使用 & 来完成锚点定义,使用 * 来完成锚点引用。 例如:
1 employees: 2 - {employee: Jack Li, manager: &SS Sara Song} 3 - {employee: John Wu, manager: *SS} 4 - {employee: Ann Liu, manager: *SS}
上面的例子转换成Python对象表示就是:
1 { 2 'employees': [ 3 {'employee': 'Jack Li', 'manager': 'Sara Song'}, 4 {'employee': 'John Wu', 'manager': 'Sara Song'}, 5 {'employee': 'Ann Liu', 'manager': 'Sara Song'} 6 ] 7 }
还可以单起一行定义锚点,例如:
1 manager: &SS Sara Song 2 employees: 3 - {employee: Jack Li, manager: *SS} 4 - {employee: John Wu, manager: *SS} 5 - {employee: Ann Liu, manager: *SS}
于是, 上面的例子转换成Python对象表示就是:
1 { 2 'manager': 'Sara Song', 3 'employees': [ 4 {'employee': 'Jack Li', 'manager': 'Sara Song'}, 5 {'employee': 'John Wu', 'manager': 'Sara Song'}, 6 {'employee': 'Ann Liu', 'manager': 'Sara Song'} 7 ] 8 }
当然,锚点还支持复杂的数据结构,例如:
1 manager: &SS 2 name: Sara Song 3 gender: Female 45 employees: 6 - {employee: Jack Li, manager: *SS} 7 - {employee: John Wu, manager: *SS} 8 - {employee: Ann Liu, manager: *SS}
看起来*SS类似C语言的指针引用。于是, 上面的例子转换成Python对象表示就是:
1 { 2 'manager': {'name': 'Sara Song', 'gender': 'Female'}, 3 'employees': [ 4 { 5 'employee': 'Jack Li', 6 'manager': {'name': 'Sara Song', 'gender': 'Female'} 7 }, 8 { 9 'employee': 'John Wu', 10 'manager': {'name': 'Sara Song', 'gender': 'Female'} 11 }, 12 { 'employee': 'Ann Liu', 13 'manager': {'name': 'Sara Song', 'gender': 'Female'} 14 } 15 ] 16 }
4.4.2 锚点定义(&)和内容合并(<<)
& 用来建立锚点,* 用来引用锚点, << 则表示将锚点数据合并到当前数据(就像C语言的宏一样就地展开)。 例如:
1 defaults: &defaults 2 adapter: mlx5 3 host: sunflower 4 5 dev: 6 hca_name: hermon0 7 <<: *defaults 8 9 test: 10 hca_name: hermon1 11 <<: *defaults
上面的例子等价于:
1 defaults: &defaults 2 adapter: mlx5 3 host: sunflower 4 5 dev: 6 hca_name: hermon0 7 adapter: mlx5 8 host: sunflower 9 10 test: 11 hca_name: hermon1 12 adapter: mlx5 13 host: sunflower
可以将 << 理解类似于Bash里的文档读入符号。 例如:
1 #!/bin/bash 2 cat << EOF 3 Hello China! 4 Hello World! 5 EOF
5. 在Python中使用YAML
要在Python中使用YAML, 需要首先安装PyYAML包。
$ sudo pip install pyyaml
5.1 将YAML文件load为Python对象
- foo_load.py
1 #!/usr/bin/python 2 3 """ Deserialize YAML text to a Python Object by using yaml.load() """ 4 5 import sys 6 import yaml 7 8 9 def main(argc, argv): 10 if argc != 2: 11 sys.stderr.write("Usage: %s <yaml file>\n" % argv[0]) 12 return 1 13 14 yaml_file = argv[1] 15 with open(yaml_file, 'r') as f: 16 txt = ''.join(f.readlines()) 17 obj = yaml.load(txt) 18 print type(obj) 19 print obj 20 21 return 0 22 23 24 if __name__ == '__main__': 25 argv = sys.argv 26 argc = len(argv) 27 sys.exit(main(argc, argv))
- Run foo_load.py
$ cat -n list.yaml 1 - name: Jack 2 - name: John 3 - name: Annie $ ./foo_load.py list.yaml <type 'list'> [{'name': 'Jack'}, {'name': 'John'}, {'name': 'Annie'}]
5.2 将Python对象dump为YAML文件
- foo_dump.py
1 #!/usr/bin/python 2 3 """ Serialize a Python Object by using yaml.dump() """ 4 5 import sys 6 import yaml 7 8 obj = { 9 "students": 10 [ 11 { 12 "name": "John", 13 "age": 23, 14 "city": "Agra", 15 "married": False, 16 "spouse": None 17 }, 18 { 19 "name": "Steve", 20 "age": 28, 21 "city": "Delhi", 22 "married": True, 23 "spouse": "Grace" 24 }, 25 { 26 "name": "Peter", 27 "age": 32, 28 "city": "Chennai", 29 "married": True, 30 "spouse": "Rachel" 31 } 32 ], 33 "teacher": { 34 "name": "John", 35 "age": 35, 36 "city": "Chennai", 37 "married": True, 38 "spouse": "Anna", 39 "childen": 40 [ 41 { 42 "name": "Jett", 43 "age": 8 44 }, 45 { 46 "name": "Lucy", 47 "age": 5 48 } 49 ] 50 } 51 } 52 53 54 def main(argc, argv): 55 if argc != 2: 56 sys.stderr.write("Usage: %s <yaml file to save obj>\n" % argv[0]) 57 return 1 58 59 with open(argv[1], 'a') as f: 60 txt = yaml.dump(obj) 61 print "DEBUG> " + str(type(obj)) 62 print "DEBUG> " + str(obj) 63 print "DEBUG> " + str(type(txt)) 64 print "DEBUG> " + txt 65 f.write(txt) 66 67 return 0 68 69 70 if __name__ == '__main__': 71 sys.exit(main(len(sys.argv), sys.argv))
- Run foo_dump.py
huanli$ rm -f /tmp/dict.yaml huanli$ ./foo_dump.py /tmp/dict.yaml DEBUG> <type 'dict'> DEBUG> {'students': [{'city': 'Agra', 'age': 23, 'married': False, 'name': 'John', 'spouse': None}, {'city': 'Delhi', 'age': 28, 'married': True, 'name': 'Steve', 'spouse': 'Grace'}, {'city': 'Chennai', 'age': 32, 'married': True, 'name': 'Peter', 'spouse': 'Rachel'}], 'teacher': {'city': 'Chennai', 'name': 'John', 'age': 35, 'married': True, 'childen': [{'age': 8, 'name': 'Jett'}, {'age': 5, 'name': 'Lucy'}], 'spouse': 'Anna'}} DEBUG> <type 'str'> DEBUG> students: - {age: 23, city: Agra, married: false, name: John, spouse: null} - {age: 28, city: Delhi, married: true, name: Steve, spouse: Grace} - {age: 32, city: Chennai, married: true, name: Peter, spouse: Rachel} teacher: age: 35 childen: - {age: 8, name: Jett} - {age: 5, name: Lucy} city: Chennai married: true name: John spouse: Anna huanli$ cat -n /tmp/dict.yaml 1 students: 2 - {age: 23, city: Agra, married: false, name: John, spouse: null} 3 - {age: 28, city: Delhi, married: true, name: Steve, spouse: Grace} 4 - {age: 32, city: Chennai, married: true, name: Peter, spouse: Rachel} 5 teacher: 6 age: 35 7 childen: 8 - {age: 8, name: Jett} 9 - {age: 5, name: Lucy} 10 city: Chennai 11 married: true 12 name: John 13 spouse: Anna
附录1: 常见问题
1. Is there an official extension for YAML files? YAML文件的官方扩展名是什么?
A: Please use ".yaml" when possible. 请尽可能地使用".yaml"。
2. Why does YAML forbid tabs? 为什么YAML禁止使用tab键?
A: Tabs have been outlawed since they are treated differently by different editors and tools. And since indentation is so critical to proper interpretation of YAML, this issue is just too tricky to even attempt. Indeed Guido van Rossum of Python has acknowledged that allowing TABs in Python source is a headache for many people and that were he to design Python again, he would forbid them. Tab键是被取缔使用的,因为不同的编辑器和不同的工具处理Tab键的方式是不一样的。缩进对于正确解释YAML来说至关重要,所以允许使用Tab键实在是太难了以至于无法尝试。事实上,Python之父(Guido van Rossum)也已经承认在Python源代码中允许使用Tab键对于很多人来说是一个头疼的问题,如果他再设计Python的话,他将禁止在Python源代码中使用Tab键进行缩进。
附录2:一个基于oyaml的脚本(yamlfmt.py),该脚本支持JSON和YAML的相互转换
1 #!/usr/bin/python3 2 3 """ 4 Convert YAML file to JSON file or convert JSON file to YAML file, also support 5 to load a YAML file and dump it out in case it looks ugly 6 7 Note we use oyaml which is a drop-in replacement for PyYAML which preserves 8 dict ordering. And you have to install PyYAML first, then have a try, e.g. 9 $ git clone https://github.com/wimglenn/oyaml.git /tmp/oyaml 10 $ export PYTHONPATH=/tmp/oyaml:$PYTHONPATH 11 12 """ 13 14 import sys 15 import getopt 16 import json 17 import collections 18 import oyaml as yaml 19 20 21 def to_json(txt, indent=4): 22 # XXX: yaml.load() support to load both JSON and YAML 23 obj = yaml.load(txt) 24 out = json.dumps(obj, indent=indent) 25 return out 26 27 28 def to_yaml(txt, indent=2): 29 # XXX: yaml.load() support to load both JSON and YAML 30 obj = yaml.load(txt) 31 out = yaml.dump(obj, default_flow_style=False, indent=indent) 32 return out.rstrip('\n') 33 34 35 def new_argv(argv0, rargv): 36 argv = [] 37 argv.append(argv0) 38 argv.extend(rargv) 39 return argv 40 41 42 def usage(argv0): 43 sys.stderr.write('Usage: %s [-t indent] [-o outfile] <subcmd> ' 44 '<yaml or json file>\n' % argv0) 45 sys.stderr.write('subcmd:\n') 46 sys.stderr.write('\ttojson | j : convert yaml to json OR\n') 47 sys.stderr.write('\t load json then dump out\n') 48 sys.stderr.write('\ttoyaml | y : convert json to yaml OR\n') 49 sys.stderr.write('\t load yaml then dump out\n') 50 sys.stderr.write('e.g.\n') 51 sys.stderr.write(' %s tojson foo1.yaml\n' % argv0) 52 sys.stderr.write(' %s toyaml foo2.json\n' % argv0) 53 sys.stderr.write(' %s toyaml foo3.yaml\n' % argv0) 54 sys.stderr.write(' %s -t 8 -o foo2.json tojson foo1.yaml\n' % argv0) 55 sys.stderr.write(' %s -t 2 -o foo1.yaml toyaml foo2.json\n' % argv0) 56 sys.stderr.write(' %s -t 2 -o foo3.yaml toyaml foo1.yaml\n' % argv0) 57 58 59 def main(argc, argv): 60 indent = 4 61 output_file = None 62 63 options, rargv = getopt.getopt(argv[1:], 64 ':t:o:h', 65 ['indent=', 'output=', 'help']) 66 for opt, arg in options: 67 if opt in ('-t', '--indent'): 68 indent = int(arg) 69 elif opt in ('-o', '--output'): 70 output_file = arg 71 else: 72 usage(argv[0]) 73 return 1 74 75 argv = new_argv(argv[0], rargv) 76 argc = len(argv) 77 if argc != 3: 78 usage(argv[0]) 79 return 1 80 81 subcmd = argv[1] 82 yaml_file = argv[2] 83 txt = None 84 with open(yaml_file, 'r') as file_handler: 85 txt = ''.join(file_handler.readlines()) 86 87 if subcmd in ['tojson', 'j']: 88 out = to_json(txt, indent) 89 elif subcmd in ['toyaml', 'y']: 90 out = to_yaml(txt, indent) 91 else: 92 usage(argv[0]) 93 return 1 94 95 if output_file is None: 96 print(out) 97 else: 98 with open(output_file, 'w') as file_handler: 99 file_handler.write('%s\n' % out) 100 101 return 0 102 103 104 if __name__ == '__main__': 105 sys.exit(main(len(sys.argv), sys.argv))
参考资料: