PyYaml“包含文件”和yaml别名(锚点/引用)

时间:2017-07-04 16:45:38

标签: yaml pyyaml cross-reference

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

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

文件太大,所以我找了一个解决方案,允许我分成2个文件:warehouse.yamlspecific.yaml,并在{{1}中包含warehouse.yaml }}。我读了this simple article,它描述了我如何使用PyYAML来实现这一点,但它也说不支持合并键(&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;

我真的遇到了错误:

  

yaml.composer.ComposerError:找到未定义的别名'obj1

当我试图这样做时。

所以,我开始寻找替代方法,我感到很困惑,因为我对PyYAML并不是很了解。

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

2 个答案:

答案 0 :(得分:3)

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

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的作曲家会更早地遇到未定义的别名而不是标记{{ 1}}用于构造(并填充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可以大大简化上述内容,因为传递给loader构造函数的yaml_include()知道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)

答案 1 :(得分:0)

似乎有人已经解决了这个问题,作为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:键。