如何在另一个文件中包含YAML文件?

时间:2009-02-09 14:14:05

标签: yaml transclusion

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

这有可能吗?怎么样?如果没有,任何指向规范参考的指针?

15 个答案:

答案 0 :(得分:263)

不,YAML不包含任何类型的“导入”或“包含”声明。

答案 1 :(得分:90)

您的问题不是要求使用Python解决方案,而是使用PyYAML

PyYAML允许您将自定义构造函数(例如!include)附加到YAML加载程序。我已经包含了一个可以设置的根目录,以便该解决方案支持相对和绝对文件引用。

基于类的解决方案

这是一个基于类的解决方案,它避免了原始响应的全局根变量。

有关使用元类注册自定义构造函数的类似,更强大的Python 3解决方案,请参阅此gist

import yaml
import os

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):

        filename = os.path.join(self._root, self.construct_scalar(node))

        with open(filename, 'r') as f:
            return yaml.load(f, Loader)

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

一个例子:

<强> foo.yaml

a: 1
b:
    - 1.43
    - 543.55
c: !include bar.yaml

<强> bar.yaml

- 3.6
- [1, 2, 3]

现在可以使用以下方式加载文件:

>>> with open('foo.yaml', 'r') as f:
>>>    data = yaml.load(f, Loader)
>>> data
{'a': 1, 'b': [1.43, 543.55], 'c': [3.6, [1, 2, 3]]}

答案 2 :(得分:25)

如果你正在使用Symfony's version of YAML,这是可能的,如下所示:

imports:
    - { resource: sub-directory/file.yml }
    - { resource: sub-directory/another-file.yml }

答案 3 :(得分:10)

据我所知,YAML不直接支持包含,但您必须自己提供一种机制,这通常很容易。

我在我的python应用程序中使用YAML作为配置语言,在这种情况下通常会定义这样的约定:

>>> main.yml <<<
includes: [ wibble.yml, wobble.yml]

然后在我的(python)代码中我做:

import yaml
cfg = yaml.load(open("main.yml"))
for inc in cfg.get("includes", []):
   cfg.update(yaml.load(open(inc)))

唯一的缺点是包含中的变量将始终覆盖main中的变量,并且无法通过更改“includes:”语句出现在main.yml文件中的位置来更改该优先级。

稍微不同一点,YAML不支持包含,因为它的设计并不像基于文件的标记那样完全。如果你在回答一个AJAX请求时得到了什么意思?

答案 4 :(得分:7)

扩展@Jos​​h_Bode的答案,这是我自己的PyYAML解决方案,它的优势在于它是yaml.Loader的自包含子类。它不依赖于任何模块级全局变量,也不依赖于修改yaml模块的全局状态。

import yaml, os

class IncludeLoader(yaml.Loader):                                                 
    """                                                                           
    yaml.Loader subclass handles "!include path/to/foo.yml" directives in config  
    files.  When constructed with a file object, the root path for includes       
    defaults to the directory containing the file, otherwise to the current       
    working directory. In either case, the root path can be overridden by the     
    `root` keyword argument.                                                      

    When an included file F contain its own !include directive, the path is       
    relative to F's location.                                                     

    Example:                                                                      
        YAML file /home/frodo/one-ring.yml:                                       
            ---                                                                   
            Name: The One Ring                                                    
            Specials:                                                             
                - resize-to-wearer                                                
            Effects: 
                - !include path/to/invisibility.yml                            

        YAML file /home/frodo/path/to/invisibility.yml:                           
            ---                                                                   
            Name: invisibility                                                    
            Message: Suddenly you disappear!                                      

        Loading:                                                                  
            data = IncludeLoader(open('/home/frodo/one-ring.yml', 'r')).get_data()

        Result:                                                                   
            {'Effects': [{'Message': 'Suddenly you disappear!', 'Name':            
                'invisibility'}], 'Name': 'The One Ring', 'Specials':              
                ['resize-to-wearer']}                                             
    """                                                                           
    def __init__(self, *args, **kwargs):                                          
        super(IncludeLoader, self).__init__(*args, **kwargs)                      
        self.add_constructor('!include', self._include)                           
        if 'root' in kwargs:                                                      
            self.root = kwargs['root']                                            
        elif isinstance(self.stream, file):                                       
            self.root = os.path.dirname(self.stream.name)                         
        else:                                                                     
            self.root = os.path.curdir                                            

    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'))                            
        self.root = oldRoot                                              
        return data                                                      

答案 5 :(得分:1)

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

但是如果您使用的是Ruby,那么有一个gem可以通过扩展ruby YAML库来提供您要求的功能: https://github.com/entwanderer/yaml_extend

答案 6 :(得分:1)

对于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,如果recursive YAML标签的!include自变量是true,则模式“**”将匹配任何文件,并且匹配零或更多目录和子目录。
  •   
  • 在大型目录树中使用“**”模式可能会由于递归搜索而浪费大量时间。
  •   

为了启用recursive参数,我们将以!includeMapping模式编写Sequence标签:

  • 处于Sequence模式的参数:
!include [tests/data/include.d/**/*.yaml, true]
  • 处于Mapping模式的参数:
!include {pathname: tests/data/include.d/**/*.yaml, recursive: true}

答案 7 :(得分:1)

YML标准未指定执行此操作的方法。而且这个问题并不局限于YML。 JSON具有相同的局限性。

许多使用基于YML或JSON的配置的应用程序最终都会遇到此问题。发生这种情况时,他们会制定自己的约定

例如大量API定义:

$ref: 'file.yml'

例如对于docker撰写配置:

services:
  app:
    extends:
      file: docker-compose.base.yml

或者,如果要将yml文件的内容分成多个文件(如目录树),则可以定义自己的文件夹结构约定并使用(现有)合并脚本。

答案 8 :(得分:1)

我举一些例子供您参考。

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.

答案 9 :(得分:1)

使用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的作者。

答案 10 :(得分:0)

也许这可以激发您的灵感,请尝试与jbb约定保持一致:

https://docs.openstack.org/infra/jenkins-job-builder/definition.html#inclusion-tags

- job: name: test-job-include-raw-1 builders: - shell: !include-raw: include-raw001-hello-world.sh

答案 11 :(得分:0)

标准YAML 1.2本身不包含此功能。尽管如此,许多实现还是为此提供了一些扩展。

我提出了一种使用Java和snakeyaml:1.24(用于解析/发送YAML文件的Java库)实现此目标的方法,该方法允许创建自定义YAML标签来实现以下目标(您将看到我正在使用它来实现以下目的)加载了在多个YAML文件中定义的测试套件,我将其用作目标test:节点的包含列表):

# ... yaml prev stuff

tests: !include
  - '1.hello-test-suite.yaml'
  - '3.foo-test-suite.yaml'
  - '2.bar-test-suite.yaml'

# ... more yaml document

这是一类Java,允许处理!include标记。文件是从类路径(Maven资源目录)中加载的:

/**
 * Custom YAML loader. It adds support to the custom !include tag which allows splitting a YAML file across several
 * files for a better organization of YAML tests.
 */
@Slf4j   // <-- This is a Lombok annotation to auto-generate logger
public class MyYamlLoader {

    private static final Constructor CUSTOM_CONSTRUCTOR = new MyYamlConstructor();

    private MyYamlLoader() {
    }

    /**
     * Parse the only YAML document in a stream and produce the Java Map. It provides support for the custom !include
     * YAML tag to split YAML contents across several files.
     */
    public static Map<String, Object> load(InputStream inputStream) {
        return new Yaml(CUSTOM_CONSTRUCTOR)
                .load(inputStream);
    }


    /**
     * Custom SnakeYAML constructor that registers custom tags.
     */
    private static class MyYamlConstructor extends Constructor {

        private static final String TAG_INCLUDE = "!include";

        MyYamlConstructor() {
            // Register custom tags
            yamlConstructors.put(new Tag(TAG_INCLUDE), new IncludeConstruct());
        }

        /**
         * The actual include tag construct.
         */
        private static class IncludeConstruct implements Construct {

            @Override
            public Object construct(Node node) {
                List<Node> inclusions = castToSequenceNode(node);
                return parseInclusions(inclusions);
            }

            @Override
            public void construct2ndStep(Node node, Object object) {
                // do nothing
            }

            private List<Node> castToSequenceNode(Node node) {
                try {
                    return ((SequenceNode) node).getValue();

                } catch (ClassCastException e) {
                    throw new IllegalArgumentException(String.format("The !import value must be a sequence node, but " +
                            "'%s' found.", node));
                }
            }

            private Object parseInclusions(List<Node> inclusions) {

                List<InputStream> inputStreams = inputStreams(inclusions);

                try (final SequenceInputStream sequencedInputStream =
                             new SequenceInputStream(Collections.enumeration(inputStreams))) {

                    return new Yaml(CUSTOM_CONSTRUCTOR)
                            .load(sequencedInputStream);

                } catch (IOException e) {
                    log.error("Error closing the stream.", e);
                    return null;
                }
            }

            private List<InputStream> inputStreams(List<Node> scalarNodes) {
                return scalarNodes.stream()
                        .map(this::inputStream)
                        .collect(toList());
            }

            private InputStream inputStream(Node scalarNode) {
                String filePath = castToScalarNode(scalarNode).getValue();
                final InputStream is = getClass().getClassLoader().getResourceAsStream(filePath);
                Assert.notNull(is, String.format("Resource file %s not found.", filePath));
                return is;
            }

            private ScalarNode castToScalarNode(Node scalarNode) {
                try {
                    return ((ScalarNode) scalarNode);

                } catch (ClassCastException e) {
                    throw new IllegalArgumentException(String.format("The value must be a scalar node, but '%s' found" +
                            ".", scalarNode));
                }
            }
        }

    }

}

答案 12 :(得分:0)

添加上面@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

答案 13 :(得分:-1)

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

<强> 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"

答案 14 :(得分:-5)

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

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

虽然我没有在线参考,但这对我有用。