В библиотеке 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 json 420 мс 136 Мб
orjson 280 мс 114 Мб
ijson 300 мс 14 Мб
msgspec 90 мс 39 Мб



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


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


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


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


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


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



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


  1. osmanpasha
    00.00.0000 00:00

    Хотелось бы подробностей, за счёт чего достигается такая эффективность.

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


    1. vanzhiganov
      00.00.0000 00:00
      +1

      Можете назвать примеры таких систем?


      1. centralhardware2
        00.00.0000 00:00

        Авторам библиотеки понадобитесь в районе двух лет чтобы полностью поддержать M1


      1. osmanpasha
        00.00.0000 00:00

        Ну вот из опыта (не про эту библиотеку): пакуешь свое приложение в маленький alpine docker-контейнер, а библиотеку на pypi никто не компилировал для musl, в итоге установка тянет еще ручную установку пару сотен Мб компиляторов, что ставит крест на идее маленького контейнера. Да, можно устроить многостадийную сборку контейнера и вообще ничего нерешаемого, но это и есть то, что я и назвал "может вызвать проблемы".

        Для Raspberry Pi тоже регулярно нет сборок, вот arm32 что-то не вижу в бинарниках msgspec, только arm64.