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