Обработка больших файлов JSON в PHP

Я пытаюсь обработать несколько больших (возможно, до 200M) файлов JSON. Структура файла в основном массив объектов.

Так что-то вроде:

[
  {"property":"value", "property2":"value2"},
  {"prop":"val"},
  ...
  {"foo":"bar"}
]

Каждый объект имеет произвольные свойства и не обязательно обменивается ими с другими объектами в массиве (как, например, имея то же самое).

Я хочу применить обработку к каждому объекту в массиве, и, поскольку файл потенциально огромен, я не могу отбросить все содержимое файла в памяти, расшифровывая JSON и перебирая массив PHP.

Поэтому в идеале я хотел бы прочитать файл, получить достаточно информации для каждого объекта и обработать его. Подход типа SAX был бы в порядке, если бы существовала подобная библиотека, доступная для JSON.

Любое предложение о том, как решить эту проблему лучше всего?

6 ответов

Решение

Я решил поработать над парсером на основе событий. Это еще не совсем сделано, и я отредактирую вопрос со ссылкой на мою работу, когда я выложу удовлетворительную версию.

РЕДАКТИРОВАТЬ:

Я наконец разработал версию парсера, которой я доволен. Это доступно на GitHub:

https://github.com/kuma-giyomu/JSONParser

Вероятно, есть место для некоторого улучшения, и я приветствую отзывы.

Я написал потоковый анализатор JSON pull для https://github.com/pcrov/JsonReader для PHP 7 с API, основанным на XMLReader.

Он значительно отличается от анализаторов на основе событий тем, что вместо настройки обратных вызовов и предоставления возможности анализатору выполнять свои функции, вы вызываете методы для анализа, чтобы перемещаться или извлекать данные по желанию. Нашли нужные вам биты и хотите прекратить разбор? Тогда прекратите разбор (и позвоните close() потому что это хорошая вещь.)

(Немного более длинный обзор парсеров, основанных на событиях, по сравнению с событиями см. В моделях чтения XML: SAX по сравнению с синтаксическим анализатором XML.)


Пример 1:

Читайте каждый объект целиком из вашего JSON.

use pcrov\JsonReader\JsonReader;

$reader = new JsonReader();
$reader->open("data.json");

$reader->read(); // Outer array.
$depth = $reader->depth(); // Check in a moment to break when the array is done.
$reader->read(); // Step to the first object.
do {
    print_r($reader->value()); // Do your thing.
} while ($reader->next() && $reader->depth() > $depth); // Read each sibling.

$reader->close();

Выход:

Array
(
    [property] => value
    [property2] => value2
)
Array
(
    [prop] => val
)
Array
(
    [foo] => bar
)

Объекты возвращаются в виде массивов со строковыми ключами из-за (частично) крайних случаев, когда действительный JSON будет производить имена свойств, которые не разрешены в объектах PHP. Обойти эти конфликты не стоит, так как анемичный объект stdClass в любом случае не имеет значения по сравнению с простым массивом.


Пример 2:

Читайте каждый названный элемент индивидуально.

$reader = new pcrov\JsonReader\JsonReader();
$reader->open("data.json");

while ($reader->read()) {
    $name = $reader->name();
    if ($name !== null) {
        echo "$name: {$reader->value()}\n";
    }
}

$reader->close();

Выход:

property: value
property2: value2
prop: val
foo: bar

Пример 3:

Прочитайте каждое свойство данного имени. Бонус: чтение из строки вместо URI, плюс получение данных из свойств с повторяющимися именами в одном и том же объекте (что допустимо в JSON, как весело.)

$json = <<<'JSON'
[
    {"property":"value", "property2":"value2"},
    {"foo":"foo", "foo":"bar"},
    {"prop":"val"},
    {"foo":"baz"},
    {"foo":"quux"}
]
JSON;

$reader = new pcrov\JsonReader\JsonReader();
$reader->json($json);

while ($reader->read("foo")) {
    echo "{$reader->name()}: {$reader->value()}\n";
}

$reader->close();

Выход:

foo: foo
foo: bar
foo: baz
foo: quux

Как лучше всего прочитать ваш JSON, зависит от его структуры и того, что вы хотите с ним делать. Эти примеры должны дать вам место для начала.

Недавно я создал библиотеку под названием JSON Machine, которая эффективно анализирует непредсказуемо большие файлы JSON. Использование через простой foreach, Я использую это сам для своего проекта.

Пример:

foreach (JsonMachine::fromFile('employees.json') as $employee) {
    $employee['name']; // etc
}

Смотрите https://github.com/halaxa/json-machine

Существует что-то подобное, но только для C++ и Java. Если вы не можете получить доступ к одной из этих библиотек из PHP, в PHP нет реализации для этого, но json_read() насколько я знаю. Тем не менее, если json структурирован так просто, просто прочитать файл до следующего } а затем обработать JSON, полученный с помощью json_read(), Но вам лучше сделать это с буферизацией, например, читать 10 КБ, разделить на}, если не найдено, прочитать еще 10 КБ и обработать найденные значения. Затем прочитайте следующий блок и так далее..

Это простой потоковый анализатор для обработки больших документов JSON. Используйте его для анализа очень больших JSON-документов, чтобы избежать загрузки всего содержимого в память, как работает практически любой другой анализатор JSON для PHP.

https://github.com/salsify/jsonstreamingparser

Существует http://github.com/sfalvo/php-yajl/ Я сам не использовал его.

Я знаю, что уже упоминался парсер потоковой передачи JSON https://github.com/salsify/jsonstreamingparser. Но поскольку я недавно (иш) добавил к нему нового слушателя, чтобы попытаться упростить его использование из коробки, я подумал, что (для разнообразия) предоставлю некоторую информацию о том, что он делает...

Есть очень хорошая запись о базовом парсере на https://www.salsify.com/blog/engineering/json-streaming-parser-for-php, но проблема, с которой я столкнулся со стандартной настройкой, заключалась в том, что у вас всегда написать слушателя для обработки файла. Это не всегда простая задача, и она также может потребовать определенного обслуживания при изменении JSON. Итак, я написалRegexListener.

Основной принцип - позволить вам сказать, какие элементы вас интересуют (через выражение регулярного выражения), и дать ему обратный вызов, чтобы сказать, что делать, когда он найдет данные. Во время чтения JSON он отслеживает путь к каждому компоненту - аналогично структуре каталогов. Так/name/forename или для массивов /items/item/2/partid- это то, с чем сопоставляется регулярное выражение.

Пример (из источника на github)...

$filename = __DIR__.'/../tests/data/example.json';
$listener = new RegexListener([
    '/1/name' => function ($data): void {
        echo PHP_EOL."Extract the second 'name' element...".PHP_EOL;
        echo '/1/name='.print_r($data, true).PHP_EOL;
    },
    '(/\d*)' => function ($data, $path): void {
        echo PHP_EOL."Extract each base element and print 'name'...".PHP_EOL;
        echo $path.'='.$data['name'].PHP_EOL;
    },
    '(/.*/nested array)' => function ($data, $path): void {
        echo PHP_EOL."Extract 'nested array' element...".PHP_EOL;
        echo $path.'='.print_r($data, true).PHP_EOL;
    },
]);
$parser = new Parser(fopen($filename, 'r'), $listener);
$parser->parse();

Всего пара объяснений...

'/1/name' => function ($data)

Так что /1 является вторым элементом в массиве (на основе 0), поэтому он позволяет получить доступ к конкретным экземплярам элементов. /name это nameэлемент. Затем значение передается в закрытие как$data

"(/\d*)" => function ($data, $path )

Это выберет каждый элемент массива и передаст его по одному, поскольку он использует группу захвата, эта информация будет передана как $path. Это означает, что когда в файле присутствует набор записей, вы можете обрабатывать каждый элемент по одному. А также знать, какой элемент, без необходимости отслеживать.

Последний

'(/.*/nested array)' => function ($data, $path):

эффективно сканирует любые элементы, называемые nested array и передает каждый из них вместе с тем, где он находится в документе.

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

$filename = __DIR__.'/../tests/data/ratherBig.json';
$listener = new RegexListener();
$parser = new Parser(fopen($filename, 'rb'), $listener);
$listener->setMatch(["/total_rows" => function ($data ) use ($parser) {
    echo "/total_rows=".$data.PHP_EOL;
    $parser->stop();
}]);

Это экономит время, когда вас не интересует оставшийся контент.

Следует отметить, что они будут реагировать на контент, так что каждый из них запускается, когда найден конец соответствующего контента, и может быть в разном порядке. Но также и то, что парсер отслеживает только интересующий вас контент и отбрасывает все остальное.

Если вы обнаружите какие-либо интересные функции (иногда называемые ошибками), сообщите мне или сообщите о проблеме на странице github.

Другие вопросы по тегам