Как я могу обновить файл .yml, игнорируя существующий синтаксис Jinja, используя Python?

У меня есть предварительная обработка некоторых существующих файлов .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? Я хотел бы добавить информацию в эти файлы, сохраняя все остальное.

Я попытался использовать PyYAML на Python 2.7+

3 Solutions collect form web for “Как я могу обновить файл .yml, игнорируя существующий синтаксис Jinja, используя Python?”

Решение в этом ответе было включено в ruamel.yaml, используя механизм плагина. В нижней части этого сообщения есть быстрые и грязные инструкции о том, как их использовать.

Существует три аспекта обновления файла YAML, содержащего jinja2 «код»:

  • делая код jinja2 приемлемым для анализатора YAML
  • убедившись, что приемлемое может быть отменено (т. е. изменения должны быть уникальными, поэтому только они меняются)
  • сохраняя макет файла YAML, чтобы обновленный файл, обработанный jinja2, все еще выдавал действительный файл YAML, который снова можно загрузить.

Начнем с того, что ваш пример несколько более реалистичен, добавив определение переменной jinja2 и for-loop и добавив некоторые комментарии ( 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=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') 

который печатает (укажите второй параметр 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 <<{ 

Если ни #{ или <{ входят в исходные входы, то очистка и реверсия могут выполняться с помощью простых однострочных функций (см. Эти версии этого сообщения ), а затем вам не нужен класс Sanitize

Ваш пример имеет отступы с одной позицией (клавиша B ), а также две позиции (элементы последовательности), ruamel.yaml не имеет точного контроля над выходным отступом (и я не знаю ни одного парсера YAML, который делает). Отступ (по умолчанию 2) применяется к сопоставлениям YAML как к элементам последовательности (измеренным до начала элемента, а не тире). Это не влияет на повторное чтение YAML и произошло также с выходом двух других ответчиков (без их указания на это изменение).

Также обратите внимание, что YAML().load() безопасен (т. yaml.load() Не загружает произвольные потенциально вредоносные объекты), тогда как yaml.load() как используется в других ответах , определенно небезопасен , он говорит об этом в документации и даже упомянутых в статье WikiPedia по YAML . Если вы используете yaml.load() , вам нужно будет проверить каждый входной файл, чтобы убедиться, что нет объектов с тегами, которые могут привести к стиранию вашего диска (или, что еще хуже).

Если вам нужно многократно обновлять свои файлы и управлять шаблоном jinja2, возможно, лучше изменить шаблоны для jinja2 один раз и не вернуть их, а затем указать соответствующие block_start_string , variable_start_string (и возможные block_end_string и variable_end_string ) в jinja2.FileSystemLoader добавлен как загрузчик в jinja2.Environment .


Если вышеописанное кажется сложным, то в aa 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).

Один из способов сделать это – использовать синтаксический анализатор jinja2 для анализа шаблона и вывода альтернативного формата.

Код Jinja2:

Этот код наследует классы Jinja2 Parser , Lexer и Environment для анализа внутри переменных блоков (обычно {{ }} ). Вместо того, чтобы оценивать переменные, этот код меняет текст на то, что 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 parse , вместо скомпилированного кода jinja, так что, когда шаблон визуализируется, строка вставляется вместо расширения переменной.

Код MyEnvironment () используется для привязки пользовательских расширений парсера и лексера. И в то время как при этом добавлена ​​обработка некоторых параметров.

Основным преимуществом такого подхода является то, что он должен быть достаточно прочным для анализа любого джиньи.

Код пользователя:

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

В их текущем формате ваши .yml файлы – это шаблоны .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 }} 

Один из способов обхода этого – заменить запасные места 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 в выражении захватывает имя переменной. Регулярное выражение такое же, как и 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 
  • принудительное сведение pyYAML
  • Переносимые переменные записи в файл YAML
  • pyyaml: демпинг без тегов
  • PyYAML и необычные теги
  • Параметры конструктора по умолчанию в pyyaml
  • Можно ли исправить допустимый YAML с привязками / ссылками, отключенными с помощью Ruby или Python?
  • Python YAML: управление форматом вывода
  • Почему PyYAML тратит столько времени на разбор файла YAML?
  • Python - лучший язык программирования в мире.