Работа с YAML-файлами с использованием библиотеки PyYAML
YAML - кто ты такой?
YAML (YAML Ain't Markup Language) - популярный формат для хранения данных, в особенности, конфигов. К примеру, конфигов для gitlab CI, для хуков линтеров в pre-commit описываются в YAML.
Формат легко читается, для восприятия легче, чем JSON, особенно когда структура сложная. Кроме того, YAML легко преобразуется в dict и с ним удобно работать через код.
Расширение файла может выглядеть и как ”.yml”
и как “.yaml”
, разницы нет.
Синтаксис и структура
Напишем небольшой конфиг и разберемся, как прописывать разные типы данных в YAML-файле. Представим, что у нас есть несколько сервисов с параметрами и база данных:
common_host: &data host: localhost services: service_1: <<: *data port: 8080 is_available: true details: - "integration" - "fast" - "awesome" service_2: <<: *data port: 8020 is_available: true details: - "slow" - "not bad" service_3: <<: *data port: 7001 is_available: false details: - null database: <<: *data port: 5432 user: admin password: admin
Итак, разберем детально что тут происходит:
строки
. Можно писать как с кавычками ("integration") так и без (admin, localhost)словари
. Можно в одну строку:data: { ”key”: “value” }
<<: *data port: 5432 user: admin password: admin
Можно указать каждый элемент на новой строке через дефис:
- "integration" - "fast" - "awesome"
якоря
. Уникальная штука в YAML, которая позволяет избежать дублирования и дает возможность переиспользовать данные.
Для начала, якорь надо определить:
common_host: &data host: localhost
а потом ссылаться на него в других местах конфига:
service_3: <<: *data
и вместо этой ссылки будет подставлено значение host: localhost
.
Чтение и запись YAML-файлов с помощью библиотеки PyYaml
PyYaml
Очень популярная библиотека для работы с YAML. Она не встроенная, потому нам надо ее установить:
pip install pyyaml
Чтение YAML-файлов
Чтобы распарсить YAML-файл в словарь Python, в PyYaml присутствует функция safe_load()
. Есть еще функция load()
, но ее не рекомендуется использовать с данными из непроверенных источников, т.к. она может вызывать любой код на Python.
Давайте создадим файл config.yml
и сохраним туда наш конфиг.
Также создадим файл yaml_parser.py
, где и будем реализовывать логику работы с YAML.
Напишем функцию для чтения конфига:
from pathlib import Path from pprint import pprint import yaml CWD = Path(__file__).parent CONFIG_PATH = Path(CWD / "config.yml") def open_config(config_path: Path = CONFIG_PATH) -> dict: if Path.exists(config_path) and config_path.is_file(): with config_path.open(mode="r", encoding="utf-8") as file: return yaml.safe_load(file) raise FileNotFoundError("Please check config file path!")
- С помощью библиотеки pathlib определяем текущую директорию (
CWD
) и путь до конфига (CONFIG_PATH
) - Внутри функции
def open_config()
проверяем, что объект по указанному в аргументеconfig_path
пути существует и является файлом. Если какое-то из этих условий не выполняется, вызываем исключениеraise FileNotFoundError
, страхуя себя от лишних ошибок - Далее открываем файл с помощью контекстного менеджера в режиме чтения, преобразуем наш конфиг в словарь и возвращаем его.
Посмотрим, что получится при вызове этой функции:
config_1 = open_config() pprint(config_1, sort_dicts=False) > {'common_host': {'host': 'localhost'}, 'services': {'service_1': {'host': 'localhost', 'port': 8080, 'is_available': True, 'details': ['integration', 'fast', 'awesome']}, 'service_2': {'host': 'localhost', 'port': 8020, 'is_available': True, 'details': ['slow', 'not bad']}, 'service_3': {'host': 'localhost', 'port': 7001, 'is_available': False, 'details': [None]}}, 'database': {'host': 'localhost', 'port': 5432, 'user': 'admin', 'password': 'admin'}}
Итак, все работает! Теперь наш конфиг преобразован в словарь и с ним можно работать в коде.
Согласитесь, если описывать конфиги именно в таком формате, то читать их будет посложнее, чем YAML.
Запись YAML-файлов
Ну и финальное. Попробуем обратную процедуру, преобразуем и запишем словарь в YAML-файл.
Попробуем обратно преобразовать тот же конфиг из dict в YAML, затем сравним файлы и убедимся, что данные идентичны. Поехали!
Напишем функцию для записи словаря в YAML-файл:
def write_config(filename: Path, data: dict) -> None: with Path(CWD / filename).open(mode="w") as file: yaml.safe_dump(data=data, stream=file, sort_keys=False)
- Функция принимает имя нового файла, включая расширение (
filename
) и словарь, который будем преобразовывать и сохранять (data
) - С помощью контекстного менеджера открываем (и создаем, если файла нет) файл в режиме записи
- с помощью функции
safe_dump()
преобразовываем и сохраняем данные без сортировки по ключам
Вызовем функцию и посмотрим, что происходит:
new_config_path = Path("new_config.yml") write_config(filename=new_config_path, data=config_1)
После выполнения кода, в той же директории рядом с исходным файлом будет создан файл new_config.yml
с таким содержимым:
common_host: host: localhost services: service_1: host: localhost port: 8080 is_available: true details: - integration - fast - awesome service_2: host: localhost port: 8020 is_available: true details: - slow - not bad service_3: host: localhost port: 7001 is_available: false details: - null database: host: localhost port: 5432 user: admin password: admin
Структура и данные абсолютно идентичны, мы справились с задачей!
Есть лишь маленькое “но”, здесь не будет якорей или явного указания строк в кавычках, могут немного скорректироваться отступы.
Давайте убедимся, что наши файлы идентичны на все 100%. Откроем оба сохраненных конфига и проверим их на равенство с помощью assert
:
config_1 = open_config(config_path=CONFIG_PATH) config_2 = open_config(config_path=new_config_path) assert config_1 == config_2, "Config dicts are not equal!"
В случае неравенства будет вызвано исключение AssertionError
с указанным текстом ошибки.