Reemplazo directo muy fácil de usar y con memoria eficiente para iteraciones ineficientes de archivos o flujos JSON grandes para PHP >=7.2. Ver TL;DR. No hay dependencias en producción excepto ext-json
opcional. README en sincronización con el código
NUEVO en la versión 1.2.0
: iteración 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);
}
El acceso aleatorio como $users[42]
aún no es posible. Utilice foreach
mencionado anteriormente y busque el elemento o utilice el puntero JSON.
Cuente los elementos a través de iterator_count($users)
. Recuerde que aún tendrá que iterar internamente todo para obtener el recuento y, por lo tanto, tomará aproximadamente el mismo tiempo que iterarlo y contarlo manualmente.
Requiere ext-json
si se usa de fábrica, pero no si se usa un decodificador personalizado. Ver Decodificadores.
Siga el REGISTRO DE CAMBIOS.
JSON Machine es un analizador de flujo/pull/incremental/lazy (como quiera que lo llame) JSON eficiente, fácil de usar y rápido basado en generadores desarrollados para flujos o documentos JSON impredeciblemente largos. Las características principales son:
foreach
. Sin eventos ni devoluciones de llamadas.json_decode
nativo para decodificar elementos de documentos JSON de forma predeterminada. Ver Decodificadores. Digamos que fruits.json
contiene este enorme documento JSON:
// fruits.json
{
"apple" : {
"color" : " red "
},
"pear" : {
"color" : " yellow "
}
}
Se puede analizar de esta manera:
<?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"
}
El análisis de una matriz json en lugar de un objeto json sigue la misma lógica. La clave en un foreach será un índice numérico de un elemento.
Si prefiere que JSON Machine devuelva matrices en lugar de objetos, utilice new ExtJsonDecoder(true)
como decodificador.
<?php
use JsonMachine JsonDecoder ExtJsonDecoder ;
use JsonMachine Items ;
$ objects = Items:: fromFile ( ' path/to.json ' , [ ' decoder ' => new ExtJsonDecoder ( true )]);
Si desea iterar solo el subárbol results
en fruits.json
:
// fruits.json
{
"results" : {
"apple" : {
"color" : " red "
},
"pear" : {
"color" : " yellow "
}
}
}
use el puntero JSON /results
como opción 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"
}
Nota:
El valor de
results
no se carga en la memoria a la vez, sino solo un elemento deresults
a la vez. Siempre hay un elemento en la memoria a la vez en el nivel/subárbol que está iterando actualmente. Por tanto, el consumo de memoria es constante.
La especificación del puntero JSON también permite utilizar un guión ( -
) en lugar de un índice de matriz específico. JSON Machine lo interpreta como un comodín que coincide con cualquier índice de matriz (no con ninguna clave de objeto). Esto le permite iterar valores anidados en matrices sin cargar todo el elemento.
Ejemplo:
// fruitsArray.json
{
"results" : [
{
"name" : " apple " ,
"color" : " red "
},
{
"name" : " pear " ,
"color" : " yellow "
}
]
}
Para iterar sobre todos los colores de las frutas, utilice el puntero 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 ' ;
}
Puede analizar un único valor escalar en cualquier parte del documento de la misma manera que una colección. Considere este ejemplo:
// fruits.json
{
"lastModified" : " 2012-12-12 " ,
"apple" : {
"color" : " red "
},
"pear" : {
"color" : " yellow "
},
// ... gigabytes follow ...
}
Obtenga el valor escalar de la clave lastModified
de esta manera:
<?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'
}
Cuando el analizador encuentra el valor y se lo entrega, deja de analizar. Entonces, cuando un único valor escalar está al comienzo de un archivo o flujo de tamaño de gigabytes, simplemente obtiene el valor desde el principio en poco tiempo y casi sin consumir memoria.
El atajo obvio es:
<?php
use JsonMachine Items ;
$ fruits = Items:: fromFile ( ' fruits.json ' , [ ' pointer ' => ' /lastModified ' ]);
$ lastModified = iterator_to_array ( $ fruits )[ ' lastModified ' ];
El acceso a un valor escalar único también admite índices de matriz en JSON Pointer.
También es posible analizar varios subárboles utilizando varios punteros JSON. Considere este ejemplo:
// 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 las bayas y cítricos, utilice los punteros JSON ["/berries", "/citrus"]
. El orden de los punteros no importa. Los elementos se repetirán en el orden de aparición en el 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 ' ;
}
Utilice RecursiveItems
en lugar de Items
cuando la estructura JSON sea difícil o incluso imposible de manejar con Items
y punteros JSON o los elementos individuales que itere sean demasiado grandes para manejarlos. Por otro lado, es notablemente más lento que Items
, así que tenlo en cuenta.
Cuando RecursiveItems
encuentra una lista o dictado en JSON, devuelve una nueva instancia de sí mismo que luego se puede iterar y el ciclo se repite. Por lo tanto, nunca devuelve una matriz u objeto PHP, sino solo valores escalares o RecursiveItems
. Ningún dictado ni lista JSON se cargará completamente en la memoria a la vez.
Veamos un ejemplo con muchísimos usuarios con muchísimos 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 ' ;
}
}
}
}
}
Si rompes una iteración de un nivel tan profundo y perezoso (es decir, te saltas algunos
"friends"
mediantebreak
) y avanzas al siguiente valor (es decir, el siguienteuser
), no podrás repetirla más tarde. JSON Machine debe iterarlo en segundo plano para poder leer el siguiente valor. Tal intento resultará en una excepción del generador cerrado.
RecursiveItems
toArray(): array
Si está seguro de que una determinada instancia de RecursiveItems apunta a una estructura de datos manejable en memoria (por ejemplo, $friend), puede llamar $friend->toArray()
y el elemento se materializará en un matriz PHP simple.
advanceToKey(int|string $key): scalar|RecursiveItems
Al buscar una clave específica en una colección (por ejemplo, 'friends'
en $user
), no necesita usar un bucle y una condición para buscarla. En su lugar, simplemente puede llamar $user->advanceToKey("friends")
. Iterará por usted y devolverá el valor en esta clave. Las llamadas se pueden encadenar. También admite una sintaxis similar a una matriz para avanzar y obtener los siguientes índices. Entonces $user['friends']
sería un alias para $user->advanceToKey('friends')
. Las llamadas se pueden encadenar. Tenga en cuenta que es solo un alias: no podrá acceder aleatoriamente a índices anteriores después de usarlo directamente en RecursiveItems
. Es solo un azúcar de sintaxis. Utilice toArray()
si necesita acceso aleatorio a índices en un registro/elemento.
Por tanto, el ejemplo anterior podría simplificarse de la siguiente manera:
<?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 ' ;
}
}
El encadenamiento le permite hacer algo como esto:
<?php
use JsonMachine RecursiveItems
$ users = RecursiveItems:: fromFile ( ' users.json ' );
$ users [ 0 ][ ' friends ' ][ 1 ][ ' username ' ] === ' friend2 ' ;
RecursiveItems implements RecursiveIterator
Entonces puedes usar, por ejemplo, las herramientas integradas de PHP para trabajar con RecursiveIterator
como estas:
Es una forma de abordar un elemento en un documento JSON. Consulte JSON Pointer RFC 6901. Es muy útil, porque a veces la estructura JSON es más profunda y desea iterar un subárbol, no el nivel principal. Así que simplemente especifica el puntero a la matriz u objeto JSON (o incluso a un valor escalar) que desea iterar y listo. Cuando el analizador llega a la colección que especificó, comienza la iteración. Puede pasarlo como opción pointer
en todas las funciones Items::from*
. Si especifica un puntero a una posición inexistente en el documento, se genera una excepción. También se puede utilizar para acceder a valores escalares. El puntero JSON en sí debe ser una cadena JSON válida . La comparación literal de tokens de referencia (las partes entre barras) se realiza con las claves/nombres de miembros del documento JSON.
Algunos ejemplos:
Valor del puntero JSON | Iterará a través de |
---|---|
(cadena vacía - predeterminado) | ["this", "array"] o {"a": "this", "b": "object"} se iterará (nivel principal) |
/result/items | {"result": {"items": ["this", "array", "will", "be", "iterated"]}} |
/0/items | [{"items": ["this", "array", "will", "be", "iterated"]}] (admite índices de matriz) |
/results/-/status | {"results": [{"status": "iterated"}, {"status": "also iterated"}]} (un guión como comodín de índice de matriz) |
/ (¡Te tengo! - una barra diagonal seguida de una cadena vacía, consulta las especificaciones) | {"":["this","array","will","be","iterated"]} |
/quotes" | {"quotes"": ["this", "array", "will", "be", "iterated"]} |
Las opciones pueden cambiar la forma en que se analiza un JSON. La matriz de opciones es el segundo parámetro de todas las funciones Items::from*
. Las opciones disponibles son:
pointer
: una cadena de puntero JSON que indica qué parte del documento desea iterar.decoder
: una instancia de la interfaz ItemDecoder
.debug
: true
o false
para habilitar o deshabilitar el modo de depuración. Cuando el modo de depuración está habilitado, datos como línea, columna y posición en el documento están disponibles durante el análisis o en excepciones. Mantener la depuración desactivada añade una ligera ventaja de rendimiento. Una respuesta API de transmisión o cualquier otra transmisión JSON se analiza exactamente de la misma manera que el archivo. La única diferencia es que usa Items::fromStream($streamResource)
para ello, donde $streamResource
es el recurso de transmisión con el documento JSON. El resto es igual que con el análisis de archivos. A continuación se muestran algunos ejemplos de clientes http populares que admiten respuestas en streaming:
Guzzle usa sus propios flujos, pero se pueden convertir nuevamente en flujos PHP llamando a GuzzleHttpPsr7StreamWrapper::getResource()
. Pase el resultado de esta función a la función Items::fromStream
y estará listo. Vea el ejemplo funcional de GuzzleHttp.
Una respuesta de flujo de Symfony HttpClient funciona como iterador. Y como JSON Machine se basa en iteradores, la integración con Symfony HttpClient es muy sencilla. Vea el ejemplo de HttpClient.
debug
habilitada) Los documentos grandes pueden tardar un poco en analizarse. Llame Items::getPosition()
en su foreach
para obtener el recuento actual de los bytes procesados desde el principio. Entonces, el porcentaje es fácil de calcular como position / total * 100
. Para conocer el tamaño total de su documento en bytes, es posible que desee verificar:
strlen($document)
si analiza una cadenafilesize($file)
si analiza un archivoContent-Length
si analiza una respuesta de flujo http Si debug
está deshabilitada, getPosition()
siempre devuelve 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 ) . ' % ' ;
}
Las funciones Items::from*
también aceptan la opción decoder
. Debe ser una instancia de JsonMachineJsonDecoderItemDecoder
. Si no se especifica ninguno, se utiliza ExtJsonDecoder
de forma predeterminada. Requiere que esté presente la extensión PHP ext-json
, porque usa json_decode
. Cuando json_decode
no hace lo que desea, implemente JsonMachineJsonDecoderItemDecoder
y cree el suyo propio.
ExtJsonDecoder
: predeterminado. Utiliza json_decode
para decodificar claves y valores. El constructor tiene los mismos parámetros que json_decode
.
PassThruDecoder
: no decodifica. Tanto las claves como los valores se generan como cadenas JSON puras. Útil cuando desea analizar un elemento JSON con otra cosa directamente en el foreach y no desea implementar JsonMachineJsonDecoderItemDecoder
. Desde 1.0.0
no usa json_decode
.
Ejemplo:
<?php
use JsonMachine JsonDecoder PassThruDecoder ;
use JsonMachine Items ;
$ items = Items:: fromFile ( ' path/to.json ' , [ ' decoder ' => new PassThruDecoder ]);
ErrorWrappingDecoder
: un decorador que envuelve los errores de decodificación dentro del objeto DecodingError
, lo que le permite omitir elementos con formato incorrecto en lugar de morir por la excepción SyntaxError
. Ejemplo: <?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 excepción extiende JsonMachineException
, por lo que puede detectarla para filtrar cualquier error de la biblioteca JSON Machine.
Si hay un error en algún lugar de una secuencia json, se genera una excepción SyntaxError
. Esto es muy inconveniente, porque si hay un error dentro de un elemento json, no podrá analizar el resto del documento debido a un elemento con formato incorrecto. ErrorWrappingDecoder
es un decorador decodificador que puede ayudarte con eso. Envuelva un decodificador con él y todos los elementos con formato incorrecto que esté iterando se le entregarán en foreach a través de DecodingError
. De esta manera puedes omitirlos y continuar con el documento. Ver ejemplo en Decodificadores disponibles. Sin embargo, los errores de sintaxis en la estructura de una secuencia json entre los elementos iterados aún generarán una excepción SyntaxError
.
La complejidad del tiempo es siempre O(n)
TL;DR: La complejidad de la memoria es O(2)
JSON Machine lee una secuencia (o un archivo) 1 elemento JSON a la vez y genera 1 elemento PHP correspondiente a la vez. Esta es la forma más eficiente, porque si tuviera, digamos, 10,000 usuarios en un archivo JSON y quisiera analizarlo usando json_decode(file_get_contents('big.json'))
, tendría toda la cadena en la memoria, así como los 10,000. Estructuras PHP. La siguiente tabla muestra la diferencia:
Encadenar elementos en la memoria a la vez | Elementos PHP decodificados en la memoria a la vez | Total | |
---|---|---|---|
json_decode() | 10000 | 10000 | 20000 |
Items::from*() | 1 | 1 | 2 |
Esto significa que JSON Machine es constantemente eficiente para cualquier tamaño de JSON procesado. 100 GB no hay problema.
TL;DR: La complejidad de la memoria es O(n+1)
También hay un método Items::fromString()
. Si se ve obligado a analizar una cadena grande y la transmisión no está disponible, JSON Machine puede ser mejor que json_decode
. La razón es que, a diferencia de json_decode
, JSON Machine aún atraviesa la cadena JSON un elemento a la vez y no carga todas las estructuras PHP resultantes en la memoria a la vez.
Sigamos con el ejemplo de 10.000 usuarios. Esta vez están todos encordados en la memoria. Al decodificar esa cadena con json_decode
, se crean 10,000 matrices (objetos) en la memoria y luego se devuelve el resultado. JSON Machine, por otro lado, crea una estructura única para cada elemento encontrado en la cadena y se la devuelve. Cuando procesa este elemento y pasa al siguiente, se crea otra estructura única. Este es el mismo comportamiento que con las secuencias/archivos. La siguiente tabla pone el concepto en perspectiva:
Encadenar elementos en la memoria a la vez | Elementos PHP decodificados en la memoria a la vez | Total | |
---|---|---|---|
json_decode() | 10000 | 10000 | 20000 |
Items::fromString() | 10000 | 1 | 10001 |
La realidad es aún mejor. Items::fromString
consume aproximadamente 5 veces menos memoria que json_decode
. La razón es que una estructura PHP requiere mucha más memoria que su correspondiente representación JSON.
Una de las razones puede ser que los elementos sobre los que desea iterar estén en alguna subclave como "results"
pero olvidó especificar un puntero JSON. Consulte Análisis de un subárbol.
La otra razón puede ser que uno de los elementos que itera sea tan grande que no se pueda decodificar de inmediato. Por ejemplo, iteras sobre usuarios y uno de ellos tiene miles de objetos "amigos". La solución más eficaz es utilizar la iteración recursiva.
Probablemente signifique que una sola cadena escalar JSON es demasiado grande para caber en la memoria. Por ejemplo, un archivo muy grande codificado en base64. En ese caso, probablemente todavía no tendrá suerte hasta que JSON Machine admita la generación de valores escalares como flujos PHP.
composer require halaxa/json-machine
Clona o descarga este repositorio y agrega lo siguiente a tu archivo de arranque:
spl_autoload_register ( require ' /path/to/json-machine/src/autoloader.php ' );
Clona este repositorio. Esta biblioteca admite dos enfoques de desarrollo:
Ejecute composer run -l
en el directorio del proyecto para ver los scripts de desarrollo disponibles. De esta manera puede ejecutar algunos pasos del proceso de compilación, como las pruebas.
Instale Docker y ejecute make
en el directorio del proyecto en su máquina host para ver las herramientas/comandos de desarrollo disponibles. Puede ejecutar todos los pasos del proceso de compilación por separado, así como todo el proceso de compilación a la vez. Básicamente, Make ejecuta scripts de desarrollo de Composer dentro de contenedores en segundo plano.
make build
: ejecuta la compilación completa. El mismo comando se ejecuta a través de GitHub Actions CI.
¿Te gusta esta biblioteca? Destacalo, compártelo, muéstralo :) Los problemas y las solicitudes de extracción son bienvenidos.
apache 2.0
Elemento de rueda dentada: los iconos creados por TutsPlus de www.flaticon.com tienen licencia CC 3.0 BY
Tabla de contenidos generada con markdown-toc