Substituição imediata muito fácil de usar e com uso eficiente de memória para iteração ineficiente de grandes arquivos JSON ou fluxos para PHP> = 7.2. Veja TL; DR. Nenhuma dependência na produção, exceto ext-json
opcional. README em sincronia com o código
NOVO na versão 1.2.0
- Iteração recursiva
<?php
use JsonMachineItems;
// this often causes Allowed Memory Size Exhausted,
// because it loads all the items in the JSON into memory
- $users = json_decode(file_get_contents('500MB-users.json'));
// this has very small memory footprint no matter the file size
// because it loads items into memory one by one
+ $users = Items::fromFile('500MB-users.json');
foreach ($users as $id => $user) {
// just process $user as usual
var_dump($user->name);
}
Acesso aleatório como $users[42]
ainda não é possível. Use foreach
mencionado acima e encontre o item ou use o JSON Pointer.
Conte os itens via iterator_count($users)
. Lembre-se de que ainda será necessário iterar tudo internamente para obter a contagem e, portanto, levará quase o mesmo tempo que iterar e contar manualmente.
Requer ext-json
se usado imediatamente, mas não requer se um decodificador personalizado for usado. Consulte Decodificadores.
Siga o CHANGELOG.
JSON Machine é um analisador de fluxo/pull/incremental/preguiçoso (seja qual for o nome) JSON eficiente, fácil de usar e rápido, baseado em geradores desenvolvidos para fluxos ou documentos JSON imprevisivelmente longos. As principais características são:
foreach
. Sem eventos e retornos de chamada.json_decode
nativo para decodificar itens de documentos JSON por padrão. Consulte Decodificadores. Digamos fruits.json
contenha este enorme documento JSON:
// fruits.json
{
"apple" : {
"color" : " red "
},
"pear" : {
"color" : " yellow "
}
}
Pode ser analisado desta forma:
<?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"
}
A análise de um array json em vez de um objeto json segue a mesma lógica. A chave em um foreach será um índice numérico de um item.
Se você preferir que o JSON Machine retorne matrizes em vez de objetos, use new ExtJsonDecoder(true)
como decodificador.
<?php
use JsonMachine JsonDecoder ExtJsonDecoder ;
use JsonMachine Items ;
$ objects = Items:: fromFile ( ' path/to.json ' , [ ' decoder ' => new ExtJsonDecoder ( true )]);
Se você deseja iterar apenas a subárvore results
fruits.json
:
// fruits.json
{
"results" : {
"apple" : {
"color" : " red "
},
"pear" : {
"color" : " yellow "
}
}
}
use JSON Pointer /results
como opção 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"
}
Observação:
O valor dos
results
não é carregado na memória de uma só vez, mas apenas um item nosresults
por vez. É sempre um item na memória por vez no nível/subárvore que você está iterando no momento. Assim, o consumo de memória é constante.
A especificação JSON Pointer também permite usar um hífen ( -
) em vez de um índice de array específico. A máquina JSON o interpreta como um curinga que corresponde a qualquer índice de array (não a qualquer chave de objeto). Isso permite iterar valores aninhados em matrizes sem carregar o item inteiro.
Exemplo:
// fruitsArray.json
{
"results" : [
{
"name" : " apple " ,
"color" : " red "
},
{
"name" : " pear " ,
"color" : " yellow "
}
]
}
Para iterar sobre todas as cores das frutas, use o ponteiro 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 ' ;
}
Você pode analisar um único valor escalar em qualquer lugar do documento da mesma forma que uma coleção. Considere este exemplo:
// fruits.json
{
"lastModified" : " 2012-12-12 " ,
"apple" : {
"color" : " red "
},
"pear" : {
"color" : " yellow "
},
// ... gigabytes follow ...
}
Obtenha o valor escalar da chave lastModified
assim:
<?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'
}
Quando o analisador encontra o valor e o entrega a você, ele interrompe a análise. Portanto, quando um único valor escalar está no início de um arquivo ou fluxo de tamanho de gigabytes, ele obtém o valor desde o início rapidamente e quase sem consumo de memória.
O atalho óbvio é:
<?php
use JsonMachine Items ;
$ fruits = Items:: fromFile ( ' fruits.json ' , [ ' pointer ' => ' /lastModified ' ]);
$ lastModified = iterator_to_array ( $ fruits )[ ' lastModified ' ];
O acesso de valor escalar único também oferece suporte a índices de array no JSON Pointer.
Também é possível analisar várias subárvores usando vários ponteiros JSON. Considere este exemplo:
// 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 "
}
]
}
Para iterar sobre todas as frutas vermelhas e cítricas, use os ponteiros JSON ["/berries", "/citrus"]
. A ordem dos ponteiros não importa. Os itens serão iterados na ordem em que aparecem no documento.
<?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 ' ;
}
Use RecursiveItems
em vez de Items
quando a estrutura JSON for difícil ou mesmo impossível de manipular com Items
e ponteiros JSON ou os itens individuais que você itera são grandes demais para serem manipulados. Por outro lado, é notavelmente mais lento que Items
, então tenha isso em mente.
Quando RecursiveItems
encontra uma lista ou ditado no JSON, ele retorna uma nova instância de si mesmo que pode ser iterada e o ciclo se repete. Assim, ele nunca retorna um array ou objeto PHP, mas apenas valores escalares ou RecursiveItems
. Nenhum ditado ou lista JSON será totalmente carregado na memória de uma só vez.
Vejamos um exemplo com muitos, muitos usuários com muitos, muitos amigos:
// users.json
[
{
"username" : " user " ,
"e-mail" : " [email protected] " ,
"friends" : [
{
"username" : " friend1 " ,
"e-mail" : " [email protected] "
},
{
"username" : " friend2 " ,
"e-mail" : " [email protected] "
}
]
}
]
<?php
use JsonMachine RecursiveItems
$ users = RecursiveItems:: fromFile ( ' users.json ' );
foreach ( $ users as $ user ) {
/** @var $user RecursiveItems */
foreach ( $ user as $ field => $ value ) {
if ( $ field === ' friends ' ) {
/** @var $value RecursiveItems */
foreach ( $ value as $ friend ) {
/** @var $friend RecursiveItems */
foreach ( $ friend as $ friendField => $ friendValue ) {
$ friendField == ' username ' ;
$ friendValue == ' friend1 ' ;
}
}
}
}
}
Se você interromper uma iteração de nível mais profundo e preguiçoso (ou seja, pular alguns
"friends"
viabreak
) e avançar para um próximo valor (ou seja, nextuser
), não será capaz de iterá-la mais tarde. A máquina JSON deve iterá-lo em segundo plano para poder ler o próximo valor. Tal tentativa resultará em uma exceção de gerador fechado.
RecursiveItems
toArray(): array
Se você tiver certeza de que uma determinada instância de RecursiveItems está apontando para uma estrutura de dados gerenciável pela memória (por exemplo, $friend), você pode chamar $friend->toArray()
, e o item se materializará em um matriz PHP simples.
advanceToKey(int|string $key): scalar|RecursiveItems
Ao procurar por uma chave específica em uma coleção (por exemplo, 'friends'
em $user
), você não precisa usar um loop e uma condição para procurá-la. Em vez disso, você pode simplesmente ligar $user->advanceToKey("friends")
. Ele irá iterar para você e retornar o valor nesta chave. As chamadas podem ser encadeadas. Ele também suporta sintaxe semelhante a array para avançar e obter os índices seguintes. Então $user['friends']
seria um alias para $user->advanceToKey('friends')
. As chamadas podem ser encadeadas. Tenha em mente que é apenas um alias - você não poderá acessar índices anteriores aleatoriamente depois de usá-lo diretamente em RecursiveItems
. É apenas um açúcar de sintaxe. Use toArray()
se precisar de acesso aleatório aos índices de um registro/item.
O exemplo anterior poderia, portanto, ser simplificado da seguinte forma:
<?php
use JsonMachine RecursiveItems
$ users = RecursiveItems:: fromFile ( ' users.json ' );
foreach ( $ users as $ user ) {
/** @var $user RecursiveItems */
foreach ( $ user [ ' friends ' ] as $ friend ) { // or $user->advanceToKey('friends')
/** @var $friend RecursiveItems */
$ friendArray = $ friend -> toArray ();
$ friendArray [ ' username ' ] === ' friend1 ' ;
}
}
O encadeamento permite que você faça algo assim:
<?php
use JsonMachine RecursiveItems
$ users = RecursiveItems:: fromFile ( ' users.json ' );
$ users [ 0 ][ ' friends ' ][ 1 ][ ' username ' ] === ' friend2 ' ;
RecursiveItems implements RecursiveIterator
Então você pode usar, por exemplo, ferramentas internas do PHP para trabalhar em RecursiveIterator
como estas:
É uma forma de endereçar um item no documento JSON. Consulte o JSON Pointer RFC 6901. É muito útil, porque às vezes a estrutura JSON é mais profunda e você deseja iterar uma subárvore, não o nível principal. Então você apenas especifica o ponteiro para o array ou objeto JSON (ou mesmo para um valor escalar) que deseja iterar e pronto. Quando o analisador atinge a coleção especificada, a iteração começa. Você pode passá-lo como opção pointer
em todas as funções Items::from*
. Se você especificar um ponteiro para uma posição inexistente no documento, uma exceção será lançada. Ele também pode ser usado para acessar valores escalares. O próprio ponteiro JSON deve ser uma string JSON válida . A comparação literal de tokens de referência (as partes entre barras) é executada em relação às chaves/nomes de membros do documento JSON.
Alguns exemplos:
Valor do ponteiro JSON | Irá iterar |
---|---|
(string vazia - padrão) | ["this", "array"] ou {"a": "this", "b": "object"} serão iterados (nível principal) |
/result/items | {"result": {"items": ["this", "array", "will", "be", "iterated"]}} |
/0/items | [{"items": ["this", "array", "will", "be", "iterated"]}] (suporta índices de array) |
/results/-/status | {"results": [{"status": "iterated"}, {"status": "also iterated"}]} (um hífen como um curinga de índice de matriz) |
/ (peguei! - uma barra seguida por uma string vazia, veja as especificações) | {"":["this","array","will","be","iterated"]} |
/quotes" | {"quotes"": ["this", "array", "will", "be", "iterated"]} |
As opções podem alterar a forma como um JSON é analisado. Matriz de opções é o segundo parâmetro de todas as funções Items::from*
. As opções disponíveis são:
pointer
- Uma string JSON Pointer que informa qual parte do documento você deseja iterar.decoder
- Uma instância da interface ItemDecoder
.debug
- true
ou false
para ativar ou desativar o modo de depuração. Quando o modo de depuração está habilitado, dados como linha, coluna e posição no documento ficam disponíveis durante a análise ou em exceções. Manter a depuração desabilitada adiciona uma pequena vantagem de desempenho. Uma resposta da API de fluxo ou qualquer outro fluxo JSON é analisado exatamente da mesma maneira que o arquivo. A única diferença é que você usa Items::fromStream($streamResource)
para isso, onde $streamResource
é o recurso de stream com o documento JSON. O resto é igual à análise de arquivos. Aqui estão alguns exemplos de clientes http populares que suportam respostas de streaming:
Guzzle usa seus próprios streams, mas eles podem ser convertidos novamente em streams PHP chamando GuzzleHttpPsr7StreamWrapper::getResource()
. Passe o resultado desta função para a função Items::fromStream
e pronto. Veja o exemplo funcional do GuzzleHttp.
Uma resposta de fluxo do Symfony HttpClient funciona como iterador. E como o JSON Machine é baseado em iteradores, a integração com o Symfony HttpClient é muito simples. Veja o exemplo HttpClient.
debug
habilitada) Documentos grandes podem demorar um pouco para serem analisados. Chame Items::getPosition()
em seu foreach
para obter a contagem atual dos bytes processados desde o início. A porcentagem é então fácil de calcular como position / total * 100
. Para descobrir o tamanho total do seu documento em bytes, você pode verificar:
strlen($document)
se você analisar uma stringfilesize($file)
se você analisar um arquivoContent-Length
se você analisar uma resposta de fluxo http Se debug
estiver desabilitada, getPosition()
sempre retornará 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 ) . ' % ' ;
}
As funções Items::from*
também aceitam a opção decoder
. Deve ser uma instância de JsonMachineJsonDecoderItemDecoder
. Se nenhum for especificado, ExtJsonDecoder
será usado por padrão. Requer que a extensão PHP ext-json
esteja presente, porque usa json_decode
. Quando json_decode
não faz o que você deseja, implemente JsonMachineJsonDecoderItemDecoder
e faça o seu próprio.
ExtJsonDecoder
– Padrão. Usa json_decode
para decodificar chaves e valores. O construtor tem os mesmos parâmetros que json_decode
.
PassThruDecoder
- Não decodifica. Tanto as chaves quanto os valores são produzidos como strings JSON puras. Útil quando você deseja analisar um item JSON com outra coisa diretamente no foreach e não deseja implementar JsonMachineJsonDecoderItemDecoder
. Desde 1.0.0
não usa json_decode
.
Exemplo:
<?php
use JsonMachine JsonDecoder PassThruDecoder ;
use JsonMachine Items ;
$ items = Items:: fromFile ( ' path/to.json ' , [ ' decoder ' => new PassThruDecoder ]);
ErrorWrappingDecoder
- Um decorador que agrupa erros de decodificação dentro do objeto DecodingError
, permitindo que você pule itens malformados em vez de morrer na exceção SyntaxError
. Exemplo: <?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 );
}
Desde 0.4.0, cada exceção estende JsonMachineException
, então você pode capturar isso para filtrar qualquer erro da biblioteca JSON Machine.
Se houver um erro em qualquer lugar em um fluxo json, a exceção SyntaxError
será lançada. Isso é muito inconveniente, porque se houver um erro dentro de um item json, você não conseguirá analisar o restante do documento por causa de um item malformado. ErrorWrappingDecoder
é um decorador de decodificador que pode ajudá-lo com isso. Envolva um decodificador com ele, e todos os itens malformados que você está iterando serão fornecidos a você no foreach via DecodingError
. Dessa forma, você pode ignorá-los e continuar com o documento. Veja exemplo em Decodificadores disponíveis. Erros de sintaxe na estrutura de um fluxo json entre os itens iterados ainda gerarão a exceção SyntaxError
.
A complexidade do tempo é sempre O(n)
DR: A complexidade da memória é O(2)
A máquina JSON lê um fluxo (ou arquivo) 1 item JSON por vez e gera 1 item PHP correspondente por vez. Esta é a maneira mais eficiente, porque se você tivesse, digamos, 10.000 usuários no arquivo JSON e quisesse analisá-lo usando json_decode(file_get_contents('big.json'))
, você teria toda a string na memória, bem como todos os 10.000 Estruturas PHP. A tabela a seguir mostra a diferença:
String itens na memória por vez | Itens PHP decodificados na memória por vez | Total | |
---|---|---|---|
json_decode() | 10.000 | 10.000 | 20.000 |
Items::from*() | 1 | 1 | 2 |
Isso significa que JSON Machine é constantemente eficiente para qualquer tamanho de JSON processado. 100 GB sem problemas.
DR: A complexidade da memória é O(n+1)
Existe também um método Items::fromString()
. Se você for forçado a analisar uma string grande e o stream não estiver disponível, JSON Machine pode ser melhor que json_decode
. O motivo é que, diferentemente de json_decode
, o JSON Machine ainda percorre a string JSON, um item por vez, e não carrega todas as estruturas PHP resultantes na memória de uma só vez.
Vamos continuar com o exemplo com 10.000 usuários. Desta vez eles estão todos em string na memória. Ao decodificar essa string com json_decode
, 10.000 arrays (objetos) são criados na memória e então o resultado é retornado. A máquina JSON, por outro lado, cria uma estrutura única para cada item encontrado na string e a devolve para você. Quando você processa esse item e itera para o próximo, outra estrutura única é criada. Este é o mesmo comportamento de fluxos/arquivos. A tabela a seguir coloca o conceito em perspectiva:
String itens na memória por vez | Itens PHP decodificados na memória por vez | Total | |
---|---|---|---|
json_decode() | 10.000 | 10.000 | 20.000 |
Items::fromString() | 10.000 | 1 | 10001 |
A realidade é ainda melhor. Items::fromString
consome cerca de 5x menos memória que json_decode
. A razão é que uma estrutura PHP ocupa muito mais memória do que sua representação JSON correspondente.
Um dos motivos pode ser que os itens que você deseja iterar estejam em alguma subchave, como "results"
mas você esqueceu de especificar um ponteiro JSON. Consulte Analisando uma subárvore.
A outra razão pode ser que um dos itens que você itera é tão grande que não pode ser decodificado de uma só vez. Por exemplo, você itera sobre os usuários e um deles contém milhares de objetos "amigos". A solução mais eficiente é usar a iteração recursiva.
Provavelmente significa que uma única string escalar JSON é grande demais para caber na memória. Por exemplo, um arquivo codificado em base64 muito grande. Nesse caso, você provavelmente ainda estará sem sorte até que o JSON Machine suporte a produção de valores escalares como fluxos PHP.
composer require halaxa/json-machine
Clone ou baixe este repositório e adicione o seguinte ao seu arquivo de inicialização:
spl_autoload_register ( require ' /path/to/json-machine/src/autoloader.php ' );
Clone este repositório. Esta biblioteca oferece suporte a duas abordagens de desenvolvimento:
Execute composer run -l
no diretório do projeto para ver os scripts de desenvolvimento disponíveis. Dessa forma, você pode executar algumas etapas do processo de construção, como testes.
Instale o Docker e execute make
no diretório do projeto em sua máquina host para ver as ferramentas/comandos de desenvolvimento disponíveis. Você pode executar todas as etapas do processo de construção separadamente, bem como todo o processo de construção de uma só vez. Make basicamente executa scripts de desenvolvimento do compositor dentro de contêineres em segundo plano.
make build
: executa a compilação completa. O mesmo comando é executado por meio do GitHub Actions CI.
Você gosta desta biblioteca? Marque com estrela, compartilhe, mostre :) Problemas e pull requests são muito bem-vindos.
Apache 2.0
Elemento roda dentada: ícones feitos por TutsPlus em www.flaticon.com são licenciados por CC 3.0 BY
Índice gerado com markdown-toc