cerbero/json-parser

零依赖的拉取解析器,以内存高效的方式读取来自任何源的大JSON。

1.1.0 2023-08-06 15:38 UTC

This package is auto-updated.

Last update: 2024-09-06 18:09:20 UTC


README

Author PHP Version Build Status Coverage Status Quality Score PHPStan Level Latest Version Software License PSR-7 PSR-12 Total Downloads

零依赖的拉取解析器,以内存高效的方式读取来自任何源的大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/-/barfoo/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

💖 贡献指南

请参阅 CONTRIBUTINGCODE_OF_CONDUCT 了解详细信息。

🏢 安全性

如果您发现任何安全问题,请通过电子邮件 andrea.marco.sartori@gmail.com 而不是使用问题跟踪器。

🏆 致谢

⚖️ 许可证

MIT 许可证 (MIT)。有关更多信息,请参阅 许可证文件