PHP. Парсинг больших сжатых JSON без распаковки - insbor.ru
insbor.ru

insbor.ru

Привет, это я

Читаю, пишу, перечитываю и исправляю.


Что здесь происходит


Предыдущие записи


PHP. Парсинг больших сжатых JSON без распаковки

Опубликовано :   |  Кем :   |  Категория :  PHP

Недавно потребовалось с помощью PHP импортировать в БД содержимое gzip-архива с json-файлом. В нём был массив некоторых однотипных объектов. Всё бы ничего, но размер архива 8ГБ, а файла в нём - 212ГБ. Mongo и т.п. не были доступны, только MySQL и PHP. А ещё на сервере не было достаточно места для распаковки архива.

Первым делом, я узнал о том, что для gzip информация о размере сжатого файла хранится в заголовке архива и может не соответствовать действительности. Т.е. вы не узнаете настоящего размера файла до окончания распаковки. Потом почитал о том, как люди решали подобные проблемы. Конечно же, крупные файлы в оперативную память никто не грузил. Вместо этого их парсили налету, читая один объект за другим.

Действительно, если условиться, что в json хранится массив, то его элементы можно читать последовательно, не храня в памяти весь файл и не пытаясь декодировать исходные данные целиком. В интернете уже есть несколько подходящих библиотек для PHP, например, salsify/jsonstreamingparser или MAXakaWIZARD/JsonCollectionParser, расширяющая первую. Они используют как раз такой принцип, однако мне не подошли, т.к. в секунду получалось обрабатывать всего два объекта. Этому есть две причины и обе видны на фрагменте кода ниже:

Во-первых, накапливание буфера чтения выполняется для каждого символа в строке. Как мы помним, входные данные могут занимать сотни гигабайт и переприсваивать многомегабайтные строки только для того, чтобы добавить к ним ещё один символ, нерационально. Во-вторых, реализовать полную логику парсинга JSON - это конечно хорошо и профессионально. Всегда лучше с недоверием относиться к входным данным и быть уверенным, что перед нами валидный текст. Но давайте будем реалистами. Мы получаем от партнёра дамп, в целостности которого он уверен. Да и найденный объект из текста всё равно будет десериализовывать функция json_decode(), а она от невалидных данных вернёт null, что очень легко отследить.

Чтобы избежать распаковки гигантского файла, я использовал функцию gzread(), она почти идентична fread(), в частности, нормально читает несжатые файлы.

Парсинг получаемого текста тоже можно упростить и ускорить. Нам достаточно искать для каждой открывающей объект скобки { её закрывающую пару }, затем функцией получения подстроки substr() брать весь текст между ними и передавать в json_decode(). Так мы избавимся от N переприсваиваний огромных строк и сложного парсинга объекта, который всё равно будет повторяться при декодировании модулем JSON

Так получилась небольшая библиотека va-fursenko/JsonFileParser (composer require viktorf/json-file-parser). Она состоит из простого класса Parser и интерфейса Listener, который должен реализовывать обработчик найденных объектов. Используется всё достаточно просто:

Теперь каждый найденный в файле объект будет обрабатываться слушателем, а в оперативной памяти одновременно будет храниться не больше $readBufferSize или максимальной длины текста с одним сериализованным объектом, в зависимости от того, что больше. Для оптимальной производительности лучше, если в одном $readBufferSize будет помещаться несколько объектов.

Для чего не подойдёт такое решение? Для разных хитрых невалидных входных данных, которые и так невозможно распознать любым способом. Во всех остальных случаях ускоренный парсинг и возможность доступа к архивам любой длины могут здорово выручить. В моём случае сжатый файл с более, чем миллионом объектов был успешно распознан на скорости примерно в 10 раз быстрее, чем для указанных выше библиотек.

Благодарю за внимание и удачных импортов!