halaxa / json-machine
高效、易于使用且快速的 JSON 提取解析器
Requires
- php: 7.0 - 8.3
Requires (Dev)
- ext-json: *
- friendsofphp/php-cs-fixer: ^3.0
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^8.0
Suggests
- ext-json: To run JSON Machine out of the box without custom decoders.
- guzzlehttp/guzzle: To run example with GuzzleHttp
This package is auto-updated.
Last update: 2024-09-20 05:04:12 UTC
README
非常易于使用且内存效率高的 PHP >=7.0 中大 JSON 文件或流的迭代替代品。见 TL;DR。除了可选的 ext-json
外,生产环境中没有依赖。README 与代码同步
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
-true
或false
以启用或禁用调试模式。当启用调试模式时,在解析或异常期间可以获取诸如行、列和文档中的位置等数据。保持调试禁用会增加轻微的性能优势。
解析来自 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-json
PHP扩展存在,因为它使用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');
开发
克隆此存储库。此库支持两种开发方法
- 非容器化(您的机器上已安装PHP和Composer)
- 容器化(您的机器上安装了Docker)
非容器化
在项目目录中运行composer run -l
以查看可用的开发脚本。这样,您可以运行构建过程的某些步骤,例如测试。
容器化
安装Docker,然后在主机的项目目录中运行make
以查看可用的开发工具/命令。您也可以单独运行构建过程的全部步骤,也可以一次性运行整个构建过程。Make基本上在后台容器中运行composer开发脚本。
make build
:运行完整构建。该命令也通过GitHub Actions CI运行。
支持
您喜欢这个库吗?给它加星标,分享它,展示它 :) 欢迎问题和拉取请求。
许可
Apache 2.0
齿轮元素:图标由TutsPlus提供,来源于www.flaticon.com,受CC 3.0 BY许可。