如何合并YAML数组?

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

some_stuff: &some_stuff
- a
- b
- c


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

我希望将组合阵列作为[a,b,c,d,e,f]

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

如何在YAML中合并数组?

93574 次浏览

这是行不通的:

  1. YAML规范

    仅对映射支持合并,而对序列不支持合并

  2. 通过使用ABC0__合并键,
  3. 您可以完全混合内容。 后跟键/值分隔符:和一个 引用,然后在同一缩进处继续使用列表 水平

这是不正确的YAML:

combine_stuff:
x: 1
- a
- b

因此,作为YAML扩展建议,您的示例语法甚至没有意义。

如果你想做一些事情,比如合并多个数组,你可能需要考虑这样的语法:

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

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

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

IMO

更好的解决方案是使用标签来加载数据结构。 为你做压平。这允许清楚地表示什么 需要被展平什么都不需要,让你完全控制 这种展平是在加载过程中完成的,还是在 访问。选择哪一个是一个易于实施的问题, 时间和存储空间的效率。这是同样需要做出的权衡。 用于实现合并密钥功能和 没有一个解决方案永远是最好的。

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


下面使用带有标签flatten的对象为Python中的列表实现了一个类似Merge的方案 其即时递归为列表和标记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

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

在我看来,

使用明确的标签比使用一些魔法更好。 使用YAML合并键<<。如果没有别的事,你现在必须通过 如果你碰巧有一个YAML文件,它的映射有一个键,那就麻烦了。 <<,您不想充当合并键,例如,当您创建 将C操作符映射到其英语(或其他自然语言)描述。

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

  • 注意:这个问题的另一个答案基本上是用关于替代办法的最新情况编辑的。
    • 更新后的答案提到了此答案中的替代解决方法。它已添加到下面的另请参阅部分。

上下文

这篇文章假设了以下背景:

  • Python 2.7
  • Python YAML解析器

问题

lfender6445希望合并YAML文件中的两个或多个列表,并拥有这些列表 分析时,合并的列表显示为一个单数列表。

解决方案(解决方法)

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

在下面

的示例中,我们有三个映射(__abc0)和三个锚点 以及在适当的地方引用这些映射的别名。

当YAML文件被加载到程序中时,我们得到了我们想要的列表,但是 它可能需要在加载后进行一些修改(见下面的陷阱)。

例子

原始YAML文件

list_one: &id001
- a
- b
- c


list_two: &id002
- e
- f
- g


list_three: &id003
- h
- i
- j


list_combined:
- *id001
- *id002
- *id003

加载YAML.SAFE_后的结果

## list_combined
[
[
"a",
"b",
"c"
],
[
"e",
"f",
"g"
],
[
"h",
"i",
"j"
]
]

陷阱

  • 该方法产生嵌套的列表,其可能不是精确的期望输出,但是可以使用弄平方法对其进行后处理
  • YAML锚点和别名的常见警告适用于唯一性和声明顺序

结论

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

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

另请参阅

@Anthon更新了替代方法

flatten方法的示例

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

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


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


\{\{ combined_stuff | list }}

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

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

如果目标是运行一系列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.attendent.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

在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文档结构,该结构将所有数组解析为__abc0,将所有值解析为__abc1。 可以在以下测试函数中测试(和修改)该行为。

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"]
)