【问题标题】:How can I update a .yml file, ignoring preexisting Jinja syntax, using Python?如何使用 Python 更新 .yml 文件,忽略预先存在的 Jinja 语法?
【发布时间】:2017-11-09 09:02:38
【问题描述】:

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

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

我想读入这个文件,并在myArray下添加val3

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

我尝试手动写出 jinja 模板,但它们被写成单引号:'{{ jinja.variable }}'

我推荐的读取此类 .yml 文件并修改它们的方法是什么,尽管使用的是预先存在的 Jinja 语法?我想在这些文件中添加信息,以保持其他所有内容相同。

我在 Python 2.7+ 上使用 PyYAML 尝试了上述方法

【问题讨论】:

    标签: python python-2.7 yaml jinja2


    【解决方案1】:

    此答案中的解决方案已使用插件机制合并到 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 标量不能以 { 开头而不被引用,我们将把 {{ 更改为 &lt;{。这在下面的代码中通过调用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 <<{
    

    如果#{&lt;{ 都没有在任何原始输入中,则可以使用简单的单行函数(参见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_stringvariable_start_string(并且可能block_end_stringvariable_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 一样) .

    【讨论】:

      【解决方案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 }}
      

      【讨论】:

        【解决方案3】:

        在其当前格式中,您的 .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&lt;jinja&gt;[a-zA-Z_][a-zA-Z0-9_]*)\s*}}'将匹配文本中的任何jinja占位符;表达式中的命名组jinja 捕获变量名称。与used by Jinja2相同的正则表达式来匹配变量名。

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

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

        【讨论】:

          猜你喜欢
          • 2010-09-20
          • 1970-01-01
          • 2012-09-18
          • 2022-08-19
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多