如何替换 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: ¶ms
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
,只是改变了输入映射值,
如果你也想做键,你要么必须弹出和
重新插入所有的钥匙(即使没有改变),保持原来的
顺序,否则您需要使用 enumerate
和 d.insert(position, key,
value)
。如果你有合并,你也不能只是走过钥匙,
你必须遍历 "dict".
的非合并键
我目前正在构建一个使用 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: ¶ms
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 否则会指示映射并破坏 YAMLYAML.load()
方法,与PyYAML的load
函数相反,是 安全(即不能通过操纵输入执行任意 Python 文件。保留注释、八进制、十六进制整数和别名。
PyYAML 根本无法加载文件
in.yaml
,尽管它是有效的 YAML上面的
recurse
,只是改变了输入映射值, 如果你也想做键,你要么必须弹出和 重新插入所有的钥匙(即使没有改变),保持原来的 顺序,否则您需要使用enumerate
和d.insert(position, key, value)
。如果你有合并,你也不能只是走过钥匙, 你必须遍历 "dict". 的非合并键