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

时间:2016-04-06 22:36:01

标签: python yaml pyyaml

我已经使用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中的默认转化行为的方法?

4 个答案:

答案 0 :(得分:8)

PyYAML是YAML 1.1 conformant用于解析和发射,而对于YAML 1.1,这是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 / {{1您也可以匹配N / null,因此您必须删除正确的模式。

删除后Nullyesnoon不再被视为bool,但OffTrue仍然是

答案 1 :(得分:3)

yaml.load接受第二个参数,一个加载器类(默认为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类是执行数据类型转换的类。覆盖布尔转换的一种(kludgy,但快速)方法可能是:

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布尔值的函数。我们在这里做的只是逐字地返回字符串。

这会影响所有 YAML加载,因为您重写了默认构造函数的行为。更合适的方法是创建一个派生自Constructor的新类,以及使用自定义构造函数的新Loader对象。

答案 2 :(得分:1)

只需清理您的输入:

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的可怕深处盲目戳戳要好得多。这些软件包带有两个,几乎但不完全相同的资源,是维护的噩梦。

答案 3 :(得分:0)

在工作中遇到此问题,必须以“正确”的方式实施。这是我采取的步骤。注意,我使用的是SafeLoader,而不是常规的Loader。步骤将非常相似。

常规步骤是

  1. 创建自定义SafeConstuctor
  2. 创建导入此自定义SafeLoader的自定义SafeConstructor
  3. 调用yaml.load的“加载”函数,并传入自定义SafeLoader 使用自定义SafeConstructor
  4. 创建

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相同的格式创建一个全新的加载程序类,除了我们传入新创建的自定义Constructor。我们实质上只是在“添加”到该列表。

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,
    }

注意:如果执行此操作,则不想覆盖布尔逻辑,只需覆盖此列表即可。