【问题标题】:How to merge YAML arrays?如何合并 YAML 数组?
【发布时间】:2014-07-28 05:11:51
【问题描述】:

我想在 YAML 中合并数组,并通过 ruby​​ 加载它们 -

some_stuff: &some_stuff
 - a
 - b
 - c

combined_stuff:
  <<: *some_stuff
  - d
  - e
  - f

我想将组合数组设为[a,b,c,d,e,f]

我收到错误:解析块映射时未找到预期的键

如何在 YAML 中合并数组?

【问题讨论】:

  • 在一个非常大的 yaml 文件中干涸复制
  • 如何尝试干坏事?
  • @PatrickCollins 我发现这个问题试图减少我的.gitlab-ci.yml 文件中的重复,不幸的是我无法控制 GitLab CI 使用的解析器:(
  • 作为一种后备,如果应用程序代码也由您维护,则可以在那里递归地合并它们,类似于我所做的 here

标签: list data-structures yaml


【解决方案1】:

这是行不通的:

  1. 合并仅受 YAML 规范的映射支持,不支持序列

  2. 您通过合并键 &lt;&lt; 完全混合了事物 后跟键/值分隔符: 和一个值 参考 ,然后在同一缩进处继续列表 等级

这不是正确的 YAML:

combine_stuff:
  x: 1
  - a
  - b

所以你的示例语法作为 YAML 扩展提案甚至没有意义。

如果您想执行合并多个数组之类的操作,您可能需要考虑如下语法:

combined_stuff:
  - <<: *s1, *s2
  - <<: *s3
  - d
  - e
  - f

其中s1s2s3 是序列上的锚点(未显示),您 想要合并成一个新的序列,然后有def 附加到那个。但是 YAML 正在解决这种结构深度 首先,因此在处理过程中没有可用的真实上下文 的合并键。您在哪里没有可用的数组/列表 可以将处理后的值(锚定序列)附加到。

您可以采用@dreftymac 提出的方法,但这有一个巨大的缺点,即您 不知何故需要知道要展平哪些嵌套序列(即通过知道根的“路径” 加载的数据结构到父序列),或者您递归地遍历加载的 搜索嵌套数组/列表的数据结构,并不加选择地将它们全部展平。

IMO 更好的解决方案是使用标签来加载数据结构 为你做扁平化。这允许清楚地表示什么 需要展平,什么不需要,让你完全控制 这种展平是在加载期间完成还是在加载期间完成 使用权。选择哪一个是易于实施和 时间和存储空间的效率。这是同样需要做出的权衡 用于实施merge key feature 和 没有单一的解决方案永远是最好的。

例如我的ruamel.yaml 库在 使用其安全加载器时加载,导致合并 是普通 Python 字典的字典。必须完成此合并 前期,并复制数据(空间效率低下),但价值很快 抬头。使用往返加载程序时,您希望能够转储 合并未合并,因此需要将它们分开。字典喜欢 由于往返加载而加载的数据结构是空间 高效但访问速度较慢,因为它需要尝试查找密钥 在合并中的字典本身中找不到(并且没有缓存,所以 每次都需要这样做)。当然这样的考虑是 对于相对较小的配置文件不是很重要。


以下使用带有标签flatten的对象在python中为列表实现类似合并的方案 它即时递归到列表项并标记为toflatten。使用这两个标签 你可以有 YAML 文件:

l1: &x1 !toflatten
  - 1 
  - 2
l2: &x2
  - 3 
  - 4
m1: !flatten
  - *x1
  - *x2
  - [5, 6]
  - !toflatten [7, 8]

(使用流与块样式序列完全是任意的,对 加载结果)。

当迭代作为键 m1 的值的项目时 “递归”到标记有toflatten 的序列中,但显示 其他列表(无论是否有别名)作为单个项目。

使用 Python 代码实现这一目标的一种可能方法是:

import sys
from pathlib import Path
import ruamel.yaml

yaml = ruamel.yaml.YAML()


@yaml.register_class
class Flatten(list):
   yaml_tag = u'!flatten'
   def __init__(self, *args):
      self.items = args

   @classmethod
   def from_yaml(cls, constructor, node):
       x = cls(*constructor.construct_sequence(node, deep=True))
       return x

   def __iter__(self):
       for item in self.items:
           if isinstance(item, ToFlatten):
               for nested_item in item:
                   yield nested_item
           else:
               yield item


@yaml.register_class
class ToFlatten(list):
   yaml_tag = u'!toflatten'

   @classmethod
   def from_yaml(cls, constructor, node):
       x = cls(constructor.construct_sequence(node, deep=True))
       return x



data = yaml.load(Path('input.yaml'))
for item in data['m1']:
    print(item)

哪个输出:

1
2
[3, 4]
[5, 6]
7
8

如您所见,在需要展平的序列中,您 可以使用标记序列的别名,也可以使用标记序列 顺序。 YAML 不允许您这样做:

- !flatten *x2

,即标记一个 锚定序列,因为这本质上会使它变成一个不同的 数据结构。

使用 explicit 标签比使用一些魔法更好 使用 YAML 合并键 &lt;&lt;。如果没有别的,你现在必须经历 如果您碰巧有一个 YAML 文件,该文件的映射具有键 &lt;&lt; 你不想表现得像一个合并键,例如当你做一个 将 C 运算符映射到英语(或其他一些自然语言)的描述。

【讨论】:

    【解决方案2】:

    在python中启用合并数组的另一种方法是定义!flatten标签。 (这使用 PyYAML,与 Anthon 上面的答案不同。如果您无法控制在支持中使用哪个包,例如 anyconfig,这可能是必要的。

    import yaml
    
    yaml.add_constructor("!flatten", construct_flat_list)
    
    def flatten_sequence(sequence: yaml.Node) -> Iterator[str]:
        """Flatten a nested sequence to a list of strings
            A nested structure is always a SequenceNode
        """
        if isinstance(sequence, yaml.ScalarNode):
            yield sequence.value
            return
        if not isinstance(sequence, yaml.SequenceNode):
            raise TypeError(f"'!flatten' can only flatten sequence nodes, not {sequence}")
        for el in sequence.value:
            if isinstance(el, yaml.SequenceNode):
                yield from flatten_sequence(el)
            elif isinstance(el, yaml.ScalarNode):
                yield el.value
            else:
                raise TypeError(f"'!flatten' can only take scalar nodes, not {el}")
    
    def construct_flat_list(loader: yaml.Loader, node: yaml.Node) -> List[str]:
        """Make a flat list, should be used with '!flatten'
    
        Args:
            loader: Unused, but necessary to pass to `yaml.add_constructor`
            node: The passed node to flatten
        """
        return list(flatten_sequence(node))
    

    这种递归展平利用了 PyYAML 文档结构,它将所有数组解析为SequenceNodes,并将所有值解析为ScalarNodes。 可以在以下测试函数中测试(和修改)行为。

    import pytest
    def test_flatten_yaml():
        # single nest
        param_string = """
        bread: &bread
          - toast
          - loafs
        chicken: &chicken
          - *bread
        midnight_meal: !flatten
          - *chicken
          - *bread
        """
        params = yaml.load(param_string)
        assert sorted(params["midnight_meal"]) == sorted(
            ["toast", "loafs", "toast", "loafs"]
        )
    

    【讨论】:

      【解决方案3】:

      如果目标是运行一系列 shell 命令,您可以通过以下方式实现:

      # note: no dash before commands
      some_stuff: &some_stuff |-
          a
          b
          c
      
      combined_stuff:
        - *some_stuff
        - d
        - e
        - f
      

      这相当于:

      some_stuff: "a\nb\nc"
      
      combined_stuff:
        - "a\nb\nc"
        - d
        - e
        - f
      

      我一直在我的gitlab-ci.yml 上使用它(回答@rink.attendant.6 对该问题的评论)。


      我们用来支持 requirements.txt 拥有来自 gitlab 的私有 repos 的工作示例:

      .pip_git: &pip_git
      - git config --global url."https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com".insteadOf "ssh://git@gitlab.com"
      - mkdir -p ~/.ssh
      - chmod 700 ~/.ssh
      - echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
      - chmod 644 ~/.ssh/known_hosts
      
      test:
          image: python:3.7.3
          stage: test
          script:
              - *pip_git
              - pip install -q -r requirements_test.txt
              - python -m unittest discover tests
      
      use the same `*pip_git` on e.g. build image...
      

      其中requirements_test.txt 包含例如

      -e git+ssh://git@gitlab.com/example/example.git@v0.2.2#egg=example

      【讨论】:

      • 聪明。我现在在我们的 Bitbucket 管道中使用它。谢谢
      • *这里不需要尾部破折号,只需要末尾的管道即可。 *这是一个较差的解决方案,因为当作业在很长的多行语句上失败时,不清楚哪个命令失败了。
      • @MinaLuke,与什么相比逊色?当前的答案都没有提供仅使用 yaml 合并两个项目的方法......此外,问题中没有任何内容表明 OP 希望在 CI/CD 中使用它。最后,当它在 CI/CD 中使用时,日志记录仅取决于所使用的特定 CI/CD,而不取决于 yaml 声明。因此,如果有的话,您所指的 CI/CD 就是做得不好的那个。此答案中的 yaml 是有效的,并且解决了 OP 的问题。
      • 它对我不起作用。 with - 我得到一个错误,就像它试图在列表项中插入列表一样。我不知道如何使用管道。这是为了什么? @Dariop 如何在 BB Pipelines 中使用它?
      • 我对此表示怀疑。如果我错了,请纠正我,但如果它被转换为"a\nb\nc",这意味着gitlab-runner 将没有机会检查ab 的退出代码。因此,如果序列中除最后一个命令之外的任何命令失败,这不会中断管道并且运行器将继续执行其余命令..
      【解决方案4】:

      更新:2019-07-01 14:06:12

      • 注意:此问题的另一个答案已使用update on alternative approaches 进行了大量编辑。
        • 更新的答案提到了此答案中解决方法的替代方法。它已添加到下面的另请参阅部分。

      上下文

      这篇文章假定以下上下文:

      • python 2.7
      • python YAML 解析器

      问题

      lfender6445 希望在一个 YAML 文件中合并两个或多个列表,并让那些 合并列表在解析时显示为一个单数列表。

      解决方案(解决方法)

      这可以简单地通过将 YAML 锚分配给映射来获得,其中 所需的列表显示为映射的子元素。但是,对此有一些警告(请参阅下文的“陷阱”)。

      在下面的示例中,我们有三个映射 (list_one, list_two, list_three) 和三个锚点 以及在适当时引用这些映射的别名。

      当 YAML 文件在程序中加载时,我们得到了我们想要的列表,但是 加载后可能需要稍作修改(请参阅下面的陷阱)。

      示例

      原始 YAML 文件

      list_one: &id001 - 一种 - b - C list_two: &id002 -e - F - G list_three: &id003 - H - 一世 -j list_combined: - *id001 - *id002 - *id003

      YAML.safe_load 后的结果

      ## list_combined [ [ “一种”, “乙”, “C” ], [ "e", “F”, “G” ], [ “H”, “一世”, “j” ] ]

      陷阱

      • 这种方法会生成一个嵌套的列表列表,这可能不是所需的确切输出,但可以使用 flatten 方法对其进行后处理
      • usual caveats to YAML anchors and aliases申请唯一性和声明顺序

      结论

      这种方法允许使用 YAML 的别名和锚点功能创建合并列表。

      虽然输出结果是列表的嵌套列表,但可以使用flatten 方法轻松转换。

      另见

      @Anthon 更新了替代方法

      flatten 方法的示例

      【讨论】:

        【解决方案5】:

        如果您只需要将一个项目合并到列表中,您可以这样做

        fruit:
          - &banana
            name: banana
            colour: yellow
        
        food:
          - *banana
          - name: carrot
            colour: orange
        

        产生

        fruit:
          - name: banana
            colour: yellow
        
        food:
          - name: banana
            colour: yellow
          - name: carrot
            colour: orange
        

        【讨论】:

          【解决方案6】:

          在这些条件下,您可以合并映射,然后将其键转换为列表:

          • 如果您使用 jinja2 模板和
          • 如果项目顺序不重要
          some_stuff: &some_stuff
           a:
           b:
           c:
          
          combined_stuff:
            <<: *some_stuff
            d:
            e:
            f:
          
          {{ combined_stuff | list }}
          

          【讨论】:

          • 这个答案有什么问题?如果有争议,我不介意投反对票。我会把答案留给可以使用它的人。
          • 可能是因为这个答案依赖于 jinja2 模板,当问题要求在 yml 中进行时。 jinja2 需要 Python 环境,如果 OP 试图 DRY,这会适得其反。此外,许多 CI/CD 工具不接受模板步骤。
          • 感谢@JorgeLeitao。这就说得通了。我在开发 Ansible 剧本和模板时一起学习了 YAML 和 Jinja2,不能没有另一个考虑
          猜你喜欢
          • 2019-05-26
          • 1970-01-01
          • 2020-08-21
          • 1970-01-01
          • 2021-06-16
          • 2021-07-23
          • 1970-01-01
          • 2015-10-22
          相关资源
          最近更新 更多