PyYAML 自动将某些键转换为布尔值

PyYAML automatically converting certain keys to boolean values

几个月来我一直在使用 PyYAML 解析器来转换文件类型作为数据管道的一部分。我发现解析器有时非常特殊,今天我似乎偶然发现了另一种奇怪的行为。我当前正在转换的文件包含以下部分:

off:
    yes: "Flavor text for yes"
    no: "Flavor text for no"

我在字典中保留了一个当前嵌套的列表,以便我可以构建一个平面文档,但保存嵌套以便稍后转换回 YAML。我得到一个 TypeError 说我正在尝试将 strbool 类型连接在一起。我调查后发现 PyYaml 实际上是将我上面的文本部分转换为以下内容:

with open(filename, "r") as f:
    data = yaml.load(f.read())
print data

>> {False: {True: "Flavor text for yes", False: "Flavor text for no}}

我快速检查了一下,发现 PyYAML 正在为 yesnotruefalseon, off.如果键未被引用,它只会进行此转换。引用的值和键将被正常传递。在寻找解决方案时,我发现此行为已记录 here

虽然知道引用密钥会阻止 PyYAML 这样做可能对其他人有帮助,但我没有这个选项,因为我不是这些文件的作者并且没有编写我的代码尽可能少地接触数据。

是否有针对此问题的解决方法或覆盖 PyYAML 中的默认转换行为的方法?

yaml.load 接受第二个参数,加载程序 class(默认情况下,yaml.loader.Loader)。预定义加载器是许多其他加载器的混搭:

class Loader(Reader, Scanner, Parser, Composer, Constructor, Resolver):

    def __init__(self, stream):
        Reader.__init__(self, stream)
        Scanner.__init__(self)
        Parser.__init__(self)
        Composer.__init__(self)
        Constructor.__init__(self)
        Resolver.__init__(self)

Constructor class 将数据类型映射到 Python。一种(笨拙但快速)覆盖布尔转换的方法可能是:

from yaml.constructor import Constructor

def add_bool(self, node):
    return self.construct_scalar(node)

Constructor.add_constructor(u'tag:yaml.org,2002:bool', add_bool)

它覆盖构造函数用于将布尔标记数据转换为 Python 布尔值的函数。我们在这里所做的只是逐字返回字符串。

不过,这会影响 ALL YAML 加载,因为您要覆盖默认构造函数的行为。一个更合适的做事方式可能是创建一个新的 class 派生自 Constructor,以及新的 Loader 对象采用你的自定义构造函数。

PyYAML 是 YAML 1.1 conformant for parsing and emitting, and for YAML 1.1 this is at least partly documented 行为,因此完全没有特殊性,而是有意识的设计。

在 YAML 1.2(2009 年取代了 2005 年的 1.1 规范)中,Off/On/Yes/No 的这种用法被删除,还有其他变化。

ruamel.yaml(免责声明:我是该软件包的作者)中,round_trip_loader 是一个 safe_loader,默认为 YAML 1.2 行为:

import ruamel.yaml as yaml

yaml_str = """\
off:
    yes: "Flavor text for yes"  # quotes around value dropped
    no: "Flavor text for no"
"""

data = yaml.round_trip_load(yaml_str)
assert 'off' in data
print(yaml.round_trip_dump(data, indent=4))

给出:

off:
    yes: Flavor text for yes    # quotes around value dropped
    no: Flavor text for no

如果您的输出需要与 1.1 版兼容,那么您可以转储 明确的 version=(1, 1).

由于嵌套映射的标量值周围的引号是不必要的,因此它们不会在写出时发出。


如果您需要使用 PyYAML 执行此操作,请重写它用于布尔识别的(全局)规则:

import  yaml
from yaml.resolver import Resolver
import re

yaml_str = """\
off:
    yes: "Flavor text for yes"  # quotes around value dropped
    no: "Flavor text for no"
"""

# remove resolver entries for On/Off/Yes/No
for ch in "OoYyNn":
    if len(Resolver.yaml_implicit_resolvers[ch]) == 1:
        del Resolver.yaml_implicit_resolvers[ch]
    else:
        Resolver.yaml_implicit_resolvers[ch] = [x for x in
                Resolver.yaml_implicit_resolvers[ch] if x[0] != 'tag:yaml.org,2002:bool']

data = yaml.load(yaml_str)
print(data)
assert 'off' in data
print(yaml.dump(data))

给出:

{'off': {'yes': 'Flavor text for yes', 'no': 'Flavor text for no'}}
off: {no: Flavor text for no, yes: Flavor text for yes}

这是可行的,因为 PyYAML 保留了一个全局字典 (Resolver.yaml_implicit_resolvers),它将第一个字母映射到 (tag, re.match_pattern) 值的列表。对于oOyY只有一个这样的模式(可以删除),但是对于n/N你也可以匹配null/Null,所以你要删除正确的模式。

删除后 yesnoonOff 不再被识别为 bool,但 TrueFalse还是。

只需清理您的输入:

import  yaml


def sanitize_load(s):
    s = ' ' + s
    for w in "yes no Yes No Off off On on".split():
        s = s.replace(' ' + w + ':', ' "' + w + '":')
    return yaml.load(s[1:])

with open(filename) as f:
    data = sanitize_load(f.read())
print data

这比盲目探索 pyyaml 的可怕深度要好得多。该软件包带有两个几乎但不完全相同的来源,是维护的噩梦。

运行 在工作中遇到了这个问题,不得不以 "correct" 的方式实现它。这是我采取的步骤。请注意,我使用的是 SafeLoader,而不是常规的 Loader。这些步骤将非常相似。

一般步骤是

  1. 创建自定义 SafeConstuctor
  2. 创建导入此自定义 SafeConstructor
  3. 的自定义 SafeLoader
  4. 调用yaml.load的"load"函数,传入自定义的SafeLoader我们 使用自定义 SafeConstructor
  5. 创建

MySafeConstructor.py

from yaml.constructor import SafeConstructor

# Create custom safe constructor class that inherits from SafeConstructor
class MySafeConstructor(SafeConstructor):

    # Create new method handle boolean logic
    def add_bool(self, node):
        return self.construct_scalar(node)

# Inject the above boolean logic into the custom constuctor
MySafeConstructor.add_constructor('tag:yaml.org,2002:bool',
                                      MySafeConstructor.add_bool)
  1. 然后我使用与 rest of the loaders defined 相同的格式创建一个全新的加载器 class,除了我们传入新创建的自定义 Constructor。我们基本上只是 "adding" 此列表。

MySafeLoader.py

from yaml.reader import *
from yaml.scanner import *
from yaml.parser import *
from yaml.composer import *
from MySafeConstructor import *
from yaml.resolver import *


class MySafeLoader(Reader, Scanner, Parser, Composer, MySafeConstructor, Resolver):

    def __init__(self, stream):
        Reader.__init__(self, stream)
        Scanner.__init__(self)
        Parser.__init__(self)
        Composer.__init__(self)
        MySafeConstructor.__init__(self)
        Resolver.__init__(self)
  1. 最后,我们将 import 自定义安全加载器放入 main.py 或您加载的任何地方(也适用于 __init__()

main.py

# Mandatory imports
from yaml import load
from MySafeLoader import MySafeLoader

def main():

    filepath_to_yaml = "/home/your/filepath/here.yml"

    # Open the stream, load the yaml doc using the custom SafeLoader
    file_stream: TextIO = open(filepath_to_yaml , 'r')
    yaml_as_dict = load(file_stream, MySafeLoader)
    file_stream.close()

    # Print our result
    print(yaml_as_dict)

现在我们可以使用标准加载器或为我们想要的布尔逻辑修改的自定义加载器。如果您想要字符串之外的其他值,您可以尝试覆盖 bool_values list in the MySafeConstructor class,因为这是一个包含翻译逻辑的全局列表。

constructor.py

    bool_values = {
        'yes':      True,
        'no':       False,
        'true':     True,
        'false':    False,
        'on':       True,
        'off':      False,
    }

注意:如果你这样做,你不会想覆盖布尔逻辑,只覆盖这个列表。