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

时间:2017-06-07 20:36:43

标签: python python-2.7 yaml jinja2

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

A:
 B:
 - ip: 1.2.3.4
 - myArray:
   - {{ jinja.variable }}
   - val1
   - val2

我想阅读此文件,并在val3下添加myArray

A:
 B:
 - ip: 1.2.3.4
 - myArray:
   - {{ jinja.variable }}
   - val1
   - val2
   - val 3

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

对于我来说,推荐使用这种.yml文件并修改它们的方法是什么,尽管有预先存在的Jinja语法?我希望信息添加到这些文件中,并保持其他所有内容相同。

我在Python 2.7 +

上使用PyYAML尝试了上述操作

3 个答案:

答案 0 :(得分:6)

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

更新包含jinja2"代码":

的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 <<{

{%开头的行不包含YAML,因此我们将这些作为注释(假设注释在往返时保留,请参阅下文)。由于YAML标量无法在未引用的情况下以{开头,因此我们会将{{更改为<{。通过调用sanitize()(也存储所使用的模式,在sanitize.reverse中完成相反的操作(使用存储的模式),在以下代码中完成此操作。

保存您的YAML代码(块式等)最好使用ruamel.yaml(免责声明:我是该套餐的作者),这样您就不必担心流量 - 输入中的样式元素被压缩为块样式,而其他答案使用的粗略default_flow_style=Falseruamel.yaml还会保留评论,包括最初在文件中的评论,以及暂时插入&#34;评论&#34; 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')

使用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),然后您不要# 39; t需要课程Sanitize

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

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

如果您需要重复更新文件并控制jinja2模板,最好更改jinja2的模式一次而不是还原它们,然后指定适当的block_start_string,{{1} (variable_start_stringblock_end_stringvariable_end_string可以jinja2.FileSystemLoader作为加载程序添加到jinja2.Environment

如果上述内容似乎很复杂,那么在一个虚拟实体中:

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评论一样)。

答案 1 :(得分:2)

在目前的格式中,您的.yml文件是jinja模板,在呈现之前无效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 }}

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

$ cat test.yml
A:
  B:
  - ip: 1.2.3.4
  - myArray:
    - {{ jinja_variable }}
    - val1
    - val2

将文件作为文本文件打开

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

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

现在将文本加载为yaml:

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

添加新值:

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

序列化回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

现在恢复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

将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

答案 2 :(得分:2)

一种方法是使用jinja2解析器本身来解析模板并输出替代格式。

Jinja2代码:

此代码继承自Jinja2 ParserLexerEnvironment类,以解析变量块(通常为{{ }})。此代码不是评估变量,而是将文本更改为yaml可以理解的内容。完全相同的代码可用于通过交换分隔符来反转过程。默认情况下,它会转换为snakecharmerb建议的分隔符。

import jinja2
import yaml

class MyParser(jinja2.parser.Parser):

    def parse_tuple(self, *args, **kwargs):

        super(MyParser, self).parse_tuple(*args, **kwargs)

        if not isinstance(self.environment._jinja_vars, list):
            node_text = self.environment._jinja_vars
            self.environment._jinja_vars = None
            return jinja2.nodes.Const(
                self.environment.new_variable_start_string +
                node_text +
                self.environment.new_variable_end_string)

class MyLexer(jinja2.lexer.Lexer):

    def __init__(self, *args, **kwargs):
        super(MyLexer, self).__init__(*args, **kwargs)
        self.environment = None

    def tokenize(self, source, name=None, filename=None, state=None):
        stream = self.tokeniter(source, name, filename, state)

        def my_stream(environment):
            for t in stream:
                if environment._jinja_vars is None:
                    if t[1] == 'variable_begin':
                        self.environment._jinja_vars = []
                elif t[1] == 'variable_end':
                    node_text = ''.join(
                        [x[2] for x in self.environment._jinja_vars])
                    self.environment._jinja_vars = node_text
                else:
                    environment._jinja_vars.append(t)
                yield t

        return jinja2.lexer.TokenStream(self.wrap(
            my_stream(self.environment), name, filename), name, filename)

jinja2.lexer.Lexer = MyLexer


class MyEnvironment(jinja2.Environment):

    def __init__(self,
                 new_variable_start_string='<<',
                 new_variable_end_string='>>',
                 reverse=False,
                 *args,
                 **kwargs):
        if kwargs.get('loader') is None:
            kwargs['loader'] = jinja2.BaseLoader()

        super(MyEnvironment, self).__init__(*args, **kwargs)
        self._jinja_vars = None
        if reverse:
            self.new_variable_start_string = self.variable_start_string
            self.new_variable_end_string = self.variable_end_string
            self.variable_start_string = new_variable_start_string
            self.variable_end_string = new_variable_end_string
        else:
            self.new_variable_start_string = new_variable_start_string
            self.new_variable_end_string = new_variable_end_string
        self.lexer.environment = self

    def _parse(self, source, name, filename):
        return MyParser(self, source, name,
                        jinja2._compat.encode_filename(filename)).parse()

如何/为什么?

jinja2解析器扫描模板文件以查找分隔符。找到分隔符时,它会切换到在分隔符之间解析相应的材料。这里代码中的更改将自身插入到词法分析器和解析器中以捕获在模板编译期间捕获的文本,然后在找到终止分隔符时,将解析的标记连接成字符串并将其作为jinja2.nodes.Const解析节点插入,代替编译的jinja代码,以便在渲染模板时插入字符串而不是变量扩展。

MyEnvironment()代码用于挂钩自定义解析器和词法分析器扩展。在此期间,添加了一些参数处理。

这种方法的主要优点是解析任何jinja将解析的内容应该相当健壮。

用户代码:

def dict_from_yaml_template(template_string):
    env = MyEnvironment()
    template = env.from_string(template_string)
    return yaml.load(template.render())

def yaml_template_from_dict(template_yaml, **kwargs):
    env = MyEnvironment(reverse=True)
    template = env.from_string(yaml.dump(template_yaml, **kwargs))
    return template.render()

测试代码:

with open('data.yml') as f:
    data = dict_from_yaml_template(f.read())
data['A']['B'][1]['myArray'].append('val 3')
data['A']['B'][1]['myArray'].append('<< jinja.variable2 >>')
new_yaml = yaml_template_from_dict(data, default_flow_style=False)
print(new_yaml)

data.yml

A:
 B:
 - ip: 1.2.3.4
 - myArray:
   - {{ x['}}'] }}
   - {{ [(1, 2, (3, 4))] }}
   - {{ jinja.variable }}
   - val1
   - val2

结果:

A:
  B:
  - ip: 1.2.3.4
  - myArray:
    - {{ x['}}'] }}
    - {{ [(1, 2, (3, 4))] }}
    - {{ jinja.variable }}
    - val1
    - val2
    - val 3
    - {{ jinja.variable2 }}