我有两个YAML文件,“A”和“B”,我希望将A的内容插入到B中,要么拼接到现有的数据结构中,就像数组一样,要么作为元素的子元素,就像某个散列键的值一样。

这可能吗?怎么做?如果不是,是否有指向规范引用的指针?


当前回答

结合其他答案,这里是一个简短的解决方案,没有重载Loader类,它可以与任何加载器操作文件:

import json
from pathlib import Path
from typing import Any

import yaml


def yaml_include_constructor(loader: yaml.BaseLoader, node: yaml.Node) -> Any:
    """Include file referenced with !include node"""

    # noinspection PyTypeChecker
    fp = Path(loader.name).parent.joinpath(loader.construct_scalar(node)).resolve()
    fe = fp.suffix.lstrip(".")

    with open(fp, 'r') as f:
        if fe in ("yaml", "yml"):
            return yaml.load(f, type(loader))
        elif fe in ("json", "jsn"):
            return json.load(f)
        else:
            return f.read()


def main():
    loader = yaml.SafeLoader  # Works with any loader
    loader.add_constructor("!include", yaml_include_constructor)

    with open(...) as f:
        yml = yaml.load(f, loader)

PyTypeChecker的存在是为了防止pep检查警告预期类型'ScalarNode',得到'节点'而不是通过节点:yaml。节点到loader.construct_scalar()。

如果yaml。加载输入流不是文件流,因为loader.name在这种情况下不包含路径:

class Reader(object):
    ...
    def __init__(self, stream):
        ...
        if isinstance(stream, str):
            self.name = "<unicode string>"
            ...
        elif isinstance(stream, bytes):
            self.name = "<byte string>"
            ...
        else:
            self.name = getattr(stream, 'name', "<file>")
            ...

在我的用例中,我知道只包含YAML文件,所以解决方案可以进一步简化:

def yaml_include_constructor(loader: yaml.Loader, node: yaml.Node) -> Any:
    """Include YAML file referenced with !include node"""
    with open(Path(loader.name).parent.joinpath(loader.construct_yaml_str(node)).resolve(), 'r') as f:
        return yaml.load(f, type(loader))


Loader = yaml.SafeLoader  # Works with any loader
Loader.add_constructor("!include", yaml_include_constructor)


def main():
    with open(...) as f:
        yml = yaml.load(f, Loader=Loader)

甚至是使用lambda的一行代码:

Loader = yaml.SafeLoader  # Works with any loader
Loader.add_constructor("!include",
                       lambda l, n: yaml.load(Path(l.name).parent.joinpath(l.construct_scalar(n)).read_text(), type(l)))

其他回答

可能在问问题时不支持,但你可以将其他YAML文件导入其中:

imports: [/your_location_to_yaml_file/Util.area.yaml]

虽然我没有任何在线参考资料,但这对我来说很有用。

加上上面@Joshbode的初始回答,我对代码片段进行了一些修改,以支持UNIX风格的通配符模式。

不过我还没有在windows中进行测试。为了便于维护,我面临着将大型yaml中的数组拆分到多个文件中的问题,并正在寻找一种解决方案,以便在基本yaml的同一个数组中引用多个文件。因此,下面的解决方案。解决方案不支持递归引用。它只支持在基本yaml中引用的给定目录级别中的通配符。

import yaml
import os
import glob


# Base code taken from below link :-
# Ref:https://stackoverflow.com/a/9577670
class Loader(yaml.SafeLoader):

    def __init__(self, stream):

        self._root = os.path.split(stream.name)[0]

        super(Loader, self).__init__(stream)

    def include(self, node):
        consolidated_result = None
        filename = os.path.join(self._root, self.construct_scalar(node))

        # Below section is modified for supporting UNIX wildcard patterns
        filenames = glob.glob(filename)
        
        # Just to ensure the order of files considered are predictable 
        # and easy to debug in case of errors.
        filenames.sort()
        for file in filenames:
            with open(file, 'r') as f:
                result = yaml.load(f, Loader)

            if isinstance(result, list):
                if not isinstance(consolidated_result, list):
                    consolidated_result = []
                consolidated_result += result
            elif isinstance(result, dict):
                if not isinstance(consolidated_result, dict):
                    consolidated_result = {}
                consolidated_result.update(result)
            else:
                consolidated_result = result

        return consolidated_result


Loader.add_constructor('!include', Loader.include)

使用

a:
  !include a.yaml

b:
  # All yamls included within b folder level will be consolidated
  !include b/*.yaml

使用Symfony,它对yaml的处理将间接地允许您嵌套yaml文件。诀窍在于使用参数选项。例如:

common.yml

parameters:
    yaml_to_repeat:
        option: "value"
        foo:
            - "bar"
            - "baz"

config.yml

imports:
    - { resource: common.yml }
whatever:
    thing: "%yaml_to_repeat%"
    other_thing: "%yaml_to_repeat%"

其结果将与:

whatever:
    thing:
        option: "value"
        foo:
            - "bar"
            - "baz"
    other_thing:
        option: "value"
        foo:
            - "bar"
            - "baz"

使用Yglu,你可以像这样导入其他文件:

A.yaml

foo: !? $import('B.yaml')

B.yaml

bar: Hello
$ yglu A.yaml
foo:
  bar: Hello

因为$import是一个函数,你也可以传递一个表达式作为参数:

  dep: !- b
  foo: !? $import($_.dep.toUpper() + '.yaml')

这将得到与上面相同的输出。

声明:我是Yglu的作者。

结合其他答案,这里是一个简短的解决方案,没有重载Loader类,它可以与任何加载器操作文件:

import json
from pathlib import Path
from typing import Any

import yaml


def yaml_include_constructor(loader: yaml.BaseLoader, node: yaml.Node) -> Any:
    """Include file referenced with !include node"""

    # noinspection PyTypeChecker
    fp = Path(loader.name).parent.joinpath(loader.construct_scalar(node)).resolve()
    fe = fp.suffix.lstrip(".")

    with open(fp, 'r') as f:
        if fe in ("yaml", "yml"):
            return yaml.load(f, type(loader))
        elif fe in ("json", "jsn"):
            return json.load(f)
        else:
            return f.read()


def main():
    loader = yaml.SafeLoader  # Works with any loader
    loader.add_constructor("!include", yaml_include_constructor)

    with open(...) as f:
        yml = yaml.load(f, loader)

PyTypeChecker的存在是为了防止pep检查警告预期类型'ScalarNode',得到'节点'而不是通过节点:yaml。节点到loader.construct_scalar()。

如果yaml。加载输入流不是文件流,因为loader.name在这种情况下不包含路径:

class Reader(object):
    ...
    def __init__(self, stream):
        ...
        if isinstance(stream, str):
            self.name = "<unicode string>"
            ...
        elif isinstance(stream, bytes):
            self.name = "<byte string>"
            ...
        else:
            self.name = getattr(stream, 'name', "<file>")
            ...

在我的用例中,我知道只包含YAML文件,所以解决方案可以进一步简化:

def yaml_include_constructor(loader: yaml.Loader, node: yaml.Node) -> Any:
    """Include YAML file referenced with !include node"""
    with open(Path(loader.name).parent.joinpath(loader.construct_yaml_str(node)).resolve(), 'r') as f:
        return yaml.load(f, type(loader))


Loader = yaml.SafeLoader  # Works with any loader
Loader.add_constructor("!include", yaml_include_constructor)


def main():
    with open(...) as f:
        yml = yaml.load(f, Loader=Loader)

甚至是使用lambda的一行代码:

Loader = yaml.SafeLoader  # Works with any loader
Loader.add_constructor("!include",
                       lambda l, n: yaml.load(Path(l.name).parent.joinpath(l.construct_scalar(n)).read_text(), type(l)))