Попытка сделать нашу жизнь с PyYAML проще.

YAML - это широко используемый язык сериализации данных. Все разработчики сталкиваются с необходимостью обработать YAML время от времени. Но обработка YAML, особенно с использованием PyYAML в Python, мучительна и полна ловушек. Здесь изложены некоторые советы, которые могут облегчить Вашу жизнь с PyYAML.

Код в этой статье гарантированно работает только в Python 3

Всегда используйте safe_load/safe_dump

Способность YAML конструировать любой объект Python делает его опасным для использования вслепую. Для вашего приложения может быть опасно загружать документ при помощи yaml.load из ненадежного источника, такого как Интернет или пользовательский ввод.

См. официальную документацию PyYAML:

Предупреждение: Вызывать yaml.load с любыми данными, полученными из ненадежного источника, небезопасно! yaml.load является таким же мощным инструментом, как pickle.load, и может вызывать любую функцию Python.

Короче говоря, Вы всегда должны использовать yaml.safe_load и yaml.safe_dump в качестве стандартных методов ввода/вывода для YAML.

Сохраняйте порядок ключей (загрузка/выгрузка)

В Python 3.7+ порядок ключей словаря сохраняется естественным образом, поэтому словарь, который Вы получаете от yaml.safe_load, имеет тот же порядок ключей, что и исходный файл.

>>> import yaml
>>> text = """---
... c: 1
... b: 1
... d: 1
... a: 1
... """
>>> d = yaml.safe_load(text)
>>> d
{'c': 1, 'b': 1, 'd': 1, 'a': 1}
>>> list(d)
['c', 'b', 'd', 'a']

При дампе словаря в строку YAML, убедитесь, что добавили аргумент sort_keys=False, чтобы сохранить порядок ключей.

>>> print(yaml.safe_dump(d))
a: 1
b: 1
c: 1
d: 1
>>> d['e'] = 1
>>> print(yaml.safe_dump(d, sort_keys=False))
c: 1
b: 1
d: 1
a: 1
e: 1

Если Ваша версия Python ниже 3.7, или Вы хотите быть уверены, что исходный порядок ключей всегда сохраняется, Вы можете использовать библиотеку oyaml в качестве замены PyYAML.

>>> import oyaml as yaml
>>> d = yaml.safe_load(text)
>>> d
OrderedDict([('c', 1), ('b', 1), ('d', 1), ('a', 1)])
>>> d['e'] = 1
>>> print(yaml.safe_dump(d, sort_keys=False))
c: 1
b: 1
d: 1
a: 1
e: 1

Улучшение отступов в списке (dump)

По умолчанию в PyYAML элементы списка отступают на том же уровне, что и их родитель.

>>> d = {'a': [1, 2, 3]}
>>> print(yaml.safe_dump(d))
a:
- 1
- 2
- 3

Это не очень хороший формат в соответствии с такими руководствами по стилю, как Ansible и HomeAssistant. Он также не распознается редакторами кода, такими как VSCode, делая элементы списка неразворачиваемыми в редакторе.

Чтобы решить эту проблему, Вы можете использовать приведенный ниже фрагмент для определения класса IndentDumper:

class IndentDumper(yaml.Dumper):
    def increase_indent(self, flow=False, indentless=False):
        return super(IndentDumper, self).increase_indent(flow, False)

Затем передайте его в качестве аргумента Dumper в функции yaml.dump.

>>> print(yaml.dump(d, Dumper=IndentDumper))
a:
  - 1
  - 2
  - 3

Обратите внимание, что Dumper не может быть передан в yaml.safe_dump, у которого определен свой собственный dumper-класс.

Вывод читаемого UTF-8 (dump)

По умолчанию PyYAML предполагает, что пользователь хочет получить на выходе только ASCII код, поэтому он преобразует символы UTF-8 в Юникод представление Python.

>>> d = {'a': '你好'}
>>> print(yaml.safe_dump(d))
a: "\u4F60\u597D"

Это делает вывод трудночитаемым для человека.

В современном мире широко поддерживается UTF-8, поэтому безопасно писать UTF-8 в выводе. Передайте allow_unicode=True в yaml.safe_dump, чтобы включить эту возможность.

>>> print(yaml.safe_dump(d, allow_unicode=True))
a: 你好

Не нужен аргумент default_flow_style (dump)

В большинстве случаев мы не хотим, чтобы в выходных данных присутствовал стиль потока (т.е. никакого JSON в YAML). Согласно документации PyYAML, для достижения этого в yaml.safe_dump следует передать default_flow_style=False.

Покопавшись в исходном коде последней версии PyYaml (6.0), можно обнаружить, что это больше не нужно. Вы можете удалить этот аргумент, чтобы сохранить код более чистым и менее запутанным.

Библиотеки

oyaml

Ссылка: https://github.com/wimglenn/oyaml

Как упоминалось выше, oyaml - это замена PyYAML, которая сохраняет упорядочивание словарей.

Используйте oyaml, если Вы уже используете PyYAML в своем коде.

Стоит отметить, что oyaml - это однофайловая библиотека, содержащая всего 53 строки кода. Это делает ее очень гибкой в использовании, Вы можете просто скопировать код в свою библиотеку и настроить ее в соответствии с Вашими потребностями.

strictyaml

Ссылка: https://github.com/crdoconnor/strictyaml

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

Именно здесь и появился StrictYAML. Это безопасный парсер YAML, который анализирует и проверяет ограниченное подмножество спецификации YAML.

Используйте StrictYAML, если Вы сильно беспокоитесь о безопасности Вашего приложения.

В документации по strictyaml есть масса замечательных статей, которые определенно стоит посмотреть, если Вы задумывались о YAML и других языках конфигурации.

ruamel.yaml

Ссылка: https://yaml.readthedocs.io/en/latest/overview.html

ruamel.yaml - это форк PyYAML, он был выпущен в 2009 году и постоянно поддерживается в течение последнего десятилетия.

Различия с PyYAML перечислены здесь. В целом, ruamel.yaml ориентируется на YAML 1.2 с некоторыми улучшениями в синтаксисе, сделанными автором.

Самым интересным в этой библиотеке является round-trip в процессе загрузки/выгрузки. Это работает как черная магия. Вот объяснение из документации ruamel.yaml:

Round-trip - это последовательность YAML загрузка-модификация-сохранение, и ruamel.yaml пытается сохранить, среди прочего:

- комментарии
- стиль блоков и порядок следования ключей, поэтому Вы можете использовать diff для данных прошедших round-trip
- последовательности в стиле потока ('a: b, c, d')
- имена якорей, созданные вручную (т.е. не в форме idNNN)
- слияния в словарях сохраняются

Стоит использовать ruamel.yaml, если у Вас есть потребность максимально сохранить оригинальное содержимое.

Обратите внимание - метод safe_load в ruamel.yaml (YAML(typ='safe').load) не может разобрать коллекцию в стиле потока (a: {"foo": "bar"}), это недокументированное различие с PyYAML.

Резюме

У YAML есть свои плюсы и минусы. Его легко читать, кривая обучения в начале легкая, но спецификация сложна, что не только вызывает хаос на практике, но и делает реализации на разных языках несовместимыми друг с другом во многих тривиальных аспектах.

Несмотря на эти причуды, YAML по-прежнему остается лучшим языком конфигурации, и пока мы используем его правильно, проблем удастся избежать, а опыт будет намного лучше.

Комментарии (2)


  1. ushankax
    06.06.2022 20:40
    +3

    Обратите внимание, что Dumper не может быть передан в yaml.safe_dump, у которого определен свой собственный dumper-класс.

    А как передать в yaml.safe_dump?


    1. AABur Автор
      08.06.2022 22:22

      Покопавшись в коде PyYAML (версия 6.0) можно заметить что в yaml.safe_dump нельзя передать свой Dumper в отличии yaml.dump

      yaml.safe_dump в обязательном порядке использует свой SafeDumper и это нельзя переопределить. Однако вам ни кто не запрещает построить свой Dumper наследуя от SafeDumper. И уже его передать в yaml.dump.

      class IndentSafeDumper(yaml.SafeDumper):
          def increase_indent(self, flow=False, indentless=False):
              return super(IndentSafeDumper, self).increase_indent(flow, False)
      

      В итоге тот же результат, но с использованием SafeDumper