如何替换 YAML 文件中的多个相同值

How to replace many identical values in a YAML file

我目前正在构建一个使用 YAML 配置的 python 应用程序。我使用其他 YAML 文件生成 YAML 配置文件。我有一个 "template" YAML,它在应用程序使用的 YAML 文件中定义了我想要的基本结构,然后是许多不同的 "data" YAML,它们填充模板以某种方式旋转应用程序的行为。例如,假设我有 10 "data" 个 YAML。根据部署应用程序的位置,选择 1 "data" YAML,并用于填写 "template" YAML。结果填写的 YAML 是应用程序用于 运行 的内容。这为我节省了大量工作。不过,我 运行 遇到了这个方法的问题。假设我有一个如下所示的模板 YAML:

id: {{id}}
endpoints:
  url1: https://website.com/{{id}}/search
  url2: https://website.com/foo/{{id}}/get_thing
  url3: https://website.com/hello/world/{{id}}/trigger_stuff
foo:
  bar:
    deeply:
      nested: {{id}}

然后在其他地方,我有 10 个 "data" YAML,每个都有不同的 {{id}} 值。我似乎无法找到一种有效的方法来替换模板中所有这些 {{id}} 的出现。我遇到了一个问题,因为有时要替换的值是我最想保留的值的子字符串,或者在层次结构中出现的次数彼此相距很远,从而使循环解决方案效率低下。我当前使用模板+数据生成配置文件的方法在 python:

中看起来像这样
import yaml
import os

template_yaml = os.path.abspath(os.path.join(os.path.dirname(__file__), 'template.yaml'))
# In this same folder you would find flavor2, flavor3, flavor4, etc, lets just use 1 for now
data_yaml = os.path.abspath(os.path.join(os.path.dirname(__file__), 'data_files', 'flavor1.yaml'))
# This is where we dump the filled out template the app will actually use
output_directory = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))

with open(template_yaml, 'r') as template:
    try:
        loaded_template = yaml.load(template)  # Load the template as a dict
        with open(data_yaml , 'r') as data:
            loaded_data= yaml.load(data)  # Load the data as a dict
        # From this point on I am basically just setting individual keys from "loaded_template" to values in "loaded_data"
        # But 1 at a time, which is what I am trying to avoid:
        loaded_template['id'] = loaded_data['id']
        loaded_template['endpoints']['url1'] = loaded_template['endpoints']['url1'].format(loaded_data['id'])
        loaded_template['foo']['bar']['deeply']['nested'] = loaded_data['id']

知道如何更快地遍历和更改所有出现的 {{id}} 吗?

如果单个 yaml 文件的每个位置的 id 都相同,那么您可以将模板作为纯文本读入并逐行使用字符串替换。

new_file = []

# New id for replacement (from loaded file)
id_ = '123'

# Open template file 
with open('template.yaml', 'r') as f:
    # Iterate through each line
    for l in f:
        # Replace every {{id}} occurrence
        new_file.append(l.replace('{{id}}', id_))

# Save the new file
with open('new_file.yaml', 'w') as f:
    for l in new_file:
        f.write(l)

这将在文件中的所有地方用相同的 id_ 替换 {{id}},并且不会更改任何格式。

YAML 内置了 "anchors",您可以创建和引用类似的变量。对我来说,这些实际上是在引用的地方替换它们的值并不明显,因为您只能在解析 YAML 之后看到结果。代码无耻地从涵盖类似主题的 Reddit post 中窃取:

# example.yaml
params: &params
  PARAM1: &P1 5
  PARAM2: &P2 "five"
  PARAM3: &P3 [*P1, *P2]

data:
  <<: *params
  more:
    - *P3
    - *P2

ff

# yaml.load(example) =>
{
'params': {
    'PARAM1': 5, 
    'PARAM2': 'five', 
    'PARAM3': [5, 'five']
},
'data': {
    'PARAM1': 5,
    'PARAM2': 'five',
    'PARAM3': [5, 'five'],
    'more': [[5, 'five'], 'five']
}
}

this post 在这里我认为你可以使用锚点作为子字符串(假设你正在使用 python)

你在向我们推荐PyYAML,但它不太适合做 YAML 文件的更新。在那个过程中,如果它可以加载你的文件 首先,你松开了你的映射键顺序,任何评论你 在文件中有,合并得到扩展,以及任何特殊的锚点名称 迷失在翻译中。除此之外,PyYAML 无法处理 最新的 YAML 规范(9 年前发布),它只能处理简单的映射键。

主要有两种解决方案:

  • 您可以在原始文件上使用替换
  • 您使用 ruamel.yaml 并递归到数据结构中

替换

如果你使用替换,你可以用比 @caseWestern 建议的逐行替换。但大部分 所有,你应该加强这些替代所采用的标量 地方。目前你有普通标量(即流式标量 没有引号),如果你插入像 #: 和其他语法上重要的元素。

为了防止这种情况发生重写你的输入文件以使用 块样式文字标量:

id: {{id}}
endpoints:
  url1: |-
    https://website.com/{{id}}/search
  url2: |-
    https://website.com/foo/{{id}}/get_thing
  url3: |-
    https://website.com/hello/world/{{id}}/trigger_stuff
foo:
  bar:
    deeply:
      nested: |-
        {{id}}

如果以上在alt.yaml中,你可以这样做:

val = 'xyz'

with open('alt.yaml') as ifp:
    with open('new.yaml', 'w') as ofp:
       ofp.write(ifp.read().replace('{{id}}', val))

获得:

id: xyz
endpoints:
  url1: |-
    https://website.com/xyz/search
  url2: |-
    https://website.com/foo/xyz/get_thing
  url3: |-
    https://website.com/hello/world/xyz/trigger_stuff
foo:
  bar:
    deeply:
      nested: |-
        xyz

ruamel.yaml

使用 ruamel.yaml(免责声明:我是该软件包的作者),您不必 担心通过具有语法意义的替换文本破坏输入。如果 你这样做,那么输出将自动被正确引用。你必须 请注意您的输入是有效的 YAML,并使用类似 {{ 的东西,在 节点的开头表示两个嵌套的流式映射,你会 运行 惹上麻烦。

这里最大的好处是你的输入文件被加载,并且被检查为 正确的 YAML。但这比文件级替换要慢得多。

因此,如果您的输入是 in.yaml:

id: <<id>>  # has to be unique
endpoints: &EP
  url1: https://website.com/<<id>>/search
  url2: https://website.com/foo/<<id>>/get_thing
  url3: https://website.com/hello/world/<<id>>/trigger_stuff
foo:
  bar:
    deeply:
      nested: <<id>>
    endpoints: *EP
    [octal, hex]: 0o123, 0x1F

你可以做到:

import sys
import ruamel.yaml

def recurse(d, pat, rep):
    if isinstance(d, dict):
        for k in d:
            if isinstance(d[k], str):
                d[k] = d[k].replace(pat, rep)
            else:
               recurse(d[k], pat, rep)
    if isinstance(d, list):
        for idx, elem in enumerate(d):
            if isinstance(elem, str):
                d[idx] = elem.replace(pat, rep)
            else:
               recurse(d[idx], pat, rep)


yaml = ruamel.yaml.YAML()
yaml.preserve_quotes = True
with open('in.yaml') as fp:
    data = yaml.load(fp)
recurse(data, '<<id>>', 'xy: z')  # not that this makes much sense, but it proves a point
yaml.dump(data, sys.stdout)

给出:

id: 'xy: z' # has to be unique
endpoints: &EP
  url1: 'https://website.com/xy: z/search'
  url2: 'https://website.com/foo/xy: z/get_thing'
  url3: 'https://website.com/hello/world/xy: z/trigger_stuff'
foo:
  bar:
    deeply:
      nested: 'xy: z'
    endpoints: *EP
    [octal, hex]: 0o123, 0x1F

请注意:

  • 具有替换模式的值在转储时自动引用,以 处理 : + space 否则会指示映射并破坏 YAML

  • YAML.load()方法,与PyYAML的load函数相反,是 安全(即不能通过操纵输入执行任意 Python 文件。

  • 保留注释、八进制、十六进制整数和别名。

  • PyYAML 根本无法加载文件 in.yaml,尽管它是有效的 YAML

  • 上面的recurse,只是改变了输入映射值, 如果你也想做键,你要么必须弹出和 重新插入所有的钥匙(即使没有改变),保持原来的 顺序,否则您需要使用 enumerated.insert(position, key, value)。如果你有合并,你也不能只是走过钥匙, 你必须遍历 "dict".

  • 的非合并键