В библиотеке msgspec много функций, например кодирование, поддержка MessagePack (альтернативный формат, который быстрее JSON) и другие. Если вы регулярно парсите файлы JSON, и у вас проблемы с производительностью или памятью, или просто нужны встроенные схемы, то попробуйте msgspec.
Ниже рассказываем о библиотеке подробнее. Итак, чтобы обработать большой файл JSON на Python без сбоев и аварийного завершения, нужно:
- Убедиться, что используется не слишком много памяти.
- Спарсить файл как можно быстрее.
- В идеале также заранее убедиться, что данные валидны и имеют правильную структуру.
Конечно, можно объединить решения с несколькими библиотеками. А можно — всего с одной. Схемы, быстрый парсинг и хитрые приемы для уменьшения потребления памяти — все это новая библиотека msgspec.
json и orjson
Начнем с двух других библиотек: встроенного модуля json на Python и быстрой библиотеки orjson. Вернемся к примеру из моей статьи о потоковом парсинге JSON и спарсим файл размером ~25 Мб, в котором кодируется список объектов JSON (например, словарей). Это события GitHub и пользователи, выполняющие определенные действия с репозиториями:
[{"id":"2489651045","type":"CreateEvent","actor":{"id":665991,"login":"petroav","gravatar_id":"","url":"https://api.github.com/users/petroav","avatar_url":"https://avatars.githubusercontent.com/u/665991?"},"repo":{"id":28688495,"name":"petroav/6.828","url":"https://api.github.com/repos/petroav/6.828"},"payload":{"ref":"master","ref_type":"branch","master_branch":"master","description":"Solution to homework and assignments from MIT's 6.828 (Operating Systems Engineering). Done in my spare time.","pusher_type":"user"},"public":true,"created_at":"2015-01-01T15:00:00Z"},
...
]
Наша цель — выяснить, с какими репозиториями взаимодействовал пользователь.
Вот как это делается со встроенным модулем json стандартной библиотеки Python:
import json
with open("large.json", "r") as f:
data = json.load(f)
user_to_repos = {}
for record in data:
user = record["actor"]["login"]
repo = record["repo"]["name"]
if user not in user_to_repos:
user_to_repos[user] = set()
user_to_repos[user].add(repo)
print(len(user_to_repos), "records")
А вот так с orjson (отличается двумя строками):
import orjson
with open("large.json", "rb") as f:
data = orjson.loads(f.read())
user_to_repos = {}
for record in data:
# ... same as stdlib code ...
Вот сколько памяти и времени занимают эти два варианта:
$ /usr/bin/time -f "RAM: %M KB, Elapsed: %E" python stdlib.py
5250 records
RAM: 136464 KB, Elapsed: 0:00.42
$ /usr/bin/time -f "RAM: %M KB, Elapsed: %E" python with_orjson.py
5250 records
RAM: 113676 KB, Elapsed: 0:00.28
Потребление памяти одинаковое, но orjson быстрее — 280 мс против 420 мс.
Теперь рассмотрим msgspec.
msgspec: декодирование и кодирование на основе схемы для JSON
Вот соответствующий код с msgspec, здесь подход к парсингу несколько отличается:
from msgspec.json import decode
from msgspec import Struct
class Repo(Struct):
name: str
class Actor(Struct):
login: str
class Interaction(Struct):
actor: Actor
repo: Repo
with open("large.json", "rb") as f:
data = decode(f.read(), type=list[Interaction])
user_to_repos = {}
for record in data:
user = record.actor.login
repo = record.repo.name
if user not in user_to_repos:
user_to_repos[user] = set()
user_to_repos[user].add(repo)
print(len(user_to_repos), "records")
Этот код длиннее и подробнее, потому что msgspec позволяет определять схемы для записей, парсинг которых вы выполняете.
Очень полезно: схемы для всех полей не нужны. И, хотя в записях JSON много полей (смотрите в примере выше), мы указываем в msgspec только нужные нам.
Вот результат парсинга с msgspec:
$ /usr/bin/time -f "RAM: %M KB, Elapsed: %E" python with_msgspec.py
5250 records
RAM: 38612 KB, Elapsed: 0:00.09
Намного быстрее и гораздо меньше памяти.
В итоге у нас три решения и еще одно потоковое — ijson:
Пакет | Время | ОЗУ | Постоянная память | Схема |
---|---|---|---|---|
Stdlib json | 420 мс | 136 Мб | ❌ | ❌ |
orjson | 280 мс | 114 Мб | ❌ | ❌ |
ijson | 300 мс | 14 Мб | ✓ | ❌ |
msgspec | 90 мс | 39 Мб | ❌ | ✓ |
При потоковом решении для парсинга всегда используется постоянный объём памяти. В остальных решениях потребление памяти зависит от размера входных данных. У msgspec потребление памяти значительно меньше, и это решение намного быстрее.
Плюсы и минусы парсинга со схемой
В msgspec, указав схему, можно создавать объекты Python только для нужных нам полей. То есть потребление оперативной памяти меньше, а декодирование быстрее. Не нужно тратить время или память на создание тысяч бесполезных объектов Python.
Кроме того, проверку схемы мы получили просто так. Если в одной из записей не хватает поля или присутствует значение неверного типа, например целочисленного вместо строкового, парсер укажет бы на это, а в стандартных библиотеках JSON проверка схемы выполняется отдельно.
С другой стороны:
- Потребление памяти при декодировании по-прежнему зависит от входного файла. А в потоковых парсерах JSON, таких как ijson, можно использовать постоянную память во время парсинга, каким бы большим ни был входной файл.
- Указание схемы подразумевает написание большего объёма кода и меньшую гибкость при работе с неполными данными.
Data Science и Machine Learning
- Профессия Data Scientist
- Профессия Data Analyst
- Курс «Математика для Data Science»
- Курс «Математика и Machine Learning для Data Science»
- Курс по Data Engineering
- Курс «Machine Learning и Deep Learning»
- Курс по Machine Learning
Python, веб-разработка
- Профессия Fullstack-разработчик на Python
- Курс «Python для веб-разработки»
- Профессия Frontend-разработчик
- Профессия Веб-разработчик
Мобильная разработка
Java и C#
- Профессия Java-разработчик
- Профессия QA-инженер на JAVA
- Профессия C#-разработчик
- Профессия Разработчик игр на Unity
От основ — в глубину
А также
osmanpasha
Хотелось бы подробностей, за счёт чего достигается такая эффективность.
Ядро парсера, как я понял, написано на сях, что может вызвать проблемы при установке на не очень распространенных платформах.
vanzhiganov
Можете назвать примеры таких систем?
centralhardware2
Авторам библиотеки понадобитесь в районе двух лет чтобы полностью поддержать M1
osmanpasha
Ну вот из опыта (не про эту библиотеку): пакуешь свое приложение в маленький alpine docker-контейнер, а библиотеку на pypi никто не компилировал для musl, в итоге установка тянет еще ручную установку пару сотен Мб компиляторов, что ставит крест на идее маленького контейнера. Да, можно устроить многостадийную сборку контейнера и вообще ничего нерешаемого, но это и есть то, что я и назвал "может вызвать проблемы".
Для Raspberry Pi тоже регулярно нет сборок, вот arm32 что-то не вижу в бинарниках msgspec, только arm64.