我早先写了一篇文章《为什么把json格式作为对人类友好的配置文件格式不是一个好主意》。今天我们将一起讨论下YAML格式里的几个普遍问题。
默认非安全的
YAML是默认非安全的。加载一个用户提供的(非可信的)YAML字符串需要谨慎考虑。
运行这条命令print(yaml.load(open("a.yaml")))你将得到如下结果:
其他许多编程语言(包括PHP1和Ruby)也是默认非安全的。在GitHub上搜索yaml.load,你会得到高达280万条结果,而搜索yaml.safe_load只能得到2.6万条结果。
值得一提的是,许多情况下,yaml.load()是OK的——也就是说用yaml.load()加载配置文件是OK的,这主要是因为配置文件的来源通常(而非总是)可靠的,其中许多配置文件来自于静态的yaml测试文件。但是仍然有人会好奇,在280万条结果中,究竟有多少不安全的case呢?
这不是一个纯理论的问题,在2013年有人发现每一个Ruby on Rails应用程序在远程执行代码的时候容易受到攻击。
有人可能会说这本质上不是yaml的错,而是调用yaml的库使用不当导致的。但是需要明白的一点是,许多的库默认是不安全的——尤其对于那些动态语言来说。所以,归根到底,还是YAML的问题。
也有人会说,这个问题可以通过用safe_load()代替load()轻松地得到解决。但是问题是许多人并没有意识到load()的这个问题,或者即使知道load()存在这样的问题,也很容易忽略这一点。所以load()是一个很不好的API设计方案。
难以编辑,尤其对于大文件
YAML文件编辑起来费力,当文件变大时,编辑愈加困难。
在Ruby on Rails的翻译文件中,有一个例子可以说明:
上面的例子看上去还好是吗?但是,如果文件是100行,甚至是1000行呢?那你很难知道你在文件的哪处,因为屏幕已经显示不下了。你必须滚动页面才行,但是这时候你还得跟踪缩进。即使有缩进提示,这时候跟踪缩进也很难,尤其是YAML使用2个字符缩进,而禁止使用tab进行缩进2。
当你意外地弄错了缩进的时候,并不会以error的方式提示你,但你会得到一个你根本想不到的结果。那就尽情享受debug吧。
我用Python编程已经有十年了。我习惯了使用空白字符,但有时候我仍然会纠结于YAML。由于Python一般没有长达几页才能写下的函数,失去清晰性和可读性这一点不是很明显,但是对于数据或者配置文件来说,长度并没有限制。
对于小文件而言,这当然算不上什么问题,但是一旦变成大文件,YAML就不那么好用了,尤其是你将来还要对它进行编辑的时候。
它很复杂
从一个简单的demo来看,YAML简单明了,而实际上并不是这样的。YAML specification包含23449个词,对比来看,TOML只有3339个词,JSON只有1969个词,而XML是20603个词。
我们中有谁完整地读了它的详细说明?有谁不仅读了,还完全理解了所有内容?又有谁读了、理解了并全部记住了它们?
例如:你知道YAML有9种不同的方式书写多行字符串吗?这9种方法只有细微的不同。
如果你查看《YAML的9种实现多行字符串的方式》那篇文章的修改历史,事情会变得更有趣,因为作者发现越来越多的方式可以实现多行字符串,而且其间的微小差别也越来越多。
YAML specification的序言如下(黑色加粗部分是我的强调):
这一章节简单地展现了YAML强大的表达能力。我们并不期望第一次阅读此文的读者理解所有的东西。相反,这些章节是用来作为YAML specification的提示物的。
奇怪的行为
下面的配置文件会解析成什么呢?
没错,Yes被解析成了bool值True,如下:
再看一个例子:
3.5.3被识别成字符串,而9.3被当成数字处理,结果是:
再看下一个例子:
013是Tilburg一个著名的音乐厅,但是却被解析成了八进制数:
由于以上这些错误,还有更多的错误case,许多经验丰富的YMALers在使用字符串时,即使有时候不是必须那么做,他们总是加上引号。许多人习惯不使用引号,这让继续编辑这个文件的也不使用引号的人(很可能是别的人)很容易忘记。
可移植性差
YAML很复杂,官方声称的可移植性强其实是被夸大了的。我们看从YAML specification中拿过来的一个例子:
先不说大多数读者不知道这玩意儿是什么意思,在Python中我们用PyYAML对它进行解析:
用Ruby对它解析是正常的:
python解析失败的原因是Python不支持用列表作为字典的键
这个限制并不是只在Python中有,很多语言像PHP,JavaScript,Go等语言都有这样的限制。
所以在YAML文件中使用这样的表达,在大多数语言中都没法解析。
另外一个例子是:
python报错如下:
ruby输出如下:
这是因为在一个yaml文件中包含多个文档(每个文档以---开头),Python中需要使用load_all()解析包含多文档的yaml文件,而Ruby中使用load()只能解析第一个文档。据我所知,Ruby中没有任何方法可以读取多文档配置。
在不同的实现之间还有许多不兼容的地方。
达到目标了吗?
YAML specification中说:
YAML的设计目标,按照优先级从高到低排序:
YAML便于人阅读;
YAML数据方便地在多种语言之间转化;
YAML格式和各种轻巧的语言的原生数据结构很接近;
YAML能支持通用的工具;
YAML支持一次性通过;
YAML表达语义能力丰富,且可扩展性强;
YAML实现和使用简单
那么YAML到底做得怎样呢?
YAML便于人阅读
当你只使用它的一个小小的子集的时候,这一说法才成立。使用它的全集很复杂,甚至比JSON和XML都复杂。
YAML数据方便地在多种语言之间转化;
事实并非如此。很容易找出一些例子,是常规语言不支持的。
YAML格式和各种轻巧的语言的原生数据结构很接近;
见上文。为什么只支持轻巧的语言(动态语言)呢?其他语言支持吗?
YAML能支持通用的工具;
对此我都不知道它具体意思是什么,也没有找到关于此的详细阐述。
YAML支持一次性通过;
对此我持保留意见。
YAML表达语义能力丰富,且可扩展性强;
是,它的确表达语义丰富,但是太丰富了,因为它语法很复杂。
YAML实现和使用简单
结语
请不要误解我的意思,并不是说YAML很糟糕,它既不是和JSON一样问题多,也不见得比JSON好很多。它有一些在使用之初不会明显显露出来的缺点,我们也有一些另外的选择,比如TOML或者其他格式。
就我个人来说,如果有其他选择,我不太可能继续使用YAML。
如果你一定要使用YAML,我建议你使用StrictYAML,它移除了一些(尽管不是所有的)不安全的部分。
脚注
1在PHP中你需要修改ini配置文件来启用安全的配置,而不是仅仅使用yaml_safe()。PHP使用者通过配置使得一些机械的方法更机械。
2我并不想在空格和tab制表符之间制造争论,但是如果允许使用tab键,通过增加tab宽度到更多的字符,可以使得阅读缩进更清晰一些——这是tab键使用的意义所在。
反馈
英文原文:https://arp242.net/weblog/yaml_probably_not_so_great_after_all.html
译者:周游
领取专属 10元无门槛券
私享最新 技术干货