PHP - 使用yield生成器对象进行json_encode

14

我在 PHP(5.6) 中有一个非常大的动态生成的数组,希望将其转换为 JSON。问题是,这个数组太大了,无法全部加载到内存中 - 当我尝试处理它时会出现致命错误 (内存不足)。因此,我想到使用生成器可以解决内存问题。

这是我目前尝试过的代码 (这个简化的示例显然没有引起内存错误):

<?php 
function arrayGenerator()// new way using generators
{
    for ($i = 0; $i < 100; $i++) {
        yield $i;
    }
}

function getArray()// old way, generating and returning the full array
{
    $array = [];
    for ($i = 0; $i < 100; $i++) {
        $array[] = $i;
    }
    return $array;
}

$object = [
    'id' => 'foo',
    'type' => 'blah',
    'data' => getArray(),
    'gen'  => arrayGenerator(),
];

echo json_encode($object);

但是 PHP 似乎无法将生成器中的值进行 JSON 编码。这是我从之前脚本中得到的输出:

{
    "id": "foo",
    "type": "blah",
    "data": [// old way - OK
        0,
        1,
        2,
        3,
        //...
    ],
    "gen": {}// using generator - empty object!
}

在调用json_encode之前,是否有可能对由生成器产生的数组进行JSON编码,而不必生成完整序列?


4
唯一编码整个序列的方法是生成整个序列。这需要在后台进行。如果您想使生成器成为可用的数组,则可以使用iterator_to_array(arrayGenerator()) - apokryfos
使用那个函数后,我再次遇到了同样的问题——内存耗尽。目前我唯一能做的就是拆分数组或增加内存限制(并不是我想要的解决方案...)。 - Iván Pérez
2
很抱歉,除非您创建自己的流式JSON编码器,否则无法以其他方式解决您的问题。但是,这可能会比使其正常工作所需的时间更少,因此效益不大。 - apokryfos
1
唯一真正生成不适合内存的JSON数据的方法是流式传输。为此,您需要a)使用流式JSON生成器(PHP没有内置),并b)立即将结果流式传输到某个地方,例如标准输出,文件或从中下载的Web服务器。将结果连接成内存中的字符串并存储在变量中将具有相同的内存问题。 - deceze
@deceze, apokryfos:感谢您的建议。我找到了一些可以创建JSON流(https://github.com/rayward/json-stream)的库,看起来非常有前途。我会尝试一下。 - Iván Pérez
1
实际上,这可能是你想要的:JSON集合的流解析器 - Ryan Vincent
2个回答

8

不幸的是,json_encode无法从生成器函数生成结果。使用iterator_to_array仍然会尝试创建整个数组,这仍然会导致内存问题。

你需要创建自己的函数,从生成器函数中生成json字符串。以下是一个示例:

function json_encode_generator(callable $generator) {
    $result = '[';

    foreach ($generator as $value) {
        $result .= json_encode($value) . ',';
    }

    return trim($result, ',') . ']';
}

它不是一次性编码整个数组,而是逐个对象进行编码,并将结果连接成一个字符串。

上面的示例只处理编码数组,但可以轻松扩展为递归编码整个对象。

如果创建的字符串仍然太大无法放入内存中,则您唯一剩下的选择是直接使用输出流。以下是它的示例:

function json_encode_generator(callable $generator, $outputStream) {
    fwrite($outputStream, '[');

    foreach ($generator as $key => $value) {
        if ($key != 0) {
            fwrite($outputStream, ','); 
        }

        fwrite($outputStream, json_encode($value));
    }

    fwrite($outputStream, ']');
}

您可以看到,唯一的区别是现在我们使用fwrite来向传递的数据流写入内容,而不是将字符串连接起来。此外,我们还需要以不同的方式处理末尾的逗号。


2
当然,这仍然会在内存中生成大量的JSON,甚至可能比原始数据还要大... - deceze
2
好的,在PHP中字符串比数组更节省内存,所以上述解决方案可能已经足够了。否则,您将不得不直接使用输出流,而不是将其临时存储在字符串中。无论是字符串还是流,逻辑都保持不变。 - Kuba Birecki
顺便提一下,通过 $result .= ... 组合生成的字符串将需要大量内存 _(而不是将其附加到数组列表中,然后再进行 implode 操作)_,因为对于每次迭代,它都会创建一个全新的(并且越来越长的)字符串。 - Smuuf

2

什么是生成器函数?

生成器函数是一种更紧凑、更高效的编写迭代器的方式。它允许您定义一个函数,该函数在您循环遍历计算并返回值:

根据http://php.net/manual/en/language.generators.overview.php中的文档:

生成器提供了一种实现简单迭代器的简单方法,无需实现实现Iterator接口的类的开销或复杂性。

生成器允许您编写使用foreach来迭代一组数据的代码,无需在内存中构建数组,这可能会导致超过内存限制,或需要大量处理时间来生成。相反,您可以编写一个生成器函数,它与普通函数相同,只是不是返回一次,而是需要多次产生收到迭代的值。

yield是什么?

yield关键字从生成器函数返回数据:

生成器函数的核心是yield关键字。在其最简单的形式中,yield语句看起来非常像return语句,但是除了停止执行函数并返回外,yield提供一个值给循环遍历生成器的代码,并暂停生成器函数的执行。

因此,在您的情况下,为了生成预期的输出,您需要使用foreach循环或iterator迭代arrayGenerator()函数的输出,然后再将其处理为json(如@apokryfos所建议的那样)。


1
在查找数组中的内存问题时,我发现了这些网站:http://php.net/manual/en/class.splfixedarray.php,http://www.php.net/manual/en/intro.judy.php和https://dev59.com/72Ei5IYBdhLWcg3wYLaT。希望这能对你有所帮助。 - Chetan Ameta

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