halaxa/json-machine

高效、易于使用且快速的 JSON 提取解析器

资助包维护!
其他

安装量: 9,194,095

依赖者: 41

建议者: 0

安全性: 0

星标: 1,074

关注者: 17

分支: 64

开放问题: 14

1.1.4 2023-11-28 21:12 UTC

README

非常易于使用且内存效率高的 PHP >=7.0 中大 JSON 文件或流的迭代替代品。见 TL;DR。除了可选的 ext-json 外,生产环境中没有依赖。README 与代码同步

Build Status codecov Latest Stable Version Monthly Downloads

TL;DR

<?php

use \JsonMachine\Items;

// this often causes Allowed Memory Size Exhausted
- $users = json_decode(file_get_contents('500MB-users.json'));

// this usually takes few kB of memory no matter the file size
+ $users = Items::fromFile('500MB-users.json');

foreach ($users as $id => $user) {
    // just process $user as usual
    var_dump($user->name);
}

$users[42] 一样的随机访问目前尚不可行。使用上面提到的 foreach 寻找项或使用 JSON Pointer

通过 iterator_count($users) 计算项数。请记住,它仍然需要内部迭代整个结构以获取计数,因此将花费相同的时间。

如果使用默认设置,则需要 ext-json。见 解码器

遵循 CHANGELOG

简介

JSON Machine 是一个高效、易于使用且快速的基于生成器的 JSON 流/提取/增量/懒惰(无论你称它为什么)解析器,用于处理不可预测的长 JSON 流或文档。主要特性包括

  • 对于不可预测的大 JSON 文档,具有恒定的内存占用。
  • 易于使用。只需使用 foreach 迭代任何大小的 JSON。没有事件和回调。
  • 高效迭代文档的任何子树,由 JSON Pointer 指定
  • 速度。关键性能代码不包含不必要的函数调用、没有正则表达式,并默认使用原生的 json_decode 解码 JSON 文档项。见 解码器
  • 解析不仅限于流,还包括任何产生 JSON 块的可迭代表。
  • 彻底测试。超过 200 个测试和 1000 个断言。

解析 JSON 文档

解析文档

假设 fruits.json 包含这个巨大的 JSON 文档

// fruits.json
{
    "apple": {
        "color": "red"
    },
    "pear": {
        "color": "yellow"
    }
}

可以这样解析

<?php

use \JsonMachine\Items;

$fruits = Items::fromFile('fruits.json');

foreach ($fruits as $name => $data) {
    // 1st iteration: $name === "apple" and $data->color === "red"
    // 2nd iteration: $name === "pear" and $data->color === "yellow"
}

解析 JSON 数组而不是 JSON 对象遵循相同的逻辑。foreach 中的键将是项的数字索引。

如果您希望 JSON Machine 返回数组而不是对象,请使用 new ExtJsonDecoder(true) 作为解码器。

<?php

use JsonMachine\JsonDecoder\ExtJsonDecoder;
use JsonMachine\Items;

$objects = Items::fromFile('path/to.json', ['decoder' => new ExtJsonDecoder(true)]);

解析子树

如果您只想迭代 fruits.json 中的 results 子树

// fruits.json
{
    "results": {
        "apple": {
            "color": "red"
        },
        "pear": {
            "color": "yellow"
        }
    }
}

请使用 JSON Pointer /results 作为 pointer 选项

<?php

use \JsonMachine\Items;

$fruits = Items::fromFile('fruits.json', ['pointer' => '/results']);
foreach ($fruits as $name => $data) {
    // The same as above, which means:
    // 1st iteration: $name === "apple" and $data->color === "red"
    // 2nd iteration: $name === "pear" and $data->color === "yellow"
}

注意

results 的值不会一次性加载到内存中,而是每次只加载 results 中的一个项。在您当前迭代的级别/子树中,内存中始终只存在一个项。因此,内存消耗是恒定的。

解析数组中的嵌套值

JSON指针规范还允许使用连字符(-)来代替特定的数组索引。JSON Machine将其解释为通配符,可以匹配任何数组索引(而不是任何对象键)。这使您能够在不加载整个项目的情况下迭代数组的嵌套值。

示例

// fruitsArray.json
{
    "results": [
        {
            "name": "apple",
            "color": "red"
        },
        {
            "name": "pear",
            "color": "yellow"
        }
    ]
}

要迭代所有水果的颜色,请使用JSON指针"/results/-/color"

<?php

use \JsonMachine\Items;

$fruits = Items::fromFile('fruitsArray.json', ['pointer' => '/results/-/color']);

foreach ($fruits as $key => $value) {
    // 1st iteration:
    $key == 'color';
    $value == 'red';
    $fruits->getMatchedJsonPointer() == '/results/-/color';
    $fruits->getCurrentJsonPointer() == '/results/0/color';

    // 2nd iteration:
    $key == 'color';
    $value == 'yellow';
    $fruits->getMatchedJsonPointer() == '/results/-/color';
    $fruits->getCurrentJsonPointer() == '/results/1/color';
}

解析单个标量值

您可以使用与集合相同的方式解析文档中的单个标量值。考虑以下示例

// fruits.json
{
    "lastModified": "2012-12-12",
    "apple": {
        "color": "red"
    },
    "pear": {
        "color": "yellow"
    },
    // ... gigabytes follow ...
}

获取lastModified键的标量值如下

<?php

use \JsonMachine\Items;

$fruits = Items::fromFile('fruits.json', ['pointer' => '/lastModified']);
foreach ($fruits as $key => $value) {
    // 1st and final iteration:
    // $key === 'lastModified'
    // $value === '2012-12-12'
}

当解析器找到值并将其返回给您时,它停止解析。因此,当单个标量值位于数GB大小的文件或流的开头时,它只需在短时间内获取值,并且几乎不消耗内存。

明显的快捷方式是

<?php

use \JsonMachine\Items;

$fruits = Items::fromFile('fruits.json', ['pointer' => '/lastModified']);
$lastModified = iterator_to_array($fruits)['lastModified'];

单个标量值访问也支持JSON指针中的数组索引。

解析多个子树

还可以使用多个JSON指针解析多个子树。考虑以下示例

// fruits.json
{
    "lastModified": "2012-12-12",
    "berries": [
        {
          "name": "strawberry", // not a berry, but whatever ...
          "color": "red"
        },
        {
          "name": "raspberry", // the same ...
          "color": "red"
        }
    ],
    "citruses": [
      {
          "name": "orange",
          "color": "orange"
      },
      {
          "name": "lime",
          "color": "green"
      }
    ]
}

要迭代所有浆果和柑橘类水果,请使用JSON指针["/berries", "/citrus"]。指针的顺序无关紧要。项目将按在文档中出现的顺序迭代。

<?php

use \JsonMachine\Items;

$fruits = Items::fromFile('fruits.json', [
    'pointer' => ['/berries', '/citruses']
]);

foreach ($fruits as $key => $value) {
    // 1st iteration:
    $value == ["name" => "strawberry", "color" => "red"];
    $fruits->getCurrentJsonPointer() == '/berries';

    // 2nd iteration:
    $value == ["name" => "raspberry", "color" => "red"];
    $fruits->getCurrentJsonPointer() == '/berries';

    // 3rd iteration:
    $value == ["name" => "orange", "color" => "orange"];
    $fruits->getCurrentJsonPointer() == '/citruses';

    // 4th iteration:
    $value == ["name" => "lime", "color" => "green"];
    $fruits->getCurrentJsonPointer() == '/citruses';
}

那么 JSON 指针究竟是什么?

这是在JSON文档中定位一个项目的一种方法。请参阅JSON Pointer RFC 6901。它非常方便,因为有时JSON结构很深,您只想迭代子树,而不是主级别。因此,您只需指定要迭代的JSON数组或对象(甚至标量值)的指针即可。当解析器遇到您指定的集合时,迭代开始。您可以将它作为所有Items::from*函数中的pointer选项传递。如果您指定了文档中不存在的位置的指针,将抛出异常。它也可以用来访问标量值。JSON指针本身必须是有效的JSON字符串。对引用令牌(斜杠之间的部分)的文本比较是针对JSON文档的键/成员名称进行的。

一些示例

选项

选项可能会改变JSON的解析方式。Items::from*函数的所有参数的第二个参数是选项数组。可用的选项包括

  • pointer - 一个JSON指针字符串,告诉您要迭代文档的哪一部分。
  • decoder - ItemDecoder接口的实例。
  • debug - truefalse以启用或禁用调试模式。当启用调试模式时,在解析或异常期间可以获取诸如行、列和文档中的位置等数据。保持调试禁用会增加轻微的性能优势。

解析来自 JSON API 的流式响应

流API响应或任何其他JSON流被解析的方式与文件相同。唯一的区别是,您使用Items::fromStream($streamResource)来处理它,其中$streamResource是包含JSON文档的流资源。其余的与解析文件相同。以下是一些支持流响应的流行http客户端的示例

GuzzleHttp

Guzzle使用自己的流,但可以通过调用\GuzzleHttp\Psr7\StreamWrapper::getResource()将它们转换回PHP流。将此函数的结果传递给Items::fromStream函数,您就设置好了。请参阅工作GuzzleHttp示例

Symfony HttpClient

Symfony HttpClient的流响应作为迭代器工作。由于JSON Machine基于迭代器,因此与Symfony HttpClient的集成非常简单。请参阅HttpClient示例

跟踪进度(启用debug

大文档可能需要一段时间来解析。在您的foreach循环中调用Items::getPosition()以获取从开始处处理过的字节数当前总数。然后,计算百分比很容易,即position / total * 100。要查找您的文档的总字节数,您可以检查:

  • 如果解析字符串,请使用strlen($document)
  • 如果解析文件,请使用filesize($file)
  • 如果解析HTTP流响应,请检查HTTP头中的Content-Length
  • ... 你明白意思了

如果debug被禁用,getPosition()总是返回0

<?php

use JsonMachine\Items;

$fileSize = filesize('fruits.json');
$fruits = Items::fromFile('fruits.json', ['debug' => true]);
foreach ($fruits as $name => $data) {
    echo 'Progress: ' . intval($fruits->getPosition() / $fileSize * 100) . ' %'; 
}

解码器

Items::from*函数也接受decoder选项。它必须是一个JsonMachine\JsonDecoder\ItemDecoder的实例。如果没有指定,则默认使用ExtJsonDecoder。它需要ext-jsonPHP扩展存在,因为它使用json_decode。当json_decode不能满足您的需求时,实现JsonMachine\JsonDecoder\ItemDecoder并创建自己的。

可用的解码器

  • ExtJsonDecoder - 默认。 使用json_decode来解码键和值。构造函数具有与json_decode相同的参数。

  • PassThruDecoder - 不进行解码。键和值都作为纯JSON字符串产生。当您想在foreach中直接解析JSON项,而无需实现JsonMachine\JsonDecoder\ItemDecoder时非常有用。自1.0.0以来不使用json_decode

示例

<?php

use JsonMachine\JsonDecoder\PassThruDecoder;
use JsonMachine\Items;

$items = Items::fromFile('path/to.json', ['decoder' => new PassThruDecoder]);
  • ErrorWrappingDecoder - 一个包装器,它将解码错误包装在DecodingError对象中,从而让您可以跳过格式不正确的项,而不是在SyntaxError异常上失败。示例
<?php

use JsonMachine\Items;
use JsonMachine\JsonDecoder\DecodingError;
use JsonMachine\JsonDecoder\ErrorWrappingDecoder;
use JsonMachine\JsonDecoder\ExtJsonDecoder;

$items = Items::fromFile('path/to.json', ['decoder' => new ErrorWrappingDecoder(new ExtJsonDecoder())]);
foreach ($items as $key => $item) {
    if ($key instanceof DecodingError || $item instanceof DecodingError) {
        // handle error of this malformed json item
        continue;
    }
    var_dump($key, $item);
}

错误处理

从0.4.0开始,每个异常都扩展了JsonMachineException,因此您可以通过捕获它来过滤来自JSON Machine库的任何错误。

跳过格式不正确的项

如果json流中任何地方有错误,会抛出SyntaxError异常。这非常不方便,因为如果其中一个json项中有一个错误,您将无法解析文档的其余部分,因为有一个格式不正确的项。ErrorWrappingDecoder是一个解码器包装器,可以帮助您处理这种情况。用它包装一个解码器,您在foreach中迭代的所有格式不正确的项都会通过DecodingError提供给您。这样,您可以跳过它们,并继续处理文档。请参阅可用的解码器中的示例。尽管如此,在迭代的项之间的json流结构中的语法错误仍然会抛出SyntaxError异常。

解析器效率

时间复杂度始终是O(n)

流/文件

TL;DR:内存复杂度是O(2)

JSON Machine一次读取一个流(或文件)中的1 JSON项,并一次生成相应的1 PHP项。这是最高效的方法,因为如果您有比如说10,000个用户在JSON文件中,并且想使用json_decode(file_get_contents('big.json'))来解析它,那么您将把整个字符串以及所有的10,000个PHP结构放入内存中。以下表格显示了差异

这意味着,JSON Machine对任何大小的处理JSON都是始终有效的。100 GB没问题。

内存中的 JSON 字符串

TL;DR:内存复杂度是O(n+1)

还有一个方法Items::fromString()。如果您被迫解析一个大字符串,而且流不可用,JSON Machine可能比json_decode更好。原因是不像json_decode,JSON Machine仍然一次遍历一个JSON字符串,并且不会一次性将所有结果PHP结构加载到内存中。

让我们继续使用有10,000个用户的示例。这次它们都在内存中的字符串中。当使用json_decode解码这个字符串时,会在内存中创建10,000个数组(对象),然后返回结果。另一方面,JSON Machine会在字符串中为每个找到的项目创建单个结构,并将其返回给你。当你处理该项目并迭代到下一个项目时,会创建另一个单个结构。这与流/文件的行为相同。以下表格将这一概念置于适当的视角。

实际情况甚至更好。与json_decode相比,Items::fromString消耗大约5倍更少的内存。原因是PHP结构比其对应的JSON表示形式占用更多的内存。

故障排除

"我仍在收到 '允许的内存大小 ... 用尽' 的消息"

可能的原因之一是你想要迭代的项位于某些子键中,例如"results",但你忘记了指定JSON指针。请参阅解析子树

"那没有帮助"

另一个可能的原因是,你迭代的某个项本身非常庞大,无法一次性解码。例如,你迭代用户,其中一个是拥有数千个“friend”对象的用户。使用PassThruDecoder,它不会解码项,获取用户json字符串,然后使用Items::fromString()迭代性地自行解析。

<?php

use JsonMachine\Items;
use JsonMachine\JsonDecoder\PassThruDecoder;

$users = Items::fromFile('users.json', ['decoder' => new PassThruDecoder]);
foreach ($users as $user) {
    foreach (Items::fromString($user, ['pointer' => "/friends"]) as $friend) {
        // process friends one by one
    }
}

"我仍然很幸运"

这可能意味着JSON字符串$user本身或其中一个朋友太大,无法适应内存。然而,你可以尝试递归地这样做。使用PassThruDecoder解析"/friends",一次获取一个$friend json字符串,然后使用Items::fromString()解析该字符串...如果这仍然不起作用,那么可能还没有通过JSON Machine解决这个问题。计划了一个功能,将允许你完全递归地迭代任何结构,字符串将作为流提供。

安装

使用Composer

composer require halaxa/json-machine

不使用Composer

克隆或下载此存储库,并将以下内容添加到您的引导文件中

spl_autoload_register(require '/path/to/json-machine/src/autoloader.php');

开发

克隆此存储库。此库支持两种开发方法

  1. 非容器化(您的机器上已安装PHP和Composer)
  2. 容器化(您的机器上安装了Docker)

非容器化

在项目目录中运行composer run -l以查看可用的开发脚本。这样,您可以运行构建过程的某些步骤,例如测试。

容器化

安装Docker,然后在主机的项目目录中运行make以查看可用的开发工具/命令。您也可以单独运行构建过程的全部步骤,也可以一次性运行整个构建过程。Make基本上在后台容器中运行composer开发脚本。

make build:运行完整构建。该命令也通过GitHub Actions CI运行。

支持

您喜欢这个库吗?给它加星标,分享它,展示它 :) 欢迎问题和拉取请求。

ko-fi

许可

Apache 2.0

齿轮元素:图标由TutsPlus提供,来源于www.flaticon.com,受CC 3.0 BY许可。

目录表由markdown-toc生成