PyYAML:加载和转储yaml文件并保留标签(!CustomTag)

时间:2017-05-03 16:50:09

标签: python parsing pyyaml

我想创建一个YAML过滤器,它读取YAML文件,处理它并在之后转储它。

它必须解决任何别名(开箱即用):

>>> yaml.dump(yaml.load("""
Foo: &bar
  name: bar
Foo2:
  <<: *bar
"""))

'Foo: {name: bar}\nFoo2: {name: bar}\n'

但它也应保留任何类型的!CustomTag: foo表达式,例如:

>>> yaml.dump(yaml.load("Name: !Foo bar "))
yaml.constructor.ConstructorError: could not determine a constructor for the tag '!Foo' in "<unicode string>", line 1, column 7:
Name: !Foo bar
      ^

我看了pyYAML Errors on "!" in a string,这很接近我需要的,除了它解析并输出自定义标签为引用字符串,因此它不再是标签了:

>>> def default_ctor(loader, tag_suffix, node):
...   return tag_suffix + ' ' + node.value

>>> yaml.add_multi_constructor('', default_ctor)
>>> yaml.dump(yaml.load("Name: !Foo bar "), default_flow_style=False)
"Name: '!Foo bar'\n"

我想没有多少遗漏,但是什么?如何加载包含任何标记的文件并在之后转储它们?

2 个答案:

答案 0 :(得分:5)

由于default_ctor()返回一个字符串(它只是标记和标量的串联),这就是被转储的内容。并且因为标记以!开头,将该字符串转储到标量会得到引号。

如果您想要一般性地保留标记和值,则需要将它们存储在特殊类型(而不是“普通”Python字符串)中,并为该类型提供代表(即转储例程):

import sys
import yaml

yaml_str = """\
Name: !Foo bar
Alt: !Bar foo
"""


class GenericScalar:
    def __init__(self, value, tag, style=None):
        self._value = value
        self._tag = tag
        self._style = style

    @staticmethod
    def to_yaml(dumper, data):
        # data is a GenericScalar
        return dumper.represent_scalar(data._tag, data._value, style=data._style)


def default_constructor(loader, tag_suffix, node):
    if isinstance(node, yaml.ScalarNode):
        return GenericScalar(node.value, tag_suffix, style=node.style)
    else:
        raise NotImplementedError('Node: ' + str(type(node)))


yaml.add_multi_constructor('', default_constructor, Loader=yaml.SafeLoader)

yaml.add_representer(GenericScalar, GenericScalar.to_yaml, Dumper=yaml.SafeDumper)

data = yaml.safe_load(yaml_str)
yaml.safe_dump(data, sys.stdout, default_flow_style=False, allow_unicode=True)

这给出了:

Alt: !Bar 'foo'
Name: !Foo 'bar'

注意:

  • 使用PyYAML的load()是不安全的。 不要使用,没有必要(正如我的代码所示)。更糟糕的是,PyYAML没有反馈说存在任何危险。
  • 即使您像我一样保留节点样式(或强制为空字符串),PyYAML也会转储所有带引号标记的标量。为了防止这种情况发生,你将不得不深入挖掘节点的序列化。我一直在我的ruamel.yaml包中修复此问题,因为引号通常不是必需的。
  • 您的锚点和别名无法解析。只是PyYAML不够智能,只能在加载时扩展merge key。如果您的YAML中有正常的自引用,您将在转储的YAML中获得锚点和别名。
  • 如果您的节点在标签之后是标量(即映射或序列)之外的任何内容,则上述情况会很好地引发错误。也可以一般地加载/转储它们。只需添加一些类型并使用default_constructorelif isinstance(node, yaml.MappingNode)扩展elif isinstance(node, yaml.SequenceNode)。我会让那些创建不同的类型(行为类似于dict resp。列表),如果你走这条路,你应该意识到构建它们需要在两步过程中发生(yield构造的对象,然后获取子节点值并填充对象),否则您不能使用自引用结构(即节点内的别名)。
  • PyYAML不保留映射中元素的顺序
  • 您可以使用以冒号结尾的标记!CustomTag:,但我发现阅读!CustomTag: foo并不是那么友好,因为它看起来非常像块样式中的键值对映射。

答案 1 :(得分:0)

可接受的答案仅处理 标量。我也需要映射类型。我想要一个通用的解决方案。我想我可能对此工程过度了。我觉得这可能更简单,欢迎进一步简化。

因此,如果您有看起来更像这样的Yaml:

Name: !Foo bar
Alt: !Bar foo
other: !Join
    - thing
    - other thing
textblock: !Mangle |
    This is a block
    of text that 
    spans lines

尝试以下较长的代码:

import sys
import yaml
import pprint

yaml_str = """\
Name: !Foo bar
Alt: !Bar foo
other: !Join
    - thing
    - other thing
textblock: !Mangle |
    This is a block
    of text that 
    spans lines
"""

class SafeUnknownConstructor(yaml.constructor.SafeConstructor):
    def __init__(self):
        yaml.constructor.SafeConstructor.__init__(self)

    def construct_undefined(self, node):
        data = getattr(self, 'construct_' + node.id)(node)
        datatype = type(data)
        wraptype = type('TagWrap_'+datatype.__name__, (datatype,), {})
        wrapdata = wraptype(data)
        wrapdata.tag = lambda: None
        wrapdata.datatype = lambda: None
        setattr(wrapdata, "wrapTag", node.tag)
        setattr(wrapdata, "wrapType", datatype)
        return wrapdata


class SafeUnknownLoader(SafeUnknownConstructor, yaml.loader.SafeLoader):

    def __init__(self, stream):
        SafeUnknownConstructor.__init__(self)
        yaml.loader.SafeLoader.__init__(self, stream)


class SafeUnknownRepresenter(yaml.representer.SafeRepresenter):
    def represent_data(self, wrapdata):
        tag = False
        if type(wrapdata).__name__.startswith('TagWrap_'):
            datatype = getattr(wrapdata, "wrapType")
            tag = getattr(wrapdata, "wrapTag")
            data = datatype(wrapdata)
        else:
            data = wrapdata
        node = super(SafeUnknownRepresenter, self).represent_data(data)
        if tag:
            node.tag = tag
        return node

class SafeUnknownDumper(SafeUnknownRepresenter, yaml.dumper.SafeDumper):

    def __init__(self, stream,
            default_style=None, default_flow_style=False,
            canonical=None, indent=None, width=None,
            allow_unicode=None, line_break=None,
            encoding=None, explicit_start=None, explicit_end=None,
            version=None, tags=None, sort_keys=True):

        SafeUnknownRepresenter.__init__(self, default_style=default_style,
                default_flow_style=default_flow_style, sort_keys=sort_keys)

        yaml.dumper.SafeDumper.__init__(self,  stream,
                                        default_style=default_style,
                                        default_flow_style=default_flow_style,
                                        canonical=canonical,
                                        indent=indent,
                                        width=width,
                                        allow_unicode=allow_unicode,
                                        line_break=line_break,
                                        encoding=encoding,
                                        explicit_start=explicit_start,
                                        explicit_end=explicit_end,
                                        version=version,
                                        tags=tags,
                                        sort_keys=sort_keys)


MySafeLoader = SafeUnknownLoader
yaml.constructor.SafeConstructor.add_constructor(None, SafeUnknownConstructor.construct_undefined)
data = yaml.load(yaml_str, MySafeLoader)
pprint.pprint(data)
yaml.dump_all([data], sys.stdout, Dumper=SafeUnknownDumper, default_flow_style=False, allow_unicode=True)

哪个输出:

{'Alt': u'foo',
 'Name': u'bar',
 'other': ['thing', 'other thing'],
 'textblock': u'This is a block\nof text that \nspans lines\n'}

Alt: !Bar 'foo'
Name: !Foo 'bar'
other: !Join
- thing
- other thing
textblock: !Mangle "This is a block\nof text that \nspans lines\n"

注意:如果添加的代码更新了数据并且不检查包装器,则它可以将元素还原为未包装的类型并丢失标签