YAML 是一個被廣泛使用的數(shù)據(jù)序列化和配置語言,作為一個開發(fā)者,總是不免和它打交道。但處理 YAML 文檔,尤其是使用 PyYAML 的過程總是非常痛苦。
這篇文章分享我在 Python 下使用 PyYAML 的技巧和代碼片段,并介紹幾個相關(guān)的庫。
注意:本文中的代碼僅保證在 Python 3 下正常工作
總是使用?safe_load/safe_dump
PyYAML 的?load?函數(shù)可以構(gòu)造任意 Python 對象(Pickle 協(xié)議),這意味著一次?load?可能導(dǎo)致任意 Python 函數(shù)被執(zhí)行。
為了確保應(yīng)用程序的安全性,盡量在任何情況下使用?yaml.safe_load?和?yaml.safe_dump。
保留字段順序
Python 3.7+ 中,dict?keys 具備保留插入順序的特性,所以通過?yaml.safe_load?得到的?dict,其 keys 順序會與原始文件保持一致。
?
>>>?import?yaml >>>?text?=?"""--- ...?c:?1 ...?b:?1 ...?d:?1 ...?a:?1 ...?""" >>>?d?=?yaml.safe_load(text) >>>?d {'c':?1,?'b':?1,?'d':?1,?'a':?1} >>>?list(d) ['c',?'b',?'d',?'a']
?
當把?dict?導(dǎo)出為 YAML 字符串時,為?yaml.safe_dump?傳遞?sort_keys=False?來保留 keys 的順序。
?
>>>?print(yaml.safe_dump(d)) a:?1 b:?1 c:?1 d:?1 >>>?d['e']?=?1 >>>?print(yaml.safe_dump(d,?sort_keys=False)) c:?1 b:?1 d:?1 a:?1 e:?1
?
如果 Python 版本較低,或者你想確保代碼能在更廣泛的環(huán)境下工作,你可以使用?oyaml?庫來代替 PyYAML 的?yaml?包。
?
>>>?import?oyaml?as?yaml >>>?d?=?yaml.safe_load(text) >>>?d OrderedDict([('c',?1),?('b',?1),?('d',?1),?('a',?1)]) >>>?d['e']?=?1 >>>?print(yaml.safe_dump(d,?sort_keys=False)) c:?1 b:?1 d:?1 a:?1 e:?1
?
優(yōu)化列表項的縮進
默認情況下,PyYAML 輸出的列表縮進與其父元素一致。
?
>>>?d?=?{'a':?[1,?2,?3]} >>>?print(yaml.safe_dump(d)) a: -?1 -?2 -?3
?
這并不是很好的格式,根據(jù) Ansible 和 HomeAssistant 等 YAML 書寫規(guī)范,列表項應(yīng)該縮進 2 空格。
這種格式也會對導(dǎo)致列表項不會被如 VSCode 等編輯器識別,進而無法使用編輯器的折疊功能。
要解決這個問題,使用如下代碼片段,在代碼中定義?IndentDumper?class:
?
class?IndentDumper(yaml.Dumper): ????def?increase_indent(self,?flow=False,?indentless=False): ????????return?super(IndentDumper,?self).increase_indent(flow,?False)
?
然后將它傳遞給?yaml.dump?的?Dumper?關(guān)鍵字參數(shù)。
?
>>>?print(yaml.dump(d,?Dumper=IndentDumper)) a: ??-?1 ??-?2 ??-?3
?
注意,yaml.safe_dump?由于有自己的 Dumper class,傳遞此參數(shù)會造成沖突。
輸出可讀的 UTF-8 字符
默認情況下,PyYAML 假設(shè)你希望輸出的結(jié)果里只有 ASCII 字符。
?
>>>?d?=?{'a':?'你好'} >>>?print(yaml.safe_dump(d)) a:?"u4F60u597D"
?
這會讓輸出結(jié)果非常難以閱讀。
在 UTF-8 足夠普及的今天,直接輸出 UTF-8 字符是非常安全的。因此我們可以將?allow_unicode=True?傳入?yaml.safe_dump?使 PyYAML 將 Unicode 轉(zhuǎn)換成 UTF-8 字符串。
?
>>>?print(yaml.safe_dump(d,?allow_unicode=True)) a:?你好
?
一些 YAML 相關(guān)的庫
oyaml
Link:?https://github.com/wimglenn/oyaml
正如上文中提到的,oyaml 是?yaml?包的替換品,使?dict?keys 的順序在 dump/load 的時候得以保留。
oyaml 是一個單文件庫,只有 53 行代碼,因此使用起來非常靈活,你可以直接把它的代碼復(fù)制到自己的項目中,然后根據(jù)自己的需求進行修改。
strictyaml
Link:?https://github.com/crdoconnor/strictyaml
有的人說 YAML 過于復(fù)雜和靈活,不是一個好的配置語言。但我認為這不是 YAML 的問題,而是使用方式的問題。如果我們限制程序只使用 YAML 的部分功能,YAML 其實可以變得像它設(shè)計的那般好用。
這就是 StrictYAML 的設(shè)計意圖,它是一個類型安全的 YAML 解析器,實現(xiàn)了 YAML 規(guī)范說明中的一個子集?。
如果你對 YAML 的輸入輸出有較強的安全考慮,建議使用 StrictYAML 代替 PyYAML。
順帶一提的是,StrictYAML 的文檔站有很多關(guān)于設(shè)計細節(jié)和配置語言思考的文章,非常值得一看。
ruamel.yaml
Link:?https://yaml.readthedocs.io/en/latest/overview.html
ruamel.yaml 是 PyYAML 的一個分叉,于 2009 年發(fā)布并持續(xù)維護至今。
ruamel.yaml 的文檔里詳細說明了它和 PyYAML 的差異??傮w來說,ruamel.yaml 專注在 YAML 1.2 上,對一些語法進行了優(yōu)化。
ruamel.yaml 最令我感興趣的特性是輸入輸出的 “round-trip”,可以最大程度地保留輸入源的原始格式。官方文檔中的定義是這樣的:
A round-trip is a YAML load-modify-save sequence and ruamel.yaml tries to preserve, among others:
comments
block style and key ordering are kept, so you can diff the round-tripped source
flow style sequences ( ‘a(chǎn): b, c, d’) (based on request and test by Anthony Sottile)
anchor names that are hand-crafted (i.e. not of the formidNNN)
merges?in dictionaries are preserved
如果你有盡可能保留原始格式的需求,建議使用 ruamel.yaml 代替 PyYAML。
在使用中我注意到 ruamel.yaml 的 safe load 方法 (YAML(typ='safe').load) 與 PyYAML 有些不同,它無法解析 flow style 的集合定義 (如?a: {"foo": "bar"}),這點沒有在文檔中提及,使用時須多加注意。
總結(jié)
YAML 有它好的地方和壞的地方。它易于閱讀,初期的學(xué)習(xí)曲線非常平緩。但 YAML 的規(guī)范說明非常復(fù)雜,不僅造成了使用中的混亂,也使不同語言的實現(xiàn)在很多細微的地方難以保持一致。
盡管有這些小毛病,YAML 仍然是我心中最好的配置語言。希望這篇文章所介紹的技巧能夠幫助你避免問題,獲得更好的開發(fā)和使用體驗。
審核編輯:湯梓紅
評論
查看更多