msgspec: быстрый и экономичный парсинг JSON на Python

Kate

Administrator
Команда форума
В библиотеке msgspec много функций, например кодирование, поддержка MessagePack (альтернативный формат, который быстрее JSON) и другие. Если вы регулярно парсите файлы JSON, и у вас проблемы с производительностью или памятью, или просто нужны встроенные схемы, то попробуйте msgspec.


Ниже рассказываем о библиотеке подробнее. Итак, чтобы обработать большой файл JSON на Python без сбоев и аварийного завершения, нужно:


  1. Убедиться, что используется не слишком много памяти.
  2. Спарсить файл как можно быстрее.
  3. В идеале также заранее убедиться, что данные валидны и имеют правильную структуру.

Конечно, можно объединить решения с несколькими библиотеками. А можно — всего с одной. Схемы, быстрый парсинг и хитрые приемы для уменьшения потребления памяти — все это новая библиотека 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 json420 мс136 Мб❌❌
orjson280 мс114 Мб❌❌
ijson300 мс14 Мб❌
msgspec90 мс39 Мб❌



При потоковом решении для парсинга всегда используется постоянный объём памяти. В остальных решениях потребление памяти зависит от размера входных данных. У msgspec потребление памяти значительно меньше, и это решение намного быстрее.


Плюсы и минусы парсинга со схемой​


В msgspec, указав схему, можно создавать объекты Python только для нужных нам полей. То есть потребление оперативной памяти меньше, а декодирование быстрее. Не нужно тратить время или память на создание тысяч бесполезных объектов Python.


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


С другой стороны:


  • Потребление памяти при декодировании по-прежнему зависит от входного файла. А в потоковых парсерах JSON, таких как ijson, можно использовать постоянную память во время парсинга, каким бы большим ни был входной файл.
  • Указание схемы подразумевает написание большего объёма кода и меньшую гибкость при работе с неполными данными.

 
Сверху