此答案中的解决方案已使用插件机制合并到 ruamel.yaml 中。在这篇文章的底部有关于如何使用它的快速和肮脏的说明。
更新包含jinja2“代码”的YAML文件有三个方面:
- 使 jinja2 代码为 YAML 解析器所接受
- 确保可以撤消可接受的更改(即更改应该是唯一的,因此只有它们会被撤消)
- 保留 YAML 文件的布局,以便 jinja2 处理的更新文件仍然生成有效的 YAML 文件,可以再次加载。
让我们首先通过添加 jinja2 变量定义和 for 循环并添加一些 cmets (input.yaml) 使您的示例更加真实:
# trying to update
{% set xyz = "123" }
A:
B:
- ip: 1.2.3.4
- myArray:
- {{ jinja.variable }}
- val1
- val2 # add a value after this one
{% for d in data %}
- phone: {{ d.phone }}
name: {{ d.name }}
{% endfor %}
- {{ xyz }}
# #% or ##% should not be in the file and neither <{ or <<{
以{% 开头的行不包含 YAML,因此我们会将它们制作成 cmets(假设 cmets 在往返过程中被保留,见下文)。由于 YAML 标量不能以 { 开头而不被引用,我们将把 {{ 更改为 <{。这在下面的代码中通过调用sanitize() 来完成(它也存储使用的模式,而相反的在sanitize.reverse 中完成(使用存储的模式)。
最好使用ruamel.yaml 保存您的 YAML 代码(块样式等)(免责声明:我是该包的作者),这样您就不必担心与其他答案使用的相当粗糙的default_flow_style=False 一样,输入被破坏为块样式。 ruamel.yaml 还保留 cmets,包括最初在文件中的 cmets,以及临时插入以“注释掉”以 %{ 开头的 jinja2 构造的 cmets。
生成的代码:
import sys
from ruamel.yaml import YAML
yaml = YAML()
class Sanitize:
"""analyse, change and revert YAML/jinja2 mixture to/from valid YAML"""
def __init__(self):
self.accacc = None
self.accper = None
def __call__(self, s):
len = 1
for len in range(1, 10):
pat = '<' * len + '{'
if pat not in s:
self.accacc = pat
break
else:
raise NotImplementedError('could not find substitute pattern '+pat)
len = 1
for len in range(1, 10):
pat = '#' * len + '%'
if pat not in s:
self.accper = pat
break
else:
raise NotImplementedError('could not find substitute pattern '+pat)
return s.replace('{{', self.accacc).replace('{%', self.accper)
def revert(self, s):
return s.replace(self.accacc, '{{').replace(self.accper, '{%')
def update_one(file_name, out_file_name=None):
sanitize = Sanitize()
with open(file_name) as fp:
data = yaml.load(sanitize(fp.read()))
myArray = data['A']['B'][1]['myArray']
pos = myArray.index('val2')
myArray.insert(pos+1, 'val 3')
if out_file_name is None:
yaml.dump(data, sys.stdout, transform=sanitize.revert)
else:
with open(out_file_name, 'w') as fp:
yaml.dump(data, out, transform=sanitize.revert)
update_one('input.yaml')
使用 Python 2.7 打印(指定 update_one() 的第二个参数以写入文件):
# trying to update
{% set xyz = "123" }
A:
B:
- ip: 1.2.3.4
- myArray:
- {{ jinja.variable }}
- val1
- val2 # add a value after this one
- val 3
{% for d in data %}
- phone: {{ d.phone }}
name: {{ d.name }}
{% endfor %}
- {{ xyz }}
# #% or ##% should not be in the file and neither <{ or <<{
如果#{ 和<{ 都没有在任何原始输入中,则可以使用简单的单行函数(参见this versions of this post)完成清理和恢复,然后您就不需要@987654341 类@
您的示例缩进了一个位置(键B)以及两个位置(序列元素),ruamel.yaml 对输出缩进没有很好的控制(而且我不知道任何 YAML解析器)。缩进(默认为 2)适用于序列元素的两个 YAML 映射(测量到元素的开头,而不是短划线)。这对重新读取 YAML 没有影响,并且也发生在其他两个应答器的输出中(没有他们指出这个变化)。
还要注意YAML().load() 是安全的(即不会加载任意潜在恶意对象),而其他答案中使用的yaml.load() 绝对不安全,它在文档,甚至在WikiPedia article on YAML 中提到。如果您使用yaml.load(),则必须检查每个输入文件,以确保没有可能导致您的光盘被擦除(或更糟)的标记对象。
如果您需要反复更新您的文件,并且可以控制 jinja2 模板,最好只更改一次 jinja2 的模式而不恢复它们,然后指定适当的 block_start_string、variable_start_string(并且可能block_end_string 和 variable_end_string) 到 jinja2.FileSystemLoader 作为加载器添加到 jinja2.Environment。
如果上面看起来很复杂,那么在 virtualenv 中做:
pip install ruamel.yaml ruamel.yaml.jinja2
假设您在运行之前拥有input.yaml:
import os
from ruamel.yaml import YAML
yaml = YAML(typ='jinja2')
with open('input.yaml') as fp:
data = yaml.load(fp)
myArray = data['A']['B'][1]['myArray']
pos = myArray.index('val2')
myArray.insert(pos+1, 'val 3')
with open('output.yaml', 'w') as fp:
yaml.dump(data, fp)
os.system('diff -u input.yaml output.yaml')
获取diff 输出:
--- input.yaml 2017-06-14 23:10:46.144710495 +0200
+++ output.yaml 2017-06-14 23:11:21.627742055 +0200
@@ -8,6 +8,7 @@
- {{ jinja.variable }}
- val1
- val2 # add a value after this one
+ - val 3
{% for d in data %}
- phone: {{ d.phone }}
name: {{ d.name }}
ruamel.yaml 0.15.7 实现了一种新的插件机制,ruamel.yaml.jinja2 是一个插件,它为用户透明地重新包装此答案中的代码。目前,还原信息附加到YAML() 实例,因此请确保为您处理的每个文件执行yaml = YAML(typ='jinja2')(该信息可以附加到顶级data 实例,就像YAML cmets 一样) .