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

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


当前回答

对于Python用户,可以尝试pyyaml-include。

安装

pip install pyyaml-include

使用

import yaml
from yamlinclude import YamlIncludeConstructor

YamlIncludeConstructor.add_to_loader_class(loader_class=yaml.FullLoader, base_dir='/your/conf/dir')

with open('0.yaml') as f:
    data = yaml.load(f, Loader=yaml.FullLoader)

print(data)

假设我们有这样的YAML文件:

├── 0.yaml
└── include.d
    ├── 1.yaml
    └── 2.yaml

1.Yaml的内容:

name: "1"

2.Yaml的内容:

name: "2"

按名称包含文件

顶层: 如果0。yaml是:

!include include.d/1.yaml

我们会得到:

{"name": "1"}

在映射: 如果0。yaml是:

file1: !include include.d/1.yaml
file2: !include include.d/2.yaml

我们会得到:

  file1:
    name: "1"
  file2:
    name: "2"

在序列: 如果0。yaml是:

files:
  - !include include.d/1.yaml
  - !include include.d/2.yaml

我们会得到:

files:
  - name: "1"
  - name: "2"

ℹ注意: 文件名可以是绝对的(如/usr/conf/1.5/ make .yml)或相对的(如../../cfg/img.yml)。

通过通配符包含文件

文件名可以包含shell样式的通配符。从通配符找到的文件中加载的数据将按顺序设置。

如果0。yaml是:

files: !include include.d/*.yaml

我们会得到:

files:
  - name: "1"
  - name: "2"

ℹ注意: 对于Python>=3.5,如果递归参数!include YAML标记为真,模式" ** "将匹配任何文件和零个或多个目录和子目录。 在大型目录树中使用“**”模式可能会因为递归搜索而消耗过多的时间。

为了启用递归参数,我们应该在映射或序列模式下编写!include标记:

序列模式参数:

!include [tests/data/include.d/**/*.yaml, true]

映射模式参数说明

!include {pathname: tests/data/include.d/**/*.yaml, recursive: true}

其他回答

我举了一些例子供你参考。

import yaml

main_yaml = """
Package:
 - !include _shape_yaml    
 - !include _path_yaml
"""

_shape_yaml = """
# Define
Rectangle: &id_Rectangle
    name: Rectangle
    width: &Rectangle_width 20
    height: &Rectangle_height 10
    area: !product [*Rectangle_width, *Rectangle_height]

Circle: &id_Circle
    name: Circle
    radius: &Circle_radius 5
    area: !product [*Circle_radius, *Circle_radius, pi]

# Setting
Shape:
    property: *id_Rectangle
    color: red
"""

_path_yaml = """
# Define
Root: &BASE /path/src/

Paths: 
    a: &id_path_a !join [*BASE, a]
    b: &id_path_b !join [*BASE, b]

# Setting
Path:
    input_file: *id_path_a
"""


# define custom tag handler
def yaml_import(loader, node):
    other_yaml_file = loader.construct_scalar(node)
    return yaml.load(eval(other_yaml_file), Loader=yaml.SafeLoader)


def yaml_product(loader, node):
    import math
    list_data = loader.construct_sequence(node)
    result = 1
    pi = math.pi
    for val in list_data:
        result *= eval(val) if isinstance(val, str) else val
    return result


def yaml_join(loader, node):
    seq = loader.construct_sequence(node)
    return ''.join([str(i) for i in seq])


def yaml_ref(loader, node):
    ref = loader.construct_sequence(node)
    return ref[0]


def yaml_dict_ref(loader: yaml.loader.SafeLoader, node):
    dict_data, key, const_value = loader.construct_sequence(node)
    return dict_data[key] + str(const_value)


def main():
    # register the tag handler
    yaml.SafeLoader.add_constructor(tag='!include', constructor=yaml_import)
    yaml.SafeLoader.add_constructor(tag='!product', constructor=yaml_product)
    yaml.SafeLoader.add_constructor(tag='!join', constructor=yaml_join)
    yaml.SafeLoader.add_constructor(tag='!ref', constructor=yaml_ref)
    yaml.SafeLoader.add_constructor(tag='!dict_ref', constructor=yaml_dict_ref)

    config = yaml.load(main_yaml, Loader=yaml.SafeLoader)

    pk_shape, pk_path = config['Package']
    pk_shape, pk_path = pk_shape['Shape'], pk_path['Path']
    print(f"shape name: {pk_shape['property']['name']}")
    print(f"shape area: {pk_shape['property']['area']}")
    print(f"shape color: {pk_shape['color']}")

    print(f"input file: {pk_path['input_file']}")


if __name__ == '__main__':
    main()

输出

shape name: Rectangle
shape area: 200
shape color: red
input file: /path/src/a

更新2

你可以把它们结合起来,像这样

# xxx.yaml
CREATE_FONT_PICTURE:
  PROJECTS:
    SUNG: &id_SUNG
      name: SUNG
      work_dir: SUNG
      output_dir: temp
      font_pixel: 24


  DEFINE: &id_define !ref [*id_SUNG]  # you can use config['CREATE_FONT_PICTURE']['DEFINE'][name, work_dir, ... font_pixel]
  AUTO_INIT:
    basename_suffix: !dict_ref [*id_define, name, !product [5, 3, 2]]  # SUNG30

# ↓ This is not correct.
# basename_suffix: !dict_ref [*id_define, name, !product [5, 3, 2]]  # It will build by Deep-level. id_define is Deep-level: 2. So you must put it after 2. otherwise, it can't refer to the correct value.

结合其他答案,这里是一个简短的解决方案,没有重载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不包括任何类型的“import”或“include”语句。

我认为@max - b使用的解决方案看起来很棒。但是,对于嵌套的包含,它没有成功。例如,如果config_1。Yaml包含config_2。Yaml,其中包括config_3。Yaml,装弹机有问题。但是,如果您在加载时简单地将新的加载器类指向它自己,它就可以工作!具体来说,如果我们将旧的_include函数替换为稍微修改过的版本:

def _include(self, loader, node):                                    
     oldRoot = self.root                                              
     filename = os.path.join(self.root, loader.construct_scalar(node))
     self.root = os.path.dirname(filename)                           
     data = yaml.load(open(filename, 'r'), loader = IncludeLoader)                            
     self.root = oldRoot                                              
     return data

经过反思,我同意其他评论,嵌套加载一般不适合yaml,因为输入流可能不是一个文件,但它非常有用!

不幸的是,YAML在其标准中没有提供这个功能。

但是如果你正在使用Ruby,有一个gem通过扩展Ruby YAML库来提供你所要求的功能: https://github.com/entwanderer/yaml_extend