【问题标题】:How can I include a YAML file inside another?如何在另一个文件中包含 YAML 文件?
【发布时间】:2010-10-06 09:50:41
【问题描述】:

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

这可能吗?如何?如果没有,是否有任何指向规范性参考的指针?

【问题讨论】:

标签: yaml transclusion


【解决方案1】:

不,YAML 不包含任何类型的“import”或“include”语句。

【讨论】:

  • 你可以创建一个 !include 处理程序。
  • @clarkevans 当然,但该结构将在 YAML 语言“之外”。
  • 现在可以实现了。我在下面添加了一个答案...希望对您有所帮助。
  • 如果你使用 Rails,你可以插入 ERB 语法,它会工作
  • 我认为这个答案应该改写为“不,标准 YAML 不包含此功能。尽管如此,许多实现都提供了一些扩展来做到这一点。”
【解决方案2】:

您的问题不要求 Python 解决方案,但这里是使用 PyYAML 的问题。

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

基于类的解决方案

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

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

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]]}

【讨论】:

  • 这是一个有趣的功能,thanx。但是使用 root/old_root 进行所有这些操作的目的是什么?我想include函数的代码可以简化:` def include(loader, node): """Include another YAML file.""" filename = loader.construct_scalar(node) data = yaml.load(open(filename) ) `
  • 根全局在那里,因此相对包括任何深度的工作,例如当包含在不同目录中的文件包含相对于该目录的文件时。绝对包含也应该起作用。可能有一种更简洁的方法可以在没有全局变量的情况下执行此操作,可能使用自定义 yaml.Loader 类。
  • 是否也可以有这样的东西: foo.yaml: a: bla bar.yaml: ` !include foo.yaml b: blubb` 所以结果会是:`{'a ': bla, 'b': blubb}
  • 这应该是公认的答案。此外,作为安全专家,您应该使用 yaml.safeload 而不是 yaml.load,以避免特制的 yaml 拥有您的服务。
【解决方案3】:

据我所知,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)))

唯一的缺点是 include 中的变量总是会覆盖 main 中的变量,并且无法通过更改 "includes: 语句在 main.yml 文件中出现的位置来更改该优先级。

在稍微不同的一点上,YAML 不支持包含,因为它的设计并没有像基于文件的标记那样专门设计。如果您在对 AJAX 请求的响应中得到它,那么包含意味着什么?

【讨论】:

  • 这仅在 yaml 文件不包含嵌套配置时有效。
【解决方案4】:

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&gt;=3.5,如果!include YAML 标记的recursive 参数是true,则“**” 模式将匹配任何文件以及零个或多个目录和子目录。
  • 在大型目录树中使用 “**” 模式可能会因为递归搜索而消耗过多的时间。

为了启用recursive参数,我们将!include标签写入MappingSequence模式:

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

【讨论】:

  • 这实际上并没有回答问题。它与 Python 解决方案有关,而不是使用标准化 YAML 格式的解决方案。
  • @oligofren 自定义标签处理程序是 YAML 的一项功能,允许解析器扩展 YAML 以指定类型并实现此类自定义行为。 YAML 规范本身要规定文件包含应如何与所有不同的操作系统路径规范、文件系统等一起工作,这将是一个漫长的过程。
  • @AntonStrogonoff 感谢您引起我的注意。你能指出我在 RFC 中的这样一个地方吗?它没有提到“习惯”这个词。参考yaml.org/spec/1.2/spec.html
  • @oligofren 不客气。查找“特定于应用程序”tags
【解决方案5】:

扩展@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                                                      

【讨论】:

  • 终于开始在我的答案中添加基于类的方法,但你打败了我 :) 注意:如果你在 _include 中使用 yaml.load(f, IncludeLoader),你可以避免更换根。此外,除非您这样做,否则解决方案的工作深度不会超过一层,因为包含的数据使用常规的 yaml.Loader 类。
  • 在设置self.root 之后,我必须删除root 的关键字self.root 才能使用字符串。我将 if-else 块移到了 super 调用上方。也许其他人可以确认我的发现或向我展示如何使用带有字符串和 root 参数的类。
  • 不幸的是,这不适用于诸如 ``` 之类的引用,包括:&INCLUDED !include inner.yaml 合并:
【解决方案6】:

YML 标准没有指定执行此操作的方法。而且这个问题并不局限于 YML。 JSON 也有同样的限制。

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

例如对于 swagger API 定义:

$ref: 'file.yml'

例如对于 docker compose 配置:

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

或者,如果您想将 yml 文件的内容拆分为多个文件,例如内容树,您可以定义自己的文件夹结构约定并使用(现有)合并脚本。

【讨论】:

  • 这应该更高。大多数情况下,如果您需要将 YAML 导入另一个 YAML,那是因为来自特定框架的一些配置文件,并且始终值得研究框架本身是否提供了一种无需重新发明轮子的方法。
【解决方案7】:

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

【讨论】:

    【解决方案8】:

    标准 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
    

    这是允许处理!include 标记的一类Java。文件从类路径(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));
                    }
                }
            }
    
        }
    
    }
    

    【讨论】:

    • 适用于简单的情况;不幸的是,引用没有从包含的文件中继承。
    • 您好! “参考”是什么意思?你的意思是传递!includes?如果这就是你的意思,我没有考虑过。但我猜想可以通过递归调用load() 直到没有!includes 来为解决方案添加支持。有意义吗?
    • 似乎我没有使用正确的术语:锚和别名 (bitbucket.org/asomov/snakeyaml/wiki/…) 是行不通的。查看snakeyaml v1 源代码,很难添加。 Mabye v2(又名 snakeyaml 引擎)更加模块化......
    【解决方案9】:

    很遗憾,YAML 在其标准中没有提供这一点。

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

    【讨论】:

      【解决方案10】:

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

      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.
      

      【讨论】:

        【解决方案11】:

        我认为@maxy-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
        

        经过反思,我同意其他 cmets,嵌套加载通常不适用于 yaml,因为输入流可能不是文件,但它非常有用!

        【讨论】:

          【解决方案12】:

          使用 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"
          

          【讨论】:

            【解决方案13】:

            也许这可以启发你,尝试与 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

            【讨论】:

              【解决方案14】:

              在上面@Joshbode 的初始答案的基础上,我稍微修改了sn-p 以支持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
              
              

              【讨论】:

                【解决方案15】:

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

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

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

                【讨论】:

                • 这根本不做任何事情。它创建一个包含单个字符串“/your_location_to_yaml_file/Util.area.yaml”的序列的映射,作为键imports的值。
                猜你喜欢
                • 2017-05-19
                • 2011-01-17
                • 2012-01-28
                • 1970-01-01
                • 2010-10-31
                • 2010-11-26
                相关资源
                最近更新 更多