PyYaml "include file" 和 yaml 别名 (anchors/references)

PyYaml "include file" and yaml aliases (anchors/references)

我有一个大型 YAML 文件,其中大量使用 YAML 锚点和引用,例如:

warehouse:
  obj1: &obj1
    key1: 1
    key2: 2
specific:
  spec1: 
    <<: *obj1
  spec2:
    <<: *obj1
    key1: 10

文件太大,所以我寻找了一个解决方案,允许我拆分为 2 个文件:warehouse.yamlspecific.yaml,并将 warehouse.yaml 包含在 specific.yaml。我读了 this simple article,它描述了我如何使用 PyYAML 来实现它,但它也说不支持合并键 (<<)。

我真的报错了:

yaml.composer.ComposerError: found undefined alias 'obj1

当我尝试那样做时。

所以,我开始寻找替代方法,但我很困惑,因为我对 PyYAML 了解不多。

我可以获得所需的合并密钥支持吗?我的问题还有其他解决方案吗?

在 PyYAML 中处理锚点和别名的关键是字典 anchors,它是 Composer 的一部分。它将锚点映射到节点,以便可以查找别名。它的存在受 Composer 的存在限制,它是您使用的 Loader 的复合元素。

Loader class 仅在调用 yaml.load() 期间存在,因此之后没有简单的方法可以提取它:首先你必须创建实例Loader() 持续存在,然后确保不调用正常的 compose_document() 方法(除其他事项外,self.anchors = {} 对下一个文档(在单个流中)是干净的)。

如果你有warehouse.yaml,事情会更复杂:

warehouse:
  obj1: &obj1
    key1: 1
    key2: 2

specific.yaml:

warehouse: !include warehouse.yaml
specific:
  spec1:
    <<: *obj1
  spec2:
    <<: *obj1
    key1: 10

即使您可以保留、提取和传递锚点信息,您也永远无法将它与您的代码段一起使用,因为处理 specific.yaml 的作曲家会比标签更早遇到未定义的别名!include 用于构造(并填充 anchors)。

要避免这个问题,你可以做的是包括 specific.yaml

specific:
  spec1:
    <<: *obj1
  spec2:
    <<: *obj1
    key1: 10

来自 warehouse.yaml:

warehouse:
  obj1: &obj1
    key1: 1
    key2: 2
specific: !include specific.yaml

,或将两者都包含在第三个文件中。 请注意,密钥 specific 在两个文件中都有

有了这两个文件 运行:

import sys
from ruamel import yaml

def my_compose_document(self):
    self.get_event()
    node = self.compose_node(None, None)
    self.get_event()
    # self.anchors = {}    # <<<< commented out
    return node

yaml.SafeLoader.compose_document = my_compose_document

# adapted from http://code.activestate.com/recipes/577613-yaml-include-support/
def yaml_include(loader, node):
    with open(node.value) as inputfile:
        return list(my_safe_load(inputfile, master=loader).values())[0]
#              leave out the [0] if your include file drops the key ^^^

yaml.add_constructor("!include", yaml_include, Loader=yaml.SafeLoader)


def my_safe_load(stream, Loader=yaml.SafeLoader, master=None):
    loader = Loader(stream)
    if master is not None:
        loader.anchors = master.anchors
    try:
        return loader.get_single_data()
    finally:
        loader.dispose()

with open('warehouse.yaml') as fp:
    data = my_safe_load(fp)
yaml.safe_dump(data, sys.stdout, default_flow_style=False)

给出:

specific:
  spec1:
    key1: 1
    key2: 2
  spec2:
    key1: 10
    key2: 2
warehouse:
  obj1:
    key1: 1
    key2: 2

如果您的 specific.yaml 没有顶级密钥 specific:

spec1:
  <<: *obj1
spec2:
  <<: *obj1
  key1: 10

然后将yaml_include()的最后一行替换为:

return my_safe_load(inputfile, master=loader)

以上是使用 ruamel.yaml 完成的(免责声明:我是该软件包的作者)并在 Python 2.7 和 3.6 上进行了测试。通过更改导入,它也可以与 PyYAML 一起使用。


使用新的 ruamel.yaml API 可以大大简化上述内容,因为传递给 yaml_include() 构造函数的 loader 知道 YAML 实例,但当然你仍然需要一个不会破坏锚点的改编 compose_document 。假设 specific.yaml 没有 顶级键 specific,下面给出与之前相同的输出。

import sys
from ruamel.std.pathlib import Path
from ruamel.yaml import YAML, version_info

yaml = YAML(typ='safe', pure=True)
yaml.default_flow_style = False


def my_compose_document(self):
    self.parser.get_event()
    node = self.compose_node(None, None)
    self.parser.get_event()
    # self.anchors = {}    # <<<< commented out
    return node

yaml.Composer.compose_document = my_compose_document

# adapted from http://code.activestate.com/recipes/577613-yaml-include-support/
def yaml_include(loader, node):
    y = loader.loader
    yaml = YAML(typ=y.typ, pure=y.pure)  # same values as including YAML
    yaml.composer.anchors = loader.composer.anchors
    return yaml.load(Path(node.value))

yaml.Constructor.add_constructor("!include", yaml_include)

data = yaml.load(Path('warehouse.yaml'))
yaml.dump(data, sys.stdout)

现在好像有人解决了这个问题作为ruamel.yaml的扩展。

pip install ruamel.yaml.include (source on GitHub)

要获得上面所需的输出:

warehouse.yml

obj1: &obj1
  key1: 1
  key2: 2

specific.yml

specific:
  spec1: 
    <<: *obj1
  spec2:
    <<: *obj1
    key1: 10

您的代码将是:

from ccorp.ruamel.yaml.include import YAML

yaml = YAML(typ='safe', pure=True)
yaml.allow_duplicate_keys = True

with open('specific.yml', 'r') as ymlfile:
    return yaml.load(ymlfile)

如果您不想在输出中包含仓库密钥,它还包括一个方便的 !exclude 函数。如果您只想要特定的密钥,您的 specific.yml 可以以:

开头
!exclude includes:
- !include warehouse.yml

在这种情况下,您的 warehouse.yml 还可以包含顶级 warehouse: 密钥。