【问题标题】:PyYAML: load and dump yaml file and preserve tags ( !CustomTag )PyYAML:加载和转储 yaml 文件并保留标签 ( !CustomTag )
【发布时间】:2017-10-01 15:03:58
【问题描述】:

我想创建一个 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"

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

【问题讨论】:

  • 使用yaml.load() 是不安全的,如果有人可以控制 YAML 文件,他们就可以执行任意代码,而且 PyYAML 不会警告它的危险。

标签: python parsing pyyaml


【解决方案1】:

由于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 中获得锚点和别名。
  • 如果您的节点在标记之后不是标量(即映射或序列),则上述内容会很好地引发错误。一般也可以加载/转储这些。只需添加一些类型并用一些elif isinstance(node, yaml.MappingNode)elif isinstance(node, yaml.SequenceNode) 扩展default_constructor。我会让它们创建不同的类型(表现得像一个字典和列表),如果你走那条路,你应该知道构建这些需要分两步进行(yield 构造的对象,然后获取子节点值并填充对象),否则不能使用自引用结构(即节点内的别名)。
  • PyYAML 不保留映射中元素的顺序
  • 您可以有一个以冒号结尾的标签!CustomTag:,但我发现阅读!CustomTag: foo 不太人性化,因为它看起来非常像块样式映射中的键值对。李>

【讨论】:

    【解决方案2】:

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

    如果你的 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"
    

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

    【讨论】:

      猜你喜欢
      • 2021-05-20
      • 2021-09-28
      • 2011-11-07
      • 2011-03-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-03-01
      • 1970-01-01
      相关资源
      最近更新 更多