如何合并YAML数组?


111

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

some_stuff: &some_stuff
 - a
 - b
 - c

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

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

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

如何在YAML中合并数组?


6
为什么要用YAML而不是用它解析的语言来做?
Patrick Collins

7
删除非常大的yaml文件中的重复文件
lfender6445 2014年

4
这是非常不好的做法。您应该分别阅读yaml,将这些数组放在Ruby中,然后再写回yaml。
sawa 2014年

73
如何尝试干坏习惯?
krak3n 2015年

13
@PatrickCollins我发现此问题试图减少.gitlab-ci.yml文件中的重复,但是不幸的是,我无法控制GitLab CI使用的解析器:(
rink.attendant.6

Answers:


40

如果目的是运行一系列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获得私有仓库的工作示例:

.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


3
聪明。我现在在我们的Bitbucket管道中使用它。谢谢
Dariop '19

*这里不需要结尾的破折号,仅末端的管道就足够了。*这是一个较差的解决方案,因为当作业在很长的多行语句中失败时,尚不清楚哪个命令失败。
米娜·卢克

1
@MinaLuke,相比之下要逊色什么?当前的答案都没有提供仅使用yaml合并两个项目的方法。此外,问题中没有任何内容表明OP希望在CI / CD中使用它。最后,当在CI / CD中使用它时,日志记录仅取决于所使用的特定CI / CD,而不取决于yaml声明。因此,如果有的话,您所指的CI / CD就是做得不好的。此答案中的yaml是有效的,可以解决OP的问题。
豪尔赫·雷涛

@JorgeLeitao我想您可以使用它来组合规则。您能否提供一个可行的gitlabci示例?我根据您的解决方案尝试了一些方法,但始终会收到验证错误。
niels

@niels,我添加了一个带有gitlabci示例的示例。请注意,有些IDE将该Yaml标记为无效,即使不是。
豪尔赫·雷涛

26

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

  • 注意:对该问题的另一个答案进行了实质性的编辑,并提供了替代方法更新
    • 该更新的答案提到了该答案中替代方法的替代方法。它已添加到下面的另请参见部分。

语境

这篇文章假设以下情况:

  • 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

  清单三:&id003
   - H
   - 一世
   -j

  list_combined:
      -* id001
      -* id002
      -* id003

YAML.safe_load之后的结果

## list_combined
  [
    [
      “一个”,
      “ b”,
      “C”
    ],
    [
      “ e”,
      “F”,
      “G”
    ],
    [
      “H”,
      “一世”,
      “ j”
    ]
  ]

陷阱

  • 这种方法会产生一个嵌套的列表列表,该列表可能不是所需的确切输出,但是可以使用flatten方法对其进行后处理
  • 对YAML锚点和别名通常警告适用于唯一性和声明顺序

结论

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

尽管输出结果是列表的嵌套列表,但是可以使用该flatten方法轻松地对其进行转换。

也可以看看

@Anthon更新了替代方法

flatten方法实例


21

这是行不通的:

  1. YAML规范仅支持映射而不是序列支持合并

  2. 您可以通过合并键<< ,键/值分隔符:和作为引用的值来完全混合事物,然后继续使用相同缩进级别的列表

这是不正确的YAML:

combine_stuff:
  x: 1
  - a
  - b

因此,您的示例语法甚至不适合作为YAML扩展建议。

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

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

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

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

IMO的一个更好的解决方案是使用标签加载为您进行展平的数据结构。这样可以清楚地表明需要展平什么,不需要展平什么,并让您完全控制此展平是在加载期间完成还是在访问期间完成。选择哪一个是易于实施以及时间和存储空间效率的问题。这是实现合并功能需要进行的权衡,并且没有单一的解决方案总是最好的。

例如,我的ruamel.yaml库在使用安全加载程序时在加载过程中使用了蛮力合并字典,这导致合并的字典是普通的Python字典。这种合并必须预先完成,并且会复制数据(空间效率低下),但查找值的速度很快。使用双向加载程序时,您希望能够转储未合并的合并,因此需要将它们分开。像数据结构这样的dict由于往返加载而被加载,它是空间有效的,但是访问速度较慢,因为它需要尝试查找合并中dict本身未找到的键(并且该键不会被缓存,因此它每次都需要做)。当然,这些考虑对于相对较小的配置文件而言不是很重要。


以下代码使用带有标签的对象在python中为列表实现了类似于合并的方案,该对象flatten 可即时递归到list和tag的项目中toflatten。使用这两个标记,您可以拥有YAML文件:

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

(使用流vs块样式序列完全是任意的,并且对加载的结果没有影响)。

遍历键值的项目时,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

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

与使用YAML合并键进行一些魔术操作相比,使用IMO 显式标签更好<<。如果您碰巧拥有一个YAML文件,且其映射的键<<不想让您像合并键一样工作,例如,当您将C运算符映射到它们的描述时,如果现在没有其他事情,那么现在您就必须经历麻烦 了。用英语(或其他自然语言)。


9

如果您只需要将一项合并到列表中,则可以执行

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

-4

在以下情况下,您可以合并映射,然后将其键转换为列表:

  • 如果您使用jinja2模板和
  • 如果项目顺序不重要
some_stuff: &some_stuff
 a:
 b:
 c:

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

{{ combined_stuff | list }}

这个答案有什么问题?如果他们提出异议,我不介意投反对票。我将保留答案给可以利用它的人。
sm4rk0

3
可能是因为当问题要求在yml中执行时,此答案取决于jinja2模板。jinja2需要一个Python环境,如果OP尝试进行DRY操作,这会适得其反。同样,许多CI / CD工具不接受模板步骤。
豪尔赫·雷涛

谢谢@JorgeLeitao。这就说得通了。我在开发Ansible剧本和模板时一起学习了YAML和Jinja2,并且一目了然
Sm4rk0
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.