Сравнительный анализ библиотек JSON для больших полезных нагрузок
Делиться

Введение
Представьте, что маркетинговая кампания, которую вы организовали к «Чёрной пятнице», имела огромный успех, и клиенты хлынули на ваш сайт. Ваша конфигурация Mixpanel, которая обычно обрабатывает около 1000 событий с клиентами в час, в итоге обрабатывает миллионы событий с клиентами в течение часа. Таким образом, ваш конвейер данных теперь должен обрабатывать огромные объёмы JSON-данных и сохранять их в базе данных. Вы видите, что ваша стандартная библиотека для парсинга JSON не способна масштабироваться в соответствии с резким ростом объёма данных, и ваши аналитические отчёты, работающие практически в режиме реального времени, отстают. Именно здесь вы осознаёте важность эффективной библиотеки для парсинга JSON. Помимо обработки больших объёмов данных, библиотеки для парсинга JSON должны уметь сериализовать и десериализовать JSON-данные с высокой степенью вложенности.
В этой статье мы рассмотрим библиотеки Python для парсинга больших объёмов данных. Мы уделим особое внимание возможностям ujson, orjson и ijson. Затем мы проведём сравнительный анализ производительности сериализации и десериализации стандартной библиотеки JSON (stdlib/json), ujson и orjson. Поскольку в статье мы используем термины «сериализация» и «десериализация», освежим в памяти эти понятия. Сериализация подразумевает преобразование объектов Python в строку JSON, тогда как десериализация подразумевает пересоздание строки JSON из структур данных Python.
По мере прочтения статьи вы найдёте диаграмму процесса принятия решений, которая поможет вам выбрать парсер, исходя из вашего рабочего процесса и индивидуальных потребностей в парсинге. Кроме того, мы рассмотрим NDJSON и библиотеки для парсинга полезных данных NDJSON. Итак, приступим.
Stdlib JSON
Stdlib JSON поддерживает сериализацию всех основных типов данных Python, включая словари, списки и кортежи. При вызове функции json.loads() весь JSON-файл загружается в память одновременно. Это приемлемо для небольших объёмов данных, но для более крупных объёмов данных json.loads() может вызывать критические проблемы с производительностью, такие как ошибки нехватки памяти и остановку последующих рабочих процессов.
import json with open(«large_payload.json», «r») as f: json_data = json.loads(f) #загружает весь файл в память, все токены одновременно
айсон
Для данных объёмом порядка сотен мегабайт рекомендуется использовать ijson. ijson (сокращение от «iterative json») считывает файлы по одному токену за раз, не занимая при этом память. В приведённом ниже коде мы сравниваем json и ijson.
#Библиотека ijson считывает записи по одному токену за раз import ijson with open(«json_data.json», «r») as f: for record in ijson.items(f, «items.item»): #извлечь один словарь из массива process(record)
Как видите, ijson извлекает по одному элементу из JSON и загружает их в объект словаря Python. Затем он передаётся вызывающей функции, в данном случае функции process(record). Общая схема работы ijson показана на иллюстрации ниже.

уйсон

Библиотека Ujson широко используется во многих приложениях, работающих с большими объёмами данных JSON, поскольку она была разработана как более быстрая альтернатива стандартной библиотеке JSON в Python. Скорость парсинга очень высокая, поскольку базовый код ujson написан на языке C с привязками Python, подключаемыми к интерфейсу Python. Области, требующие улучшения в стандартной библиотеке JSON, были оптимизированы в Ujson для скорости и производительности. Однако Ujson больше не используется в новых проектах, поскольку сами создатели библиотеки упомянули на PyPI, что она переведена в режим «только для обслуживания». Ниже представлена иллюстрация процессов ujson на высоком уровне.
import ujson taxonomy_data = '{«id»:1, «genus»:»Thylacinus», «species»:»cynocephalus», «extinct»: true}' data_dict = ujson.loads(taxonomy_data) #Десериализация с помощью open(«taxonomy_data.json», «w») как fh: #Сериализация ujson.dump(data_dict, fh) с помощью open(«taxonomy_data.json», «r») как fh: #Десериализация data = ujson.load(fh) print(data)
Переходим к следующей потенциальной библиотеке под названием «orjson».
орджсон
Поскольку Orjson написан на Rust, он оптимизирован не только для скорости, но и обладает безопасными для памяти механизмами для предотвращения переполнений буфера, с которыми сталкиваются разработчики при использовании JSON-библиотек на языке C, таких как ujson. Более того, Orjson поддерживает сериализацию нескольких дополнительных типов данных помимо стандартных типов данных Python, включая объекты dataclass и datetime. Ещё одно ключевое отличие orjson от других библиотек заключается в том, что функция dumps() в orjson возвращает объект bytes, тогда как другие библиотеки возвращают строку. Возврат данных в виде объекта bytes — одна из основных причин высокой производительности orjson.
import orjson book_payload = '{«id»:1,»name»:»Великий Гэтсби»,»author»:»Ф. Скотт Фицджеральд»,»Издательство»:»Сыновья Чарльза Скрибнера»}' data_dict = orjson.loads(book_payload) #Десериализует print(data_dict) с open(«book_data.json», «wb») как f: #Сериализует f.write(orjson.dumps(data_dict)) #Возвращает объект bytes с open(«book_data.json», «rb») как f:#Десериализует book_data = orjson.loads(f.read()) print(book_data)
Теперь, когда мы изучили некоторые библиотеки анализа JSON, давайте протестируем их возможности сериализации.
Тестирование возможностей сериализации JSON, ujson и orjson
Мы создаем пример объекта dataclass с целым числом, строкой и переменной datetime.
из dataclasses импорт dataclass из datetime импорт datetime @dataclass класс Пользователь: id: int имя: str создан: datetime u = Пользователь(id=1, name=»Thomas», created=datetime.now())
Затем мы передаём его каждой из библиотек, чтобы посмотреть, что произойдёт. Начнём с JSON-файла stdlib.
import json try: print(«json:», json.dumps(u)) except TypeError as e: print(«json error:», e)
Как и ожидалось, мы получаем следующую ошибку. (Стандартная библиотека JSON не поддерживает сериализацию объектов «dataclass» и объектов datetime.)

Далее мы проверим то же самое с помощью библиотеки ujson.
import ujson try: print(«json:», ujson.dumps(u)) except TypeError as e: print(«json error:», e)

Как видно выше, ujson не может сериализовать объект класса данных и тип данных datetime. Поэтому для сериализации мы используем библиотеку orjson.
import orjson try: print(«orjson:», orjson.dumps(u)) except TypeError as e: print(«orjson error:», e)
Мы видим, что orjson смог сериализовать как типы данных dataclass, так и datetime.

Работа с NDJSON (особое упоминание)
Мы рассмотрели библиотеки для парсинга JSON, но что насчёт NDJSON? NDJSON (Newline Delimited JSON), как вы, возможно, знаете, — это формат, в котором каждая строка представляет собой JSON-объект. Другими словами, разделителем служит не запятая, а символ перевода строки. Например, вот как выглядит NDJSON.
{«id»: «A13434», «name»: «Элла»} {«id»: «A13455», «name»: «Шармон»} {«id»: «B32434», «name»: «Ареида»}
NDJSON в основном используется для журналов и потоковых данных, поэтому полезные данные NDJSON отлично подходят для анализа с помощью библиотеки ijson. Для небольших и средних объёмов данных NDJSON рекомендуется использовать библиотеку stdlib JSON. Помимо ijson и stdlib JSON, существует специальная библиотека NDJSON. Ниже приведены фрагменты кода, демонстрирующие каждый из подходов.
NDJSON с использованием stdlib JSON и ijson
Поскольку NDJSON не разделён запятыми, он не подходит для массовой загрузки, поскольку stdlib json ожидает увидеть список словарей. Другими словами, парсер stdlib JSON ищет один допустимый элемент JSON, но вместо этого получает несколько элементов JSON в файле полезной нагрузки. Следовательно, файл необходимо анализировать итеративно, построчно, и отправлять вызывающей функции для дальнейшей обработки.
import json ndjson_payload = «»»{«id»: «A13434», «name»: «Элла»} {«id»: «A13455», «name»: «Шармон»} {«id»: «B32434», «name»: «Арейда»}»»» #Запись файла NDJSON с помощью open(«json_lib.ndjson», «w», encoding=»utf-8″) as fh: for line in ndjson_payload.splitlines(): #Разделение строки на объект JSON fh.write(line.strip() + «n») #Запись каждого объекта JSON в виде строки #Чтение файла NDJSON с помощью json.loads с помощью open(«json_lib.ndjson», «r», encoding=»utf-8″) as fh: for line in fh: if line.strip(): #Удаление новых строк item= json.loads(line) #Десериализуем print(item) #или передаем его вызывающей функции
В ijson парсинг выполняется так, как показано ниже. В стандартном JSON у нас есть только один корневой элемент, который представляет собой либо словарь, если это отдельный JSON, либо массив, если это список словарей. Но в NDJSON каждая строка является отдельным корневым элементом. Аргумент «» в ijson.items() указывает парсеру ijson на необходимость проверки каждого корневого элемента. Аргументы «» и multiple_values=True сообщают парсеру ijson о наличии в файле нескольких корневых элементов JSON и необходимости извлекать по одной строке (каждый JSON) за раз.
import ijson ndjson_payload = «»»{«id»: «A13434», «name»: «Элла»} {«id»: «A13455», «name»: «Чармонт»} {«id»: «B32434», «name»: «Арейда»}»»» #Запись полезной нагрузки в файл для обработки ijson with open(«ijson_lib.ndjson», «w», encoding=»utf-8″) as fh: fh.write(ndjson_payload) with open(«ijson_lib.ndjson», «r», encoding=»utf-8″) as fh: for item in ijson.items(fh, «», multiple_values=True): print(item)
Наконец, у нас есть специальная библиотека NDJSON. Она, по сути, преобразует формат NDJSON в стандартный JSON.
import ndjson ndjson_payload = «»»{«id»: «A13434», «name»: «Ella»} {«id»: «A13455», «name»: «Charmont»} {«id»: «B32434», «name»: «Areida»}»»» #запись полезной нагрузки в файл для обработки ijson с помощью open(«ndjson_lib.ndjson», «w», encoding=»utf-8″) как fh: fh.write(ndjson_payload) с помощью open(«ndjson_lib.ndjson», «r», encoding=»utf-8″) как fh: ndjson_data = ndjson.load(fh) #возвращает список словарей
Как вы видели, файлы формата NDJSON обычно можно анализировать с помощью библиотеки json и ijson из stdlib. Для очень больших объёмов данных ijson — лучший выбор, поскольку он эффективно использует память. Но если вы хотите генерировать данные NDJSON из других объектов Python, библиотека NDJSON — идеальный выбор. Это связано с тем, что функция ndjson.dumps() автоматически преобразует объекты Python в формат NDJSON, не перебирая эти структуры данных.
Теперь, когда мы изучили NDJSON, давайте вернемся к сравнительному анализу библиотек stdlib json, ujson и orjson.
Причина, по которой IJSON не рассматривается для бенчмаркинга
Потоковый парсер ijson сильно отличается от анализаторов массового анализа, которые мы рассматривали. Если бы мы сравнивали ijson с этими парсерами массового анализа, мы бы сравнивали яблоки с апельсинами. Даже если бы мы сравнивали ijson с другими парсерами, у нас сложилось бы ложное впечатление, что ijson самый медленный, хотя на самом деле он служит совершенно другой цели. ijson оптимизирован для эффективного использования памяти и, следовательно, имеет более низкую пропускную способность, чем парсеры массового анализа.
Создание синтетической полезной нагрузки JSON для целей сравнительного анализа
Мы генерируем большой синтетический JSON-файл, содержащий 1 миллион записей, используя библиотеку «mimesis». Эти данные будут использоваться для бенчмаркинга библиотек. Приведённый ниже код можно использовать для создания полезной нагрузки для этого бенчмаркинга, если вы захотите повторить его. Размер сгенерированного файла составит от 100 до 150 МБ, что, на мой взгляд, достаточно для проведения бенчмаркинга.
from mimesis import Person, Address import json person_name = Person(«en») complete_address = Address(«en») # потоковая передача в файл с помощью open(«large_payload.json», «w») as fh: fh.write(«[«) # массив JSON for i in range(1_000_000): payload = { «id»: person_name.identifier(), «name»: person_name.full_name(), «email»: person_name.email(), «address»: { «street»: complete_address.street_name(), «city»: complete_address.city(), «postal_code»: complete_address.postal_code() } } json.dump(payload, fh) if i < 999_999: # Чтобы избежать запятой в последней записи fh.write(",") fh.write("]") # конец массива JSON
Ниже представлен пример того, как будут выглядеть сгенерированные данные. Как видите, поля адреса вложены друг в друга, чтобы JSON не только был большим по размеру, но и соответствовал реальным иерархическим JSON-данным.
[ { «id»: «8177», «name»: «Уильям Хейс», «email»: «[email protected]», «address»: { «street»: «Изумрудная бухта», «city»: «Краун-Пойнт», «postal_code»: «58293» } }, { «id»: «5931», «name»: «Куинн Грир», «email»: «[email protected]», «address»: { «street»: «Олон», «city»: «Бриджпорт», «postal_code»: «92982» } } ]
Начнем с бенчмаркинга.
Предпосылки для сравнительного анализа
Мы используем функцию read() для сохранения JSON-файла в виде строки. Затем мы используем функцию loads() в каждой из библиотек (json, ujson и orjson) для десериализации JSON-строки в объект Python. Сначала мы создаём объект payload_str из исходного JSON-текста.
с открытым(«large_payload1.json», «r») как fh: payload_str = fh.read() #необработанный текст JSON
Затем мы создаём функцию бенчмаркинга с двумя аргументами. Первый аргумент — это тестируемая функция. В данном случае это функция loads(). Второй аргумент — это payload_str, сконструированная из файла выше.
def benchmark_load(func, payload_str): start = time.perf_counter() for _ in range(3): func(payload_str) end = time.perf_counter() return end — start
Мы используем указанную выше функцию для проверки скорости сериализации и десериализации.
Сравнительный анализ скорости десериализации
Мы загружаем три тестируемые библиотеки. Затем мы запускаем функцию benchmark_load() для функции loads() каждой из этих библиотек.
импорт json, ujson, orjson, время результаты = { «json.loads»: benchmark_load(json.loads, payload_str), «ujson.loads»: benchmark_load(ujson.loads, payload_str), «orjson.loads»: benchmark_load(orjson.loads, payload_str), } для lib, t в results.items(): print(f»{lib}: {t:.4f} секунд»)
Как мы видим, наименьшее время десериализации потребовалось orjson.

Сравнительный анализ скорости сериализации
Далее мы тестируем скорость сериализации этих библиотек.
импорт json импорт ujson импорт orjson импорт время результаты = { «json.dumps»: benchmark(«json», json.dumps, payload_str), «ujson.dumps»: benchmark(«ujson», ujson.dumps, payload_str), «orjson.dumps»: benchmark(«orjson», orjson.dumps, payload_str), } для lib, t в results.items(): print(f»{lib}: {t:.4f} секунд»)
Сравнивая время выполнения, мы видим, что orjson тратит наименьшее количество времени на сериализацию объектов Python в объект JSON.

Выбор лучшей библиотеки JSON для вашего рабочего процесса

Хаки для буфера обмена и рабочих процессов JSON
Предположим, вы хотите просмотреть JSON-файл в текстовом редакторе, например, Notepad++, или поделиться фрагментом (из большого объёма данных) в Slack с коллегой по команде. Вы быстро столкнётесь со сбоями буфера обмена, текстового редактора или IDE. В таких ситуациях можно использовать Pyperclip или Tkinter. Pyperclip хорошо подходит для данных размером до 50 МБ, тогда как Tkinter — для данных среднего размера. Для больших объёмов данных можно записать JSON-файл для просмотра данных.
Заключение
JSON может показаться простым, но чем больше объём полезной нагрузки и чем выше уровень вложенности, тем быстрее эти нагрузки могут стать узким местом производительности. Цель этой статьи — показать, как каждая библиотека для парсинга Python решает эту проблему. При выборе библиотек для парсинга JSON скорость и пропускная способность не всегда являются главными критериями. Именно рабочий процесс определяет, требуется ли для парсинга полезной нагрузки пропускная способность, эффективность использования памяти или долгосрочная масштабируемость. Короче говоря, парсинг JSON не должен быть универсальным подходом.
Источник: towardsdatascience.com























