在PHP中处理大型JSON文件

28

我正在尝试处理一些相对较大的JSON文件(可能高达200M)。

文件的结构基本上是对象数组。

因此,大致如下:

[
  {"property":"value", "property2":"value2"},
  {"prop":"val"},
  ...
  {"foo":"bar"}
]
每个对象都具有任意属性,并且不一定与数组中的其他对象共享它们(例如,拥有相同的属性)。我想对数组中的每个对象应用处理,但由于文件可能非常大,我无法将整个文件内容读入内存,解码JSON并遍历PHP数组。因此,理想情况下,我希望读取文件,获取每个对象的足够信息并进行处理。如果有类似的库可用于JSON,则SAX类型方法可以接受。如何最好地处理这个问题?

2
为了维护目的,我想保持一种语言。我也不熟悉Python,所以如果我需要更新,那会引起其他问题。谢谢你的提供! - The Mighty Rubber Duck
7个回答

22
我已经为PHP 7编写了一个流式JSON拉取解析器pcrov/JsonReader,其API基于XMLReader
它与事件驱动的解析器有很大不同,因为您不需要设置回调函数并让解析器执行其操作,而是可以在解析器上调用方法以按需移动或检索数据。如果找到所需的位并希望停止解析,则可以停止解析(并调用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取决于其结构和您想要执行的操作。这些示例应该为您提供一个开始的地方。


PHP 5.x有没有类似于@user3942918提到的这个库的库呢? - gumuruh

16

我决定开发一个基于事件的解析器。目前还没有完全完成,当我推出满意的版本后,我将通过链接在问题中进行编辑。

编辑:

最终,我完成了一个令我满意的解析器版本。它已经在GitHub上提供:

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

也许还有改进的空间,欢迎反馈。


这个基于事件的解析器有进展了吗? - David Higgins
我的 JSON 文件包含一个已解码的对象数组。[{"prop1": "valu", "prop2": "val2", "prop3": "val3", "pro4": "val4"}, {"prop1": "valu", "prop2": "val2", "prop3": "val3", "pro4": "val4"}..... ] 解析此数据失败。有什么建议吗? - Gaurav Phapale
@GauravPhapale 看起来解析器目前不支持顶级数组。不过修复起来应该很容易。 - The Mighty Rubber Duck
1
@GauravPhapale 我推送了一个更新,修复了错误的行为并消除了另一个错误(不接受数组中的字符串)。这应该教会我编写详尽测试的重要性。 - The Mighty Rubber Duck

6

最近我创建了一个名为 JSON Machine 的库,它可以高效地解析不可预测的大型 JSON 文件。使用简单的 foreach 可以完成操作。我自己也在项目中使用它。

例子:

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

请查看https://github.com/halaxa/json-machine


@gumuruh 我猜是因为我的回答更加新近。 - Filip Halaxa
我知道我来晚了,而且我可能会打开一个 Github 问题请求,但是如果不通过 Composer 安装,你如何使用你的工具 Json Machine?它确实提到可以克隆存储库,但这并不推荐。还有其他安全的方法吗? - Robin

2
存在这样的东西,但仅适用于C ++Java。除非您可以从PHP访问这些库之一,否则在我所知道的范围内,PHP中没有实现这个功能,只有json_read()。但是,如果JSON结构如此简单,那么很容易只需读取文件直到下一个},然后通过json_read()处理收到的JSON。但最好进行缓冲,例如读取10kb,按}分割,如果找不到,则再读取另外10k,否则处理找到的值。然后读取下一个块,以此类推。

好的,对象可以潜在地具有对象作为属性。我无法控制对象本身的内容。听起来像是词法分析器/解析器的工作,或者我可以通过计算 {} 的数量手动切割它。不过我想避免这样做。 - The Mighty Rubber Duck

2
这是一个简单的流式解析器,用于处理大型JSON文档。它可用于解析非常大的JSON文档,以避免将整个文档加载到内存中,这是PHP中几乎所有其他JSON解析器的工作方式。

https://github.com/salsify/jsonstreamingparser


0

2
最新的提交评论并没有真正帮助赢得我的信任 => “数组因无法观察到的原因而崩溃。” - The Mighty Rubber Duck
2
大概最后一次提交修复了那个问题。所以你刚好及时赶到 :-) - Thilo
2
不是的。但是,我所有的提交信息也都是这样的: 描述被修复的错误。 - Thilo
2
我明白了 :) 通常我的解决bug的过程很清晰。 - The Mighty Rubber Duck

0

我知道已经提到了JSON流解析器https://github.com/salsify/jsonstreamingparser。但是,由于我最近添加了一个新的监听器,试图使它更容易使用,所以我想(换个口味)发布一些关于它的信息...

有一篇非常好的基本解析器介绍文章https://www.salsify.com/blog/engineering/json-streaming-parser-for-php,但我对标准设置的问题是,您总是需要编写一个监听器来处理文件。这并不总是一项简单的任务,如果/当JSON发生更改时,还需要进行一定的维护。因此,我编写了RegexListener

基本原则是允许您通过正则表达式说出您感兴趣的元素,并给它一个回调函数来告诉它在找到数据时要做什么。在读取JSON时,它会跟踪每个组件的路径-类似于目录结构。因此,对于数组,例如/items/item/2/partid/name/forename - 这就是正则表达式所匹配的内容。
一个例子是(来自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):

有效地扫描任何名为嵌套数组的元素,并将每个元素以及其在文档中的位置传递。

我发现另一个有用的功能是,如果在大型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页面上报告一个问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接