在PHP中处理大型JSON文件

时间:2010-10-29 06:10:35

标签: php json large-files

我正在尝试处理稍大(可能高达200M)的JSON文件。 该文件的结构基本上是一个对象数组。

这就是:

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

每个对象都有任意属性,并且不需要与数组中的其他对象共享它们(例如,具有相同的对象)。

我想对数组中的每个对象应用处理,因为文件可能很大,我不能在内存中篡改整个文件内容,解码JSON并迭代PHP数组。

理想情况下,我想阅读该文件,获取每个对象的足够信息并进行处理。 如果有类似的库可用于JSON,那么SAX类型的方法就可以了。

关于如何最好地处理这个问题的任何建议?

7 个答案:

答案 0 :(得分:15)

我为PHP 7编写了一个流式JSON拉解析器pcrov/JsonReader,其中包含基于XMLReader的api。

它与基于事件的解析器的不同之处在于,不是设置回调并让解析器执行其操作,而是调用解析器上的方法来移动或根据需要检索数据。找到你想要的位,想要停止解析?然后停止解析(并调用close()因为这是件好事。)

(有关拉动与基于事件的解析器的略长概述,请参阅XML reader models: SAX versus XML pull parser。)

示例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取决于它的结构以及您想要用它做什么。这些例子应该为你提供一个起点。

答案 1 :(得分:14)

我决定开发一个基于事件的解析器。它还没有完成,当我推出一个令人满意的版本时,它会用我工作的链接编辑问题。

修改

我终于找到了一个我满意的解析器版本。它可以在GitHub上找到:

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

可能还有一些改进空间,欢迎提供反馈。

答案 2 :(得分:2)

存在类似的内容,但仅适用于C++Java。除非您可以从PHP访问其中一个库,否则在PHP中没有这个实现,但据我所知json_read()}。但是,如果json的结构很简单,那么只需读取文件直到下一个json_read(),然后处理通过{{1}}收到的JSON。但你应该更好地做那个缓冲,比如读10kb,拆分},如果找不到,再读10k,然后处理找到的值。然后阅读下一个块等等..

答案 3 :(得分:2)

这是一个简单的流式解析器,用于处理大型JSON文档。使用它来解析非常大的JSON文档,以避免将整个内容加载到内存中,这就是PHP的每个其他JSON解析器的工作方式。

https://github.com/salsify/jsonstreamingparser

答案 4 :(得分:0)

http://github.com/sfalvo/php-yajl/我自己没有使用它。

答案 5 :(得分:0)

最近,我制作了一个名为JSON Machine的库,该库可以有效地解析无法预测的大JSON文件。通过简单的foreach使用。我自己在项目中使用它。

示例:

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

请参见https://github.com/halaxa/json-machine

答案 6 :(得分:0)

我知道JSON流解析器https://github.com/salsify/jsonstreamingparser已经被提及。但是,正如我最近在(ish)上添加一个新的侦听器,尝试使其更易于使用,我想我可以(进行更改)提供一些有关其功能的信息...

https://www.salsify.com/blog/engineering/json-streaming-parser-for-php上有一篇关于基本解析器的很好的文章,但是我在标准设置中遇到的问题是,您总是必须编写一个侦听器来处理文件。这并非总是一件容易的事,如果/当JSON更改时,也可能需要一定数量的维护。所以我写了RegexListener

基本原理是允许您说出您感兴趣的元素(通过正则表达式),并给其回调以说出找到数据时的操作。在读取JSON的同时,它会跟踪每个组件的路径-与目录结构类似。因此,/name/forename或对于数组/items/item/2/partid而言,这就是正则表达式所针对的。

一个例子是(来自source on 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)中的第二个元素,因此可以访问元素的特定实例。 /namename元素。然后将值作为$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();
}]);

当您对其余内容不感兴趣时​​,这可以节省时间。

要注意的一件事是,这些内容将对内容做出反应,以便在找到匹配内容的结尾时触发每个内容,并且它们的顺序可能不同。而且解析器只会跟踪您感兴趣的内容,而丢弃其他任何内容。

如果您发现任何有趣的功能(有时可怕地称为bug),请在github页面上让我知道或报告问题。