使用Python在YAML中获取重复密钥

2024-05-21 01:36:22 发布

您现在位置:Python中文网/ 问答频道 /正文

我们需要解析包含重复密钥的YAML文件,所有这些都需要解析。跳过重复项是不够的。我知道这是违反YAML规范的,我不想这样做,但是我们使用的第三方工具可以实现这种使用,我们需要处理它。

文件示例:

build:
  step: 'step1'

build:
  step: 'step2'

在分析之后,我们应该有一个类似的数据结构:

yaml.load('file.yml')
# [('build', [('step', 'step1')]), ('build', [('step', 'step2')])]

dict不能再用于表示已分析的内容。

我正在寻找一个Python的解决方案,但是我没有找到支持它的库,我遗漏了什么吗?

或者,我很乐意写我自己的东西,但我想让它尽可能简单。ruamel.yaml看起来像是Python中最高级的YAML解析器,而且它看起来具有适度的可扩展性,是否可以扩展以支持重复字段?


Tags: 文件工具build规范yaml示例数据结构step
3条回答

PyYAML只会默默地覆盖第一个条目,ruamel.yaml如果与旧API一起使用,则会给出一个DuplicateKeyFutureWarning,并用新API引发一个DuplicateKeyError

如果您不想为所有类型创建一个完整的Constructor,那么覆盖SafeConstructor中的映射构造函数就可以:

import sys
from ruamel.yaml import YAML
from ruamel.yaml.constructor import SafeConstructor

yaml_str = """\
build:
  step: 'step1'

build:
  step: 'step2'
"""


def construct_yaml_map(self, node):
    # test if there are duplicate node keys
    data = []
    yield data
    for key_node, value_node in node.value:
        key = self.construct_object(key_node, deep=True)
        val = self.construct_object(value_node, deep=True)
        data.append((key, val))


SafeConstructor.add_constructor(u'tag:yaml.org,2002:map', construct_yaml_map)
yaml = YAML(typ='safe')
data = yaml.load(yaml_str)
print(data)

它给出:

[('build', [('step', 'step1')]), ('build', [('step', 'step2')])]

然而,似乎没有必要将step: 'step1'放入列表中。只有当存在重复项时(如果需要,可以通过缓存self.construct_object(key_node, deep=True)的结果进行优化),以下操作才会创建列表:

def construct_yaml_map(self, node):
    # test if there are duplicate node keys
    keys = set()
    for key_node, value_node in node.value:
        key = self.construct_object(key_node, deep=True)
        if key in keys:
            break
        keys.add(key)
    else:
        data = {}  # type: Dict[Any, Any]
        yield data
        value = self.construct_mapping(node)
        data.update(value)
        return
    data = []
    yield data
    for key_node, value_node in node.value:
        key = self.construct_object(key_node, deep=True)
        val = self.construct_object(value_node, deep=True)
        data.append((key, val))

它给出:

[('build', {'step': 'step1'}), ('build', {'step': 'step2'})]

一些要点:

  • 或许不用说,这对YAML merge keys<<: *xyz)不起作用
  • 如果需要ruamel.yaml的往返功能(yaml = YAML()),则需要更复杂的construct_yaml_map
  • 如果要转储输出,应该为此实例实例化一个新的YAML()实例,而不是重新使用用于加载的“修补”实例(它可能工作,这只是为了确保):

    yaml_out = YAML(typ='safe')
    yaml_out.dump(data, sys.stdout)
    

    它给出(第一个construct_yaml_map):

    - - build
      - - [step, step1]
    - - build
      - - [step, step2]
    
  • 在PyYAML和ruamel.yaml中不起作用的是yaml.load('file.yml')。如果您不想open()您可以自己执行以下操作:

    from pathlib import Path  # or: from ruamel.std.pathlib import Path
    yaml = YAML(typ='safe')
    yaml.load(Path('file.yml')
    

1免责声明:我是该软件包的作者。

如果您可以稍微修改输入数据,那么您应该可以通过将单个类似yaml的文件转换为多个yaml文档来完成这项工作。如果yaml文档本身在一行上被---分隔开,那么它们可以在同一个文件中,并且您很容易地看到条目彼此之间被两个新行分隔开:

with open('file.yml', 'r') as f:
    data = f.read()
    data = data.replace('\n\n', '\n---\n')

    for document in yaml.load_all(data):
        print(document)

输出:

{'build': {'step': 'step1'}}
{'build': {'step': 'step2'}}

您可以重写pyyaml加载密钥的方式。例如,可以将defaultdict与每个键的值列表一起使用:

from collections import defaultdict
import yaml


def parse_preserving_duplicates(src):
    # We deliberately define a fresh class inside the function,
    # because add_constructor is a class method and we don't want to
    # mutate pyyaml classes.
    class PreserveDuplicatesLoader(yaml.loader.Loader):
        pass

    def map_constructor(loader, node, deep=False):
        """Walk the mapping, recording any duplicate keys.

        """
        mapping = defaultdict(list)
        for key_node, value_node in node.value:
            key = loader.construct_object(key_node, deep=deep)
            value = loader.construct_object(value_node, deep=deep)

            mapping[key].append(value)

        return mapping

    PreserveDuplicatesLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, map_constructor)
    return yaml.load(src, PreserveDuplicatesLoader)

相关问题 更多 >