如何使用Python更新.yml文件,忽略已有的Jinja语法?

PhD*_*PhD 9 python yaml jinja2 python-2.7

我有一些预处理与一些现有的.yml文件 - 但是,其中一些嵌入了Jinja模板语法:

A:
 B:
 - ip: 1.2.3.4
 - myArray:
   - {{ jinja.variable }}
   - val1
   - val2
Run Code Online (Sandbox Code Playgroud)

我想在这个文件中读取,并添加val3myArray这样:

A:
 B:
 - ip: 1.2.3.4
 - myArray:
   - {{ jinja.variable }}
   - val1
   - val2
   - val 3
Run Code Online (Sandbox Code Playgroud)

我试着手动写出jinja模板,但是他们用单引号写了: '{{ jinja.variable }}'

对于我来说,阅读这些.yml文件并修改它们的推荐方法是什么,尽管有预先存在的Jinja语法?我想向这些文件添加信息,保持其他所有相同.

我在Python 2.7+上使用PyYAML尝试了上述内容

Ant*_*hon 7

此答案中的解决方案已使用插件机制合并到ruamel.yaml中.在这篇文章的底部有关于如何使用它的快速和脏的说明.

更新包含jinja2"code"的YAML文件有三个方面:

  • 使jinja2代码可以被YAML解析器接受
  • 确保可接受的可以逆转(即变化应该是唯一的,所以只有它们才能被逆转)
  • 保留YAML文件的布局,以便jinja2处理的更新文件仍然生成一个有效的YAML文件,该文件也可以加载.

让我们首先通过添加jinja2变量定义和for循环以及添加一些注释(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 <<{
Run Code Online (Sandbox Code Playgroud)

{%#YLL 开头的行不包含YAML,因此我们将这些作为注释(假设注释在往返时保留,见下文).由于YAML标量不能在{没有引用的情况下开始,我们将更{{改为<{.这可以通过调用以下代码完成sanitize()(它也存储使用的模式,反之在sanitize.reverse(使用存储的模式)完成.

保存您的YAML代码(块样式等)最好使用ruamel.yaml(免责声明:我是该软件包的作者),这样您就不必担心输入中的流式元素被破坏为块与default_flow_style=False其他答案使用的相当粗糙的风格一样.ruamel.yaml还保留了注释,包括最初在文件中的注释,以及那些暂时插入以"注释掉"jinja2结构的注释%{.

结果代码:

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')
Run Code Online (Sandbox Code Playgroud)

update_one()使用Python 2.7 打印(指定要写入文件的第二个参数):

# 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 <<{
Run Code Online (Sandbox Code Playgroud)

如果既不是#{也不<{在任何原始输入中,则可以使用简单的单行函数完成清理和恢复(请参阅此文章的此版本),然后您不需要该类Sanitize

您的示例缩进了一个位置(键B)以及两个位置(序列元素),ruamel.yaml没有对输出缩进的精确控制(我不知道任何YAML解析器).缩进(默认为2)应用于两个YAML映射关于序列元素(测量到元素的开头,而不是短划线).这对重新阅读YAML没有影响,也发生在其他两个回答者的输出上(没有他们指出这个变化).

另请注意,这YAML().load()是安全的(即不加载任意潜在的恶意对象),而yaml.load()其他答案中使用的肯定是不安全的,它在文档中也是如此,甚至在YAML上WikiPedia文章中也有提及.如果使用yaml.load(),则必须检查每个输入文件,以确保没有标记的对象可能导致光盘被擦除(或更糟).

如果您需要反复更新你的文件,并有过的Jinja2模板控制,它可能是更好的改变Jinja2的模式一次,不能回复他们,然后指定相应的block_start_string,variable_start_string(可能block_end_stringvariable_end_string)的jinja2.FileSystemLoader添加加载到jinja2.Environment.


如果以上似乎很复杂,那么在a virtudalenv做:

pip install ruamel.yaml ruamel.yaml.jinja2
Run Code Online (Sandbox Code Playgroud)

假设您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')
Run Code Online (Sandbox Code Playgroud)

得到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 }}
Run Code Online (Sandbox Code Playgroud)

ruamel.yaml0.15.7实现了一个新的插件机制,ruamel.yaml.jinja2是一个插件,可以为用户透明地重写此答案中的代码.目前,还原的信息附加到YAML()实例,因此请确保yaml = YAML(typ='jinja2')对您处理的每个文件执行此操作(该信息可以附加到顶级data实例,就像YAML注释一样).

  • @PhD我意识到,这种预处理和后期处理将是ruamel.yaml插件机制的理想选择。因此,我实现了该插件。我更新了如何安装和使用它的答案。 (2认同)

sna*_*erb 4

在当前格式下,您的文件是 jinja 模板,在渲染后才.yml有效。yaml这是因为 jinja 占位符语法与 yaml 语法冲突,因为大括号 ({}) 可用于表示 yaml 中的映射。

>>> yaml.load('foo: {{ bar }}')
Traceback (most recent call last):
...
yaml.constructor.ConstructorError: while constructing a mapping
  in "<string>", line 1, column 6:
    foo: {{ bar }}
     ^
found unacceptable key (unhashable type: 'dict')
  in "<string>", line 1, column 7:
    foo: {{ bar }}
Run Code Online (Sandbox Code Playgroud)

解决此问题的一种方法是将 jinja 占位符替换为其他内容,将文件处理为 yaml,然后恢复占位符。

$ cat test.yml
A:
  B:
  - ip: 1.2.3.4
  - myArray:
    - {{ jinja_variable }}
    - val1
    - val2
Run Code Online (Sandbox Code Playgroud)

将文件作为文本文件打开

>>> with open('test.yml') as f:
...     text = f.read()
... 
>>> print text
A:
  B:
  - ip: 1.2.3.4
  - myArray:
    - {{ jinja_variable }}
    - val1
    - val2
Run Code Online (Sandbox Code Playgroud)

正则表达式r'{{\s*(?P<jinja>[a-zA-Z_][a-zA-Z0-9_]*)\s*}}'将匹配文本中的任何 jinja 占位符;jinja表达式中的命名组捕获变量名称。正则表达式与 Jinja2 用于匹配变量名称的正则表达式相同。

re.sub函数可以使用语法在其替换字符串中引用命名组\g。我们可以使用此功能将 jinja 语法替换为与 yaml 语法不冲突且尚未出现在您正在处理的文件中的语法。例如替换{{ ... }}<< ... >>.

>>> import re
>>> yml_text = re.sub(r'{{\s*(?P<jinja>[a-zA-Z_][a-zA-Z0-9_]*)\s*}}', '<<\g<jinja>>>', text)
>>> print yml_text
A:
  B:
  - ip: 1.2.3.4
  - myArray:
    - <<jinja_variable>>
    - val1
    - val2
Run Code Online (Sandbox Code Playgroud)

现在将文本加载为 yaml:

>>> yml = yaml.load(yml_text)
>>> yml
{'A': {'B': [{'ip': '1.2.3.4'}, {'myArray': ['<<jinja_variable>>', 'val1', 'val2']}]}}
Run Code Online (Sandbox Code Playgroud)

添加新值:

>>> yml['A']['B'][1]['myArray'].append('val3')
>>> yml
{'A': {'B': [{'ip': '1.2.3.4'}, {'myArray': ['<<jinja_variable>>', 'val1', 'val2', 'val3']}]}}
Run Code Online (Sandbox Code Playgroud)

序列化回 yaml 字符串:

>>> new_text = yaml.dump(yml, default_flow_style=False)
>>> print new_text
A:
  B:
  - ip: 1.2.3.4
  - myArray:
    - <<jinja_variable>>
    - val1
    - val2
    - val3
Run Code Online (Sandbox Code Playgroud)

现在恢复 jinja 语法。

>>> new_yml = re.sub(r'<<(?P<placeholder>[a-zA-Z_][a-zA-Z0-9_]*)>>', '{{ \g<placeholder> }}', new_text)
>>> print new_yml
A:
  B:
  - ip: 1.2.3.4
  - myArray:
    - {{ jinja_variable }}
    - val1
    - val2
    - val3
Run Code Online (Sandbox Code Playgroud)

并将 yaml 写入磁盘。

>>> with open('test.yml', 'w') as f:
...     f.write(new_yml)
... 

$cat test.yml
A:
  B:
  - ip: 1.2.3.4
  - myArray:
    - {{ jinja_variable }}
    - val1
    - val2
    - val3
Run Code Online (Sandbox Code Playgroud)