PyYAML: загрузить и выгрузить файл yaml и сохранить теги ( !CustomTag )

Я хочу создать фильтр 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 на ! в строке, и это близко к тому, что мне нужно, за исключением того, что он анализирует и выводит пользовательский тег как строку в кавычках, следовательно, это больше не тег:

>>> 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"

Я думаю, что не так много пропало, но что? Как я могу загрузить файл, содержащий какие-либо теги, и потом сбросить их?


person Jan    schedule 03.05.2017    source источник
comment
Использование yaml.load() небезопасно, поскольку кто-то может выполнить произвольный код, если у него есть контроль над файлом YAML, а PyYAML не предупреждает об опасностях.   -  person Anthon    schedule 03.05.2017


Ответы (2)


Поскольку 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'

Примечания:

  • Небезопасно использовать load() PyYAML. Не используйте его, в этом нет необходимости (как показывает мой код). Что еще хуже, так это отсутствие обратной связи от PyYAML о какой-либо опасности.
  • PyYAML сбрасывает все скаляры, у которых есть тег с кавычками, даже если вы сохраняете стиль узла, как я (или принудительно используете пустую строку). Чтобы этого не произошло, вам придется довольно глубоко покопаться в сериализации узлов. Я работаю над исправлением этого в своем пакете ruamel.yaml, так как кавычки очень часто не нужны.
  • Ваши якоря и псевдонимы не разрешаются. Просто PyYAML недостаточно умен, чтобы делать что-либо, кроме расширения ключа слияния во время загрузки. . Если у вас есть нормальная ссылка на себя в вашем YAML, вы получите якорь и псевдоним в своем дампе YAML.
  • Приведенное выше вызовет ошибку, если ваш узел после тега не является скаляром (то есть отображением или последовательностью). Их также можно загружать/сбрасывать. просто добавив несколько типов и расширив default_constructor некоторыми elif isinstance(node, yaml.MappingNode) и elif isinstance(node, yaml.SequenceNode). Я бы заставил их создавать разные типы (которые ведут себя как dict или список), и если вы пойдете по этому пути, вы должны знать, что их создание должно происходить в двухэтапном процессе (yield построенный объект, затем получить значения подузла и заполнить объект), в противном случае вы не можете использовать самореферентные структуры (т. е. псевдонимы внутри узла).
  • PyYAML не сохраняет порядок элементов в отображении
  • У вас может быть тег !CustomTag:, оканчивающийся двоеточием, но я считаю, что читать !CustomTag: foo не очень удобно для человека, так как это очень похоже на пару ключ-значение в отображении блочного стиля.
person Anthon    schedule 03.05.2017

Принятый ответ обрабатывает только скаляры. Мне также нужны типы отображения. Я хотел универсальное решение. Я думаю, что я, возможно, перепроектировал это. Я чувствую, что это может быть проще, и дальнейшее упрощение приветствуется.

Итак, если у вас есть 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"

Примечание. Если добавленный код обновляет данные и не проверяет оболочку, он может вернуть элементы к развернутому типу и потерять тег.

person Frobbit    schedule 21.03.2020