cerbero / json-parser
零依赖的拉取解析器,以内存高效的方式读取来自任何源的大JSON。
Requires
- php: ^8.1
Requires (Dev)
- guzzlehttp/guzzle: ^7.2
- illuminate/http: >=6.20
- mockery/mockery: ^1.5
- pestphp/pest: ^2.0
- phpstan/phpstan: ^1.9
- scrutinizer/ocular: ^1.8
- squizlabs/php_codesniffer: ^3.0
Suggests
- guzzlehttp/guzzle: Required to load JSON from endpoints (^7.2).
This package is auto-updated.
Last update: 2024-09-06 18:09:20 UTC
README
零依赖的拉取解析器,以内存高效的方式读取来自任何源的大JSON。
📦 安装
通过Composer
composer require cerbero/json-parser
🔮 使用方法
👣 基础知识
JSON解析器提供了一个最小的API,可以从任何源读取大JSON。
// a source is anything that can provide a JSON, in this case an endpoint $source = 'https://randomuser.me/api/1.4?seed=json-parser&results=5'; foreach (new JsonParser($source) as $key => $value) { // instead of loading the whole JSON, we keep in memory only one key and value at a time }
根据我们的代码风格,我们可以以三种不同的方式实例化解析器。
use Cerbero\JsonParser\JsonParser; use function Cerbero\JsonParser\parseJson; // classic object instantiation new JsonParser($source); // static instantiation JsonParser::parse($source); // namespaced function parseJson($source);
如果我们不想使用foreach()
来遍历每个键和值,我们可以链式调用traverse()
方法。
JsonParser::parse($source)->traverse(function (mixed $value, string|int $key, JsonParser $parser) { // lazily load one key and value at a time, we can also access the parser if needed }); // no foreach needed
⚠️ 请注意回调函数的参数顺序:值在键之前传递。
💧 源
JSON源是任何提供JSON的数据点。默认情况下,支持广泛的源。
- 字符串,例如
{"foo":"bar"}
- 可迭代对象,即数组或
Traversable
的实例 - 文件路径,例如
/path/to/large.json
- 资源,例如流
- API端点URL,例如
https://endpoint.json
或任何Psr\Http\Message\UriInterface
的实例 - PSR-7请求,即任何
Psr\Http\Message\RequestInterface
的实例 - PSR-7消息,即任何
Psr\Http\Message\MessageInterface
的实例 - PSR-7流,即任何
Psr\Http\Message\StreamInterface
的实例 - Laravel HTTP客户端请求,即任何
Illuminate\Http\Client\Request
的实例 - Laravel HTTP客户端响应,即任何
Illuminate\Http\Client\Response
的实例 - 用户定义的源,即任何
Cerbero\JsonParser\Sources\Source
的实例
如果需要解析的源不支持默认情况,我们可以实现自己的自定义源。
点击此处查看如何实现自定义源。
要实现自定义源,我们需要扩展Source
并实现3个方法。
use Cerbero\JsonParser\Sources\Source; use Traversable; class CustomSource extends Source { public function getIterator(): Traversable { // return a Traversable holding the JSON source, e.g. a Generator yielding chunks of JSON } public function matches(): bool { // return TRUE if this class can handle the JSON source } protected function calculateSize(): ?int { // return the size of the JSON in bytes or NULL if it can't be calculated } }
父类Source
提供了对我们以下两个属性的访问权限:
$source
:传递给解析器的JSON源,即:new JsonParser($source)
$config
:通过链式方法设置的配置,如$parser->pointer('/foo')
getIterator()
方法定义了以内存高效的方式读取JSON源的逻辑。它向解析器提供小块的JSON。请参考现有源以查看一些实现。
matches()
方法确定传递给解析器的JSON源是否可以由我们的自定义实现处理。换句话说,我们正在告诉解析器是否应使用我们的类来解析JSON。
最后,calculateSize()
计算整个JSON源的大小。它用于跟踪解析进度,然而,有时无法知道JSON源的大小。在这种情况下,或者如果我们不需要跟踪进度,我们可以返回null
。
现在我们已经实现了自定义源,我们可以将其传递给解析器。
$json = JsonParser::parse(new CustomSource($source)); foreach ($json as $key => $value) { // process one key and value of $source at a time }
如果您在不同项目中实现相同的自定义源,请随时提交PR,我们将考虑默认支持您的自定义源。感谢您做出的任何贡献!
🎯 指针
JSON指针是一种用于指向JSON中节点的标准。这个包利用JSON指针从大型JSON中提取一些子树。
例如,考虑这个JSON。为了只提取第一个性别并避免解析整个JSON,我们可以设置/results/0/gender
指针
$json = JsonParser::parse($source)->pointer('/results/0/gender'); foreach ($json as $key => $value) { // 1st and only iteration: $key === 'gender', $value === 'female' }
JSON解析器利用-
通配符指向任何数组索引,因此我们可以使用/results/-/gender
指针提取所有性别
$json = JsonParser::parse($source)->pointer('/results/-/gender'); foreach ($json as $key => $value) { // 1st iteration: $key === 'gender', $value === 'female' // 2nd iteration: $key === 'gender', $value === 'female' // 3rd iteration: $key === 'gender', $value === 'male' // and so on for all the objects in the array... }
如果我们想提取更多的子树,我们可以设置多个指针。让我们提取所有性别和国家
$json = JsonParser::parse($source)->pointers(['/results/-/gender', '/results/-/location/country']); foreach ($json as $key => $value) { // 1st iteration: $key === 'gender', $value === 'female' // 2nd iteration: $key === 'country', $value === 'Germany' // 3rd iteration: $key === 'gender', $value === 'female' // 4th iteration: $key === 'country', $value === 'Mexico' // and so on for all the objects in the array... }
⚠️不允许交集指针,如
/foo
和/foo/bar
,但允许交集通配符,如foo/-/bar
和foo/0/bar
我们还可以指定一个回调,当找到JSON指针时执行。当我们有不同的指针并需要对每个指针运行自定义逻辑时,这很有用
$json = JsonParser::parse($source)->pointers([ '/results/-/gender' => fn (string $gender, string $key) => new Gender($gender), '/results/-/location/country' => fn (string $country, string $key) => new Country($country), ]); foreach ($json as $key => $value) { // 1st iteration: $key === 'gender', $value instanceof Gender // 2nd iteration: $key === 'country', $value instanceof Country // and so on for all the objects in the array... }
⚠️请注意回调的参数顺序:值在键之前传递。
这也可以通过多次链式调用pointer()
方法来实现
$json = JsonParser::parse($source) ->pointer('/results/-/gender', fn (string $gender, string $key) => new Gender($gender)) ->pointer('/results/-/location/country', fn (string $country, string $key) => new Country($country)); foreach ($json as $key => $value) { // 1st iteration: $key === 'gender', $value instanceof Gender // 2nd iteration: $key === 'country', $value instanceof Country // and so on for all the objects in the array... }
指针回调也可以用于自定义键。我们可以通过更新键reference
来实现这一点
$json = JsonParser::parse($source)->pointer('/results/-/name/first', function (string $name, string &$key) { $key = 'first_name'; }); foreach ($json as $key => $value) { // 1st iteration: $key === 'first_name', $value === 'Sara' // 2nd iteration: $key === 'first_name', $value === 'Andrea' // and so on for all the objects in the array... }
如果回调足以处理指针,并且我们不需要为所有指针运行任何共同逻辑,我们可以通过链式调用方法traverse()
来避免手动调用foreach()
JsonParser::parse($source) ->pointer('/-/gender', $this->handleGender(...)) ->pointer('/-/location/country', $this->handleCountry(...)) ->traverse(); // no foreach needed
否则,如果需要一些共同逻辑但更倾向于方法链而不是手动循环,我们可以将回调传递给traverse()
方法
JsonParser::parse($source) ->pointer('/results/-/gender', fn (string $gender, string $key) => new Gender($gender)) ->pointer('/results/-/location/country', fn (string $country, string $key) => new Country($country)) ->traverse(function (Gender|Country $value, string $key, JsonParser $parser) { // 1st iteration: $key === 'gender', $value instanceof Gender // 2nd iteration: $key === 'country', $value instanceof Country // and so on for all the objects in the array... }); // no foreach needed
⚠️请注意回调的参数顺序:值在键之前传递。
有时指针提取的子树足够小,可以完全保留在内存中。在这种情况下,我们可以链式调用toArray()
来预先加载提取的子树到数组中
// ['gender' => 'female', 'country' => 'Germany'] $array = JsonParser::parse($source)->pointers(['/results/0/gender', '/results/0/location/country'])->toArray();
🐼 懒指针
JSON解析器一次只保留一个键和一个值在内存中。然而,如果值是一个大的数组或对象,可能效率低下,甚至不可能将其全部保留在内存中。
为了解决这个问题,我们可以使用延迟指针。这些指针递归地只保留任何嵌套数组或对象中的每个键和值。
$json = JsonParser::parse($source)->lazyPointer('/results/0/name'); foreach ($json as $key => $value) { // 1st iteration: $key === 'name', $value instanceof Parser }
延迟指针返回一个轻量级的Cerbero\JsonParser\Tokens\Parser
实例,而不是实际的大值。然后我们可以遍历解析器来延迟加载嵌套的键和值
$json = JsonParser::parse($source)->lazyPointer('/results/0/name'); foreach ($json as $key => $value) { // 1st iteration: $key === 'name', $value instanceof Parser foreach ($value as $nestedKey => $nestedValue) { // 1st iteration: $nestedKey === 'title', $nestedValue === 'Mrs' // 2nd iteration: $nestedKey === 'first', $nestedValue === 'Sara' // 3rd iteration: $nestedKey === 'last', $nestedValue === 'Meder' } }
如上所述,延迟指针是递归的。这意味着永远不会保留任何嵌套对象或数组在内存中
$json = JsonParser::parse($source)->lazyPointer('/results/0/location'); foreach ($json as $key => $value) { // 1st iteration: $key === 'location', $value instanceof Parser foreach ($value as $nestedKey => $nestedValue) { // 1st iteration: $nestedKey === 'street', $nestedValue instanceof Parser // 2nd iteration: $nestedKey === 'city', $nestedValue === 'Sontra' // ... // 6th iteration: $nestedKey === 'coordinates', $nestedValue instanceof Parser // 7th iteration: $nestedKey === 'timezone', $nestedValue instanceof Parser } }
要延迟解析整个JSON,我们可以简单地链式调用lazy()
方法
foreach (JsonParser::parse($source)->lazy() as $key => $value) { // 1st iteration: $key === 'results', $value instanceof Parser // 2nd iteration: $key === 'info', $value instanceof Parser }
我们可以通过链式调用wrap()
来递归地包装任何Cerbero\JsonParser\Tokens\Parser
实例。这让我们可以将延迟加载的JSON数组和对象包装到具有高级功能(如映射或过滤)的类中
$json = JsonParser::parse($source) ->wrap(fn (Parser $parser) => new MyWrapper(fn () => yield from $parser)) ->lazy(); foreach ($json as $key => $value) { // 1st iteration: $key === 'results', $value instanceof MyWrapper foreach ($value as $nestedKey => $nestedValue) { // 1st iteration: $nestedKey === 0, $nestedValue instanceof MyWrapper // 2nd iteration: $nestedKey === 1, $nestedValue instanceof MyWrapper // ... } }
ℹ️如果您的包装类实现了
toArray()
方法,那么在将子树预先加载到数组中时,将调用该方法。
延迟指针也有正常指针的所有其他功能:它们接受回调,可以逐个或全部设置,可以预先加载到数组中,也可以与正常指针混合使用
// set custom callback to run only when names are found $json = JsonParser::parse($source)->lazyPointer('/results/-/name', fn (Parser $name) => $this->handleName($name)); // set multiple lazy pointers one by one $json = JsonParser::parse($source) ->lazyPointer('/results/-/name', fn (Parser $name) => $this->handleName($name)) ->lazyPointer('/results/-/location', fn (Parser $location) => $this->handleLocation($location)); // set multiple lazy pointers all together $json = JsonParser::parse($source)->lazyPointers([ '/results/-/name' => fn (Parser $name) => $this->handleName($name)), '/results/-/location' => fn (Parser $location) => $this->handleLocation($location)), ]); // eager load lazy pointers into an array // ['name' => ['title' => 'Mrs', 'first' => 'Sara', 'last' => 'Meder'], 'street' => ['number' => 46, 'name' => 'Römerstraße']] $array = JsonParser::parse($source)->lazyPointers(['/results/0/name', '/results/0/location/street'])->toArray(); // mix pointers and lazy pointers $json = JsonParser::parse($source) ->pointer('/results/-/gender', fn (string $gender) => $this->handleGender($gender)) ->lazyPointer('/results/-/name', fn (Parser $name) => $this->handleName($name));
⚙️ 解码器
默认情况下,JSON解析器使用内置的PHP函数json_decode()
一次解码一个键和值。
通常它将值解码为关联数组,但如果我们更喜欢将值解码为对象,我们可以设置一个自定义解码器
use Cerbero\JsonParser\Decoders\JsonDecoder; JsonParser::parse($source)->decoder(new JsonDecoder(decodesToArray: false));
simdjson 扩展提供了比 json_decode()
更快的解码器,可以通过 pecl install simdjson
安装,如果您的服务器满足 要求。如果加载了扩展,JSON 解析器默认使用 simdjson 解码器。
如果我们需要默认不支持解码器,我们可以实现自己的解码器。
点击此处了解如何实现自定义解码器。
要创建自定义解码器,我们需要实现 Decoder
接口并实现 1 个方法
use Cerbero\JsonParser\Decoders\Decoder; use Cerbero\JsonParser\Decoders\DecodedValue; class CustomDecoder implements Decoder { public function decode(string $json): DecodedValue { // return an instance of DecodedValue both in case of success or failure } }
decode()
方法定义了解码给定 JSON 值的逻辑,无论成功或失败都需要返回 DecodedValue
实例。
为了使自定义解码器实现更加简单,JSON 解析器提供了一个 抽象解码器,它会为我们填充 DecodedValue
,所以我们只需要定义如何解码 JSON 值。
use Cerbero\JsonParser\Decoders\AbstractDecoder; class CustomDecoder extends AbstractDecoder { protected function decodeJson(string $json): mixed { // decode the given JSON or throw an exception on failure return json_decode($json, flags: JSON_THROW_ON_ERROR); } }
⚠️ 请确保在
decodeJson()
中解码失败时抛出异常。
现在我们已经实现了自定义解码器,可以按如下方式设置
JsonParser::parse($source)->decoder(new CustomDecoder());
要查看一些实现示例,请参阅 已存在的解码器。
如果您在不同项目中实现了相同的自定义解码器,请随意发送 PR,我们将考虑默认支持您的自定义解码器。感谢您的贡献!
💢 错误处理
并非所有的 JSON 都有效,一些可能由于结构不正确(例如 [}
)或解码错误(例如 [1a]
)而出现语法错误。JSON 解析器允许我们介入并定义在这些问题发生时运行的逻辑
use Cerbero\JsonParser\Decoders\DecodedValue; use Cerbero\JsonParser\Exceptions\SyntaxException; $json = JsonParser::parse($source) ->onSyntaxError(fn (SyntaxException $e) => $this->handleSyntaxError($e)) ->onDecodingError(fn (DecodedValue $decoded) => $this->handleDecodingError($decoded));
我们甚至可以用占位符替换无效值,以避免因为它们而使整个 JSON 解析失败
// instead of failing, replace invalid values with NULL $json = JsonParser::parse($source)->patchDecodingError(); // instead of failing, replace invalid values with '<invalid>' $json = JsonParser::parse($source)->patchDecodingError('<invalid>');
对于更高级的解码错误修复,我们可以传递一个闭包,该闭包可以访问 DecodedValue
实例
use Cerbero\JsonParser\Decoders\DecodedValue; $patches = ['1a' => 1, '2b' => 2]; $json = JsonParser::parse($source) ->patchDecodingError(fn (DecodedValue $decoded) => $patches[$decoded->json] ?? null);
此包抛出的任何异常都实现了 JsonParserException
接口。这使得在单个捕获块中处理所有异常变得简单
use Cerbero\JsonParser\Exceptions\JsonParserException; try { JsonParser::parse($source)->traverse(); } catch (JsonParserException) { // handle any exception thrown by JSON Parser }
以下是此包抛出的所有异常的完整表格
⏳ 进度
在处理大型 JSON 时,跟踪解析进度可能很有帮助。JSON 解析器提供了方便的方法来访问所有进度详情
$json = new JsonParser($source); $json->progress(); // <Cerbero\JsonParser\ValueObjects\Progress> $json->progress()->current(); // the already parsed bytes e.g. 86759341 $json->progress()->total(); // the total bytes to parse e.g. 182332642 $json->progress()->fraction(); // the completed fraction e.g. 0.47583 $json->progress()->percentage(); // the completed percentage e.g. 47.583 $json->progress()->format(); // the formatted progress e.g. 47.5%
JSON 的总大小根据 来源 的不同而不同。在某些情况下,可能无法确定 JSON 的大小,只知道当前进度
$json->progress()->current(); // 86759341 $json->progress()->total(); // null $json->progress()->fraction(); // null $json->progress()->percentage(); // null $json->progress()->format(); // null
🛠 设置
JSON 解析器还提供了其他设置来微调解析过程。例如,我们可以设置解析 JSON 字符串或流时读取的字节数
$json = JsonParser::parse($source)->bytes(1024 * 16); // read JSON chunks of 16KB
📅 变更日志
有关最近更改的更多信息,请参阅 CHANGELOG。
🧪 贡献
composer test
💖 贡献指南
请参阅 CONTRIBUTING 和 CODE_OF_CONDUCT 了解详细信息。
🏢 安全性
如果您发现任何安全问题,请通过电子邮件 andrea.marco.sartori@gmail.com 而不是使用问题跟踪器。
🏆 致谢
⚖️ 许可证
MIT 许可证 (MIT)。有关更多信息,请参阅 许可证文件。