proklung/json-parser

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

1.0.7 2023-06-22 08:10 UTC

This package is auto-updated.

Last update: 2024-09-22 10:39:59 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 proklung/json-parser

🔮 与原始版本的区别

最低PHP版本 - 7.4

针对特定需求的修改,PSR整理。

移除了所有与Laravel相关的代码

🔮 使用方法

👣 基础知识

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
}

根据我们的代码风格,我们可以以3种不同的方式实例化解析器

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为我们提供了访问2个属性的方法

  • $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
}

懒指针也具有所有其他正常指针的功能:它们接受回调,可以一个接一个地设置或全部设置,可以预加载到数组中,也可以与正常指针混合使用。

// 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

📅 变更日志

有关最近更改的更多信息,请参阅变更日志

🧪 贡献

composer test

💖 贡献指南

有关详细信息,请参阅贡献指南行为准则

🏯 安全性

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

🏅 致谢

⚖️ 许可证

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