Очень простая в использовании и эффективная замена памяти для неэффективной итерации больших файлов или потоков JSON для PHP >=7.2. См. TL;DR. Никаких зависимостей в производстве, кроме необязательного ext-json
. README синхронно с кодом
НОВОЕ в версии 1.2.0
— Рекурсивная итерация.
<?php
use JsonMachineItems;
// this often causes Allowed Memory Size Exhausted,
// because it loads all the items in the JSON into memory
- $users = json_decode(file_get_contents('500MB-users.json'));
// this has very small memory footprint no matter the file size
// because it loads items into memory one by one
+ $users = Items::fromFile('500MB-users.json');
foreach ($users as $id => $user) {
// just process $user as usual
var_dump($user->name);
}
Произвольный доступ, такой как $users[42]
пока невозможен. Используйте вышеупомянутый foreach
и найдите элемент или используйте указатель JSON.
Подсчитайте элементы с помощью iterator_count($users)
. Помните, что для получения подсчета все равно придется выполнить внутреннюю итерацию, и, следовательно, это займет примерно то же время, что и итерация и подсчет вручную.
Требуется ext-json
, если он используется по умолчанию, но не требуется, если используется собственный декодер. См. Декодеры.
Следите за CHANGELOG.
JSON Machine — это эффективный, простой в использовании и быстрый синтаксический анализатор потока/вытягивания/инкрементного/ленивого (как бы вы его ни называли) JSON, основанный на генераторах, разработанных для непредсказуемо длинных потоков или документов JSON. Основные особенности:
foreach
. Никаких событий и обратных вызовов.json_decode
для декодирования элементов документа JSON. См. Декодеры. Допустим, fruits.json
содержит огромный документ JSON:
// fruits.json
{
"apple" : {
"color" : " red "
},
"pear" : {
"color" : " yellow "
}
}
Его можно разобрать следующим образом:
<?php
use JsonMachine Items ;
$ fruits = Items:: fromFile ( ' fruits.json ' );
foreach ( $ fruits as $ name => $ data ) {
// 1st iteration: $name === "apple" and $data->color === "red"
// 2nd iteration: $name === "pear" and $data->color === "yellow"
}
Анализ массива json вместо объекта json следует той же логике. Ключом в foreach будет числовой индекс элемента.
Если вы предпочитаете, чтобы JSON Machine возвращал массивы вместо объектов, используйте new ExtJsonDecoder(true)
в качестве декодера.
<?php
use JsonMachine JsonDecoder ExtJsonDecoder ;
use JsonMachine Items ;
$ objects = Items:: fromFile ( ' path/to.json ' , [ ' decoder ' => new ExtJsonDecoder ( true )]);
Если вы хотите перебирать только поддерево results
в этом fruits.json
:
// fruits.json
{
"results" : {
"apple" : {
"color" : " red "
},
"pear" : {
"color" : " yellow "
}
}
}
используйте JSON Pointer /results
в качестве опции pointer
:
<?php
use JsonMachine Items ;
$ fruits = Items:: fromFile ( ' fruits.json ' , [ ' pointer ' => ' /results ' ]);
foreach ( $ fruits as $ name => $ data ) {
// The same as above, which means:
// 1st iteration: $name === "apple" and $data->color === "red"
// 2nd iteration: $name === "pear" and $data->color === "yellow"
}
Примечание:
Значение
results
не загружается в память сразу, а только по одному элементуresults
за раз. В памяти всегда находится один элемент на уровне/поддереве, который вы в данный момент выполняете. Таким образом, потребление памяти постоянно.
Спецификация JSON Pointer также позволяет использовать дефис ( -
) вместо определенного индекса массива. JSON Machine интерпретирует его как подстановочный знак, который соответствует любому индексу массива (а не какому-либо ключу объекта). Это позволяет перебирать вложенные значения в массивах, не загружая весь элемент.
Пример:
// fruitsArray.json
{
"results" : [
{
"name" : " apple " ,
"color" : " red "
},
{
"name" : " pear " ,
"color" : " yellow "
}
]
}
Чтобы перебрать все цвета фруктов, используйте указатель JSON "/results/-/color"
.
<?php
use JsonMachine Items ;
$ fruits = Items:: fromFile ( ' fruitsArray.json ' , [ ' pointer ' => ' /results/-/color ' ]);
foreach ( $ fruits as $ key => $ value ) {
// 1st iteration:
$ key == ' color ' ;
$ value == ' red ' ;
$ fruits -> getMatchedJsonPointer () == ' /results/-/color ' ;
$ fruits -> getCurrentJsonPointer () == ' /results/0/color ' ;
// 2nd iteration:
$ key == ' color ' ;
$ value == ' yellow ' ;
$ fruits -> getMatchedJsonPointer () == ' /results/-/color ' ;
$ fruits -> getCurrentJsonPointer () == ' /results/1/color ' ;
}
Вы можете анализировать одно скалярное значение в любом месте документа так же, как и коллекцию. Рассмотрим этот пример:
// fruits.json
{
"lastModified" : " 2012-12-12 " ,
"apple" : {
"color" : " red "
},
"pear" : {
"color" : " yellow "
},
// ... gigabytes follow ...
}
Получите скалярное значение ключа lastModified
следующим образом:
<?php
use JsonMachine Items ;
$ fruits = Items:: fromFile ( ' fruits.json ' , [ ' pointer ' => ' /lastModified ' ]);
foreach ( $ fruits as $ key => $ value ) {
// 1st and final iteration:
// $key === 'lastModified'
// $value === '2012-12-12'
}
Когда парсер находит значение и передает его вам, он прекращает анализ. Поэтому, когда одно скалярное значение находится в начале файла или потока размером в гигабайты, оно просто получает значение с начала в кратчайшие сроки и практически без использования памяти.
Очевидный ярлык:
<?php
use JsonMachine Items ;
$ fruits = Items:: fromFile ( ' fruits.json ' , [ ' pointer ' => ' /lastModified ' ]);
$ lastModified = iterator_to_array ( $ fruits )[ ' lastModified ' ];
Доступ к одному скалярному значению также поддерживает индексы массива в указателе JSON.
Также возможно анализировать несколько поддеревьев, используя несколько указателей JSON. Рассмотрим этот пример:
// fruits.json
{
"lastModified" : " 2012-12-12 " ,
"berries" : [
{
"name" : " strawberry " , // not a berry, but whatever ...
"color" : " red "
},
{
"name" : " raspberry " , // the same ...
"color" : " red "
}
],
"citruses" : [
{
"name" : " orange " ,
"color" : " orange "
},
{
"name" : " lime " ,
"color" : " green "
}
]
}
Чтобы перебрать все ягоды и цитрусовые, используйте указатели JSON ["/berries", "/citrus"]
. Порядок указателей не имеет значения. Элементы будут повторяться в порядке появления в документе.
<?php
use JsonMachine Items ;
$ fruits = Items:: fromFile ( ' fruits.json ' , [
' pointer ' => [ ' /berries ' , ' /citruses ' ]
]);
foreach ( $ fruits as $ key => $ value ) {
// 1st iteration:
$ value == [ " name " => " strawberry " , " color " => " red " ];
$ fruits -> getCurrentJsonPointer () == ' /berries ' ;
// 2nd iteration:
$ value == [ " name " => " raspberry " , " color " => " red " ];
$ fruits -> getCurrentJsonPointer () == ' /berries ' ;
// 3rd iteration:
$ value == [ " name " => " orange " , " color " => " orange " ];
$ fruits -> getCurrentJsonPointer () == ' /citruses ' ;
// 4th iteration:
$ value == [ " name " => " lime " , " color " => " green " ];
$ fruits -> getCurrentJsonPointer () == ' /citruses ' ;
}
Используйте RecursiveItems
вместо Items
, когда структуру JSON сложно или даже невозможно обработать с помощью Items
и указателей JSON или отдельные элементы, которые вы повторяете, слишком велики для обработки. С другой стороны, он заметно медленнее, чем Items
, так что имейте это в виду.
Когда RecursiveItems
встречает список или словарь в JSON, он возвращает новый экземпляр самого себя, который затем можно перебрать, и цикл повторяется. Таким образом, он никогда не возвращает массив или объект PHP, а только скалярные значения или RecursiveItems
. Ни один словарь или список JSON никогда не будут полностью загружены в память сразу.
Давайте посмотрим пример со многими-многими пользователями и многими-многими друзьями:
// users.json
[
{
"username" : " user " ,
"e-mail" : " [email protected] " ,
"friends" : [
{
"username" : " friend1 " ,
"e-mail" : " [email protected] "
},
{
"username" : " friend2 " ,
"e-mail" : " [email protected] "
}
]
}
]
<?php
use JsonMachine RecursiveItems
$ users = RecursiveItems:: fromFile ( ' users.json ' );
foreach ( $ users as $ user ) {
/** @var $user RecursiveItems */
foreach ( $ user as $ field => $ value ) {
if ( $ field === ' friends ' ) {
/** @var $value RecursiveItems */
foreach ( $ value as $ friend ) {
/** @var $friend RecursiveItems */
foreach ( $ friend as $ friendField => $ friendValue ) {
$ friendField == ' username ' ;
$ friendValue == ' friend1 ' ;
}
}
}
}
}
Если вы прервете итерацию такого ленивого более глубокого уровня (т. е. пропустите некоторых
"friends"
с помощьюbreak
) и перейдете к следующему значению (т. е. следующемуuser
), вы не сможете повторить его позже. JSON Machine должна выполнить итерацию в фоновом режиме, чтобы иметь возможность прочитать следующее значение. Такая попытка приведет к закрытию исключения генератора.
RecursiveItems
toArray(): array
Если вы уверены, что определенный экземпляр RecursiveItems указывает на структуру данных, управляемую памятью (например, $friend), вы можете вызвать $friend->toArray()
, и элемент материализуется в простой PHP-массив.
advanceToKey(int|string $key): scalar|RecursiveItems
При поиске определенного ключа в коллекции (например, 'friends'
в $user
) вам не нужно использовать цикл и условие для его поиска. Вместо этого вы можете просто вызвать $user->advanceToKey("friends")
. Он выполнит итерацию за вас и вернет значение по этому ключу. Звонки могут быть объединены в цепочку. Он также поддерживает синтаксис типа массива для перехода к следующим индексам и получения их. Таким образом, $user['friends']
будет псевдонимом для $user->advanceToKey('friends')
. Звонки могут быть объединены в цепочку. Имейте в виду, что это всего лишь псевдоним — вы не сможете получить произвольный доступ к предыдущим индексам после использования его непосредственно в RecursiveItems
. Это просто синтаксический сахар. Используйте toArray()
если вам нужен произвольный доступ к индексам записи/элемента.
Таким образом, предыдущий пример можно упростить следующим образом:
<?php
use JsonMachine RecursiveItems
$ users = RecursiveItems:: fromFile ( ' users.json ' );
foreach ( $ users as $ user ) {
/** @var $user RecursiveItems */
foreach ( $ user [ ' friends ' ] as $ friend ) { // or $user->advanceToKey('friends')
/** @var $friend RecursiveItems */
$ friendArray = $ friend -> toArray ();
$ friendArray [ ' username ' ] === ' friend1 ' ;
}
}
Цепочка позволяет вам сделать что-то вроде этого:
<?php
use JsonMachine RecursiveItems
$ users = RecursiveItems:: fromFile ( ' users.json ' );
$ users [ 0 ][ ' friends ' ][ 1 ][ ' username ' ] === ' friend2 ' ;
RecursiveItems implements RecursiveIterator
Таким образом, вы можете использовать, например, встроенные инструменты PHP для работы с RecursiveIterator
, например:
Это способ обращения к одному элементу в документе JSON. См. JSON Pointer RFC 6901. Это очень удобно, потому что иногда структура JSON уходит глубже, и вам нужно перебирать поддерево, а не основной уровень. Таким образом, вы просто указываете указатель на массив или объект JSON (или даже на скалярное значение), которое хотите перебрать, и все готово. Когда парсер попадает в указанную вами коллекцию, начинается итерация. Вы можете передать его как опцию pointer
во всех функциях Items::from*
. Если вы укажете указатель на несуществующую позицию в документе, будет выдано исключение. Его также можно использовать для доступа к скалярным значениям. Указатель JSON сам по себе должен быть допустимой строкой JSON . Буквальное сравнение ссылочных токенов (частей между косыми чертами) выполняется с ключами/именами членов документа JSON.
Несколько примеров:
Значение указателя JSON | Будет перебирать |
---|---|
(пустая строка – по умолчанию) | ["this", "array"] или {"a": "this", "b": "object"} будут повторяться (основной уровень) |
/result/items | {"result": {"items": ["this", "array", "will", "be", "iterated"]}} |
/0/items | [{"items": ["this", "array", "will", "be", "iterated"]}] (поддерживает индексы массива) |
/results/-/status | {"results": [{"status": "iterated"}, {"status": "also iterated"}]} (дефис как подстановочный знак индекса массива) |
/ (попался! — косая черта, за которой следует пустая строка, см. спецификацию) | {"":["this","array","will","be","iterated"]} |
/quotes" | {"quotes"": ["this", "array", "will", "be", "iterated"]} |
Параметры могут изменить способ анализа JSON. Массив опций — второй параметр всех Items::from*
. Доступные варианты:
pointer
— строка указателя JSON, указывающая, какую часть документа вы хотите просмотреть.decoder
— экземпляр интерфейса ItemDecoder
.debug
— true
или false
чтобы включить или отключить режим отладки. Когда режим отладки включен, такие данные, как строка, столбец и позиция в документе, доступны во время анализа или в исключениях. Отключение отладки дает небольшое преимущество в производительности. Ответ API потока или любой другой поток JSON анализируется точно так же, как и файл. Единственная разница в том, что для этого вы используете Items::fromStream($streamResource)
, где $streamResource
— это ресурс потока с документом JSON. Остальное то же, что и при разборе файлов. Вот несколько примеров популярных http-клиентов, поддерживающих потоковую передачу ответов:
Guzzle использует свои собственные потоки, но их можно преобразовать обратно в потоки PHP, вызвав GuzzleHttpPsr7StreamWrapper::getResource()
. Передайте результат этой функции в функцию Items::fromStream
, и все готово. См. рабочий пример GuzzleHttp.
Потоковый ответ Symfony HttpClient работает как итератор. А поскольку JSON Machine основан на итераторах, интеграция с Symfony HttpClient очень проста. См. пример HttpClient.
debug
) Анализ больших документов может занять некоторое время. Вызовите Items::getPosition()
в своем foreach
чтобы получить текущее количество обработанных байтов с самого начала. Процент затем легко вычислить как position / total * 100
. Чтобы узнать общий размер вашего документа в байтах, вы можете проверить:
strlen($document)
если вы анализируете строкуfilesize($file)
если вы анализируете файлContent-Length
если вы анализируете ответ HTTP-потока Если debug
отключена, getPosition()
всегда возвращает 0
.
<?php
use JsonMachine Items ;
$ fileSize = filesize ( ' fruits.json ' );
$ fruits = Items:: fromFile ( ' fruits.json ' , [ ' debug ' => true ]);
foreach ( $ fruits as $ name => $ data ) {
echo ' Progress: ' . intval ( $ fruits -> getPosition () / $ fileSize * 100 ) . ' % ' ;
}
Функции Items::from*
также принимают опцию decoder
. Это должен быть экземпляр JsonMachineJsonDecoderItemDecoder
. Если ничего не указано, по умолчанию используется ExtJsonDecoder
. Для этого требуется наличие расширения PHP ext-json
, поскольку оно использует json_decode
. Если json_decode
не делает то, что вы хотите, реализуйте JsonMachineJsonDecoderItemDecoder
и создайте свой собственный.
ExtJsonDecoder
— по умолчанию. Использует json_decode
для декодирования ключей и значений. Конструктор имеет те же параметры, что и json_decode
.
PassThruDecoder
— не декодирует. И ключи, и значения создаются в виде чистых строк JSON. Полезно, если вы хотите проанализировать элемент JSON с чем-то еще непосредственно в foreach и не хотите реализовывать JsonMachineJsonDecoderItemDecoder
. Начиная с 1.0.0
json_decode
не используется.
Пример:
<?php
use JsonMachine JsonDecoder PassThruDecoder ;
use JsonMachine Items ;
$ items = Items:: fromFile ( ' path/to.json ' , [ ' decoder ' => new PassThruDecoder ]);
ErrorWrappingDecoder
— декоратор, который оборачивает ошибки декодирования внутри объекта DecodingError
, что позволяет вам пропускать некорректные элементы вместо того, чтобы умирать из-за исключения SyntaxError
. Пример: <?php
use JsonMachine Items ;
use JsonMachine JsonDecoder DecodingError ;
use JsonMachine JsonDecoder ErrorWrappingDecoder ;
use JsonMachine JsonDecoder ExtJsonDecoder ;
$ items = Items:: fromFile ( ' path/to.json ' , [ ' decoder ' => new ErrorWrappingDecoder ( new ExtJsonDecoder ())]);
foreach ( $ items as $ key => $ item ) {
if ( $ key instanceof DecodingError || $ item instanceof DecodingError) {
// handle error of this malformed json item
continue ;
}
var_dump ( $ key , $ item );
}
Начиная с версии 0.4.0 каждое исключение расширяет JsonMachineException
, поэтому вы можете перехватить его, чтобы отфильтровать любую ошибку из библиотеки JSON Machine.
Если где-либо в потоке json возникает ошибка, выдается исключение SyntaxError
. Это очень неудобно, потому что если внутри одного элемента json есть ошибка, вы не сможете проанализировать остальную часть документа из-за одного неправильного элемента. ErrorWrappingDecoder
— декоратор декодера, который может вам в этом помочь. Оберните им декодер, и все некорректные элементы, которые вы повторяете, будут переданы вам в foreach через DecodingError
. Таким образом, вы можете пропустить их и продолжить работу с документом. См. пример в разделе «Доступные декодеры». Однако синтаксические ошибки в структуре потока json между повторяемыми элементами все равно будут вызывать исключение SyntaxError
.
Временная сложность всегда равна O(n)
TL;DR: сложность памяти равна O(2)
JSON Machine считывает из потока (или файла) по 1 элементу JSON за раз и генерирует соответствующий 1 элемент PHP за раз. Это наиболее эффективный способ, потому что если бы у вас было, скажем, 10 000 пользователей в файле JSON и вы хотели бы проанализировать его с помощью json_decode(file_get_contents('big.json'))
, у вас была бы вся строка в памяти, а также все 10 000 PHP-структуры. В следующей таблице показана разница:
Строковые элементы в памяти одновременно | Декодированные элементы PHP в памяти одновременно | Общий | |
---|---|---|---|
json_decode() | 10000 | 10000 | 20000 |
Items::from*() | 1 | 1 | 2 |
Это означает, что машина JSON всегда эффективна для любого размера обрабатываемого JSON. 100 ГБ без проблем.
TL;DR: сложность памяти равна O(n+1)
Также существует метод Items::fromString()
. Если вам приходится анализировать большую строку, а поток недоступен, JSON Machine может быть лучше, чем json_decode
. Причина в том, что в отличие от json_decode
, JSON Machine по-прежнему обрабатывает строку JSON по одному элементу за раз и не загружает все результирующие структуры PHP в память одновременно.
Продолжим пример с 10 000 пользователей. На этот раз они все находятся в памяти в виде строки. При декодировании этой строки с помощью json_decode
в памяти создается 10 000 массивов (объектов), а затем возвращается результат. JSON Machine, с другой стороны, создает единую структуру для каждого найденного элемента в строке и возвращает ее вам. Когда вы обрабатываете этот элемент и переходите к следующему, создается еще одна структура. Это то же самое поведение, что и с потоками/файлами. Следующая таблица представляет эту концепцию в перспективе:
Строковые элементы в памяти одновременно | Декодированные элементы PHP в памяти одновременно | Общий | |
---|---|---|---|
json_decode() | 10000 | 10000 | 20000 |
Items::fromString() | 10000 | 1 | 10001 |
Реальность еще лучше. Items::fromString
потребляет примерно в 5 раз меньше памяти, чем json_decode
. Причина в том, что структура PHP занимает гораздо больше памяти, чем соответствующее ей представление JSON.
Одной из причин может быть то, что элементы, которые вы хотите перебрать, находятся в каком-то подразделе, например "results"
но вы забыли указать указатель JSON. См. Анализ поддерева.
Другая причина может заключаться в том, что один из элементов, которые вы повторяете, сам по себе настолько огромен, что его невозможно декодировать сразу. Например, вы перебираете пользователей, и у одного из них есть тысячи объектов «друзей». Наиболее эффективное решение — использовать рекурсивную итерацию.
Вероятно, это означает, что одна скалярная строка JSON слишком велика, чтобы поместиться в памяти. Например, очень большой файл в кодировке Base64. В этом случае вам, вероятно, все равно не повезет, пока JSON Machine не поддержит выдачу скалярных значений в виде потоков PHP.
composer require halaxa/json-machine
Клонируйте или загрузите этот репозиторий и добавьте в свой загрузочный файл следующее:
spl_autoload_register ( require ' /path/to/json-machine/src/autoloader.php ' );
Клонируйте этот репозиторий. Эта библиотека поддерживает два подхода к разработке:
Запустите composer run -l
в каталоге проекта, чтобы просмотреть доступные сценарии разработки. Таким образом, вы можете выполнить некоторые этапы процесса сборки, например тесты.
Установите Docker и запустите make
в каталоге проекта на вашем хост-компьютере, чтобы просмотреть доступные инструменты/команды разработки. Вы можете запустить все этапы процесса сборки по отдельности, а также весь процесс сборки сразу. Make в основном запускает сценарии разработки композитора внутри контейнеров в фоновом режиме.
make build
: Запускает полную сборку. Эта же команда выполняется через GitHub Actions CI.
Вам нравится эта библиотека? Отмечайте, делитесь, показывайте :) Проблемы и запросы на включение очень приветствуются.
Апач 2.0
Элемент шестеренки: значки, созданные TutsPlus с сайта www.flaticon.com, лицензируются CC 3.0 BY.
Оглавление, созданное с помощью markdown-toc