Remplacement instantané très facile à utiliser et économe en mémoire pour les itérations inefficaces de gros fichiers ou flux JSON pour PHP >=7.2. Voir TL;DR. Aucune dépendance en production sauf ext-json
facultatif. README en synchronisation avec le code
NOUVEAUTÉ de la version 1.2.0
- Itération récursive
<?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);
}
L'accès aléatoire comme $users[42]
n'est pas encore possible. Utilisez foreach
mentionné ci-dessus et recherchez l'élément ou utilisez le pointeur JSON.
Comptez les éléments via iterator_count($users)
. N'oubliez pas qu'il faudra toujours itérer le tout en interne pour obtenir le décompte et cela prendra donc à peu près le même temps que l'itérer et le compter à la main.
Nécessite ext-json
s'il est utilisé directement, mais ce n'est pas le cas si un décodeur personnalisé est utilisé. Voir Décodeurs.
Suivez CHANGELOG.
JSON Machine est un analyseur de flux/pull/incrémental/paresseux JSON efficace, facile à utiliser et rapide (quel que soit votre nom) basé sur des générateurs développés pour des flux ou des documents JSON de longueur imprévisible. Les principales caractéristiques sont :
foreach
. Aucun événement ni rappel.json_decode
natif pour décoder les éléments de document JSON par défaut. Voir Décodeurs. Disons que fruits.json
contient cet énorme document JSON :
// fruits.json
{
"apple" : {
"color" : " red "
},
"pear" : {
"color" : " yellow "
}
}
Il peut être analysé de cette façon :
<?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"
}
L'analyse d'un tableau json au lieu d'un objet json suit la même logique. La clé dans un foreach sera un index numérique d'un élément.
Si vous préférez que JSON Machine renvoie des tableaux plutôt que des objets, utilisez new ExtJsonDecoder(true)
comme décodeur.
<?php
use JsonMachine JsonDecoder ExtJsonDecoder ;
use JsonMachine Items ;
$ objects = Items:: fromFile ( ' path/to.json ' , [ ' decoder ' => new ExtJsonDecoder ( true )]);
Si vous souhaitez parcourir uniquement le sous-arbre results
dans ce fruits.json
:
// fruits.json
{
"results" : {
"apple" : {
"color" : " red "
},
"pear" : {
"color" : " yellow "
}
}
}
utilisez JSON Pointer /results
comme option 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"
}
Note:
La valeur des
results
n'est pas chargée en mémoire en une seule fois, mais seulement un élément desresults
à la fois. Il s'agit toujours d'un élément en mémoire à la fois au niveau/sous-arbre que vous parcourez actuellement. Ainsi, la consommation mémoire est constante.
La spécification JSON Pointer permet également d'utiliser un trait d'union ( -
) au lieu d'un index de tableau spécifique. JSON Machine l'interprète comme un caractère générique qui correspond à n'importe quel index de tableau (pas à n'importe quelle clé d'objet). Cela vous permet d'itérer des valeurs imbriquées dans des tableaux sans charger l'intégralité de l'élément.
Exemple:
// fruitsArray.json
{
"results" : [
{
"name" : " apple " ,
"color" : " red "
},
{
"name" : " pear " ,
"color" : " yellow "
}
]
}
Pour parcourir toutes les couleurs des fruits, utilisez le pointeur 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 ' ;
}
Vous pouvez analyser une seule valeur scalaire n'importe où dans le document de la même manière qu'une collection. Considérez cet exemple :
// fruits.json
{
"lastModified" : " 2012-12-12 " ,
"apple" : {
"color" : " red "
},
"pear" : {
"color" : " yellow "
},
// ... gigabytes follow ...
}
Obtenez la valeur scalaire de la clé lastModified
comme ceci :
<?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'
}
Lorsque l'analyseur trouve la valeur et vous la donne, il arrête l'analyse. Ainsi, lorsqu'une seule valeur scalaire se trouve au début d'un fichier ou d'un flux de plusieurs gigaoctets, elle obtient simplement la valeur depuis le début en un rien de temps et avec presque aucune consommation de mémoire.
Le raccourci évident est :
<?php
use JsonMachine Items ;
$ fruits = Items:: fromFile ( ' fruits.json ' , [ ' pointer ' => ' /lastModified ' ]);
$ lastModified = iterator_to_array ( $ fruits )[ ' lastModified ' ];
L'accès à une valeur scalaire unique prend également en charge les indices de tableau dans le pointeur JSON.
Il est également possible d'analyser plusieurs sous-arbres à l'aide de plusieurs pointeurs JSON. Considérez cet exemple :
// 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 "
}
]
}
Pour parcourir toutes les baies et agrumes, utilisez les pointeurs JSON ["/berries", "/citrus"]
. L'ordre des pointeurs n'a pas d'importance. Les éléments seront itérés dans l’ordre d’apparition dans le document.
<?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 ' ;
}
Utilisez RecursiveItems
au lieu de Items
lorsque la structure JSON est difficile, voire impossible, à gérer avec Items
et des pointeurs JSON ou que les éléments individuels que vous itérez sont trop volumineux à gérer. D'un autre côté, il est nettement plus lent que Items
, alors gardez cela à l'esprit.
Lorsque RecursiveItems
rencontre une liste ou un dict dans le JSON, il renvoie une nouvelle instance de lui-même qui peut ensuite être itérée et le cycle se répète. Ainsi, il ne renvoie jamais un tableau ou un objet PHP, mais uniquement des valeurs scalaires ou RecursiveItems
. Aucun dict ni liste JSON ne sera jamais entièrement chargé en mémoire en même temps.
Voyons un exemple avec de très nombreux utilisateurs et de très nombreux amis :
// 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 vous interrompez une itération d'un niveau aussi paresseux et plus profond (c'est-à-dire que vous sautez quelques
"friends"
viabreak
) et avancez vers une valeur suivante (c'est-à-dire nextuser
), vous ne pourrez pas l'itérer plus tard. JSON Machine doit l'itérer en arrière-plan pour pouvoir lire la valeur suivante. Une telle tentative entraînera une exception de générateur fermé.
RecursiveItems
toArray(): array
Si vous êtes sûr qu'une certaine instance de RecursiveItems pointe vers une structure de données gérable en mémoire (par exemple, $friend), vous pouvez appeler $friend->toArray()
, et l'élément se matérialisera en un tableau PHP simple.
advanceToKey(int|string $key): scalar|RecursiveItems
Lors de la recherche d'une clé spécifique dans une collection (par exemple, 'friends'
dans $user
), vous n'avez pas besoin d'utiliser une boucle ni une condition pour la rechercher. Au lieu de cela, vous pouvez simplement appeler $user->advanceToKey("friends")
. Il effectuera une itération pour vous et renverra la valeur à cette clé. Les appels peuvent être enchaînés. Il prend également en charge la syntaxe de type tableau pour avancer et obtenir les index suivants. Donc $user['friends']
serait un alias pour $user->advanceToKey('friends')
. Les appels peuvent être enchaînés. Gardez à l'esprit qu'il ne s'agit que d'un alias - vous ne pourrez pas accéder de manière aléatoire aux index précédents après l'avoir utilisé directement sur RecursiveItems
. C'est juste un sucre de syntaxe. Utilisez toArray()
si vous avez besoin d'un accès aléatoire aux index d'un enregistrement/élément.
L’exemple précédent pourrait ainsi être simplifié comme suit :
<?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 ' ;
}
}
Le chaînage vous permet de faire quelque chose comme ceci :
<?php
use JsonMachine RecursiveItems
$ users = RecursiveItems:: fromFile ( ' users.json ' );
$ users [ 0 ][ ' friends ' ][ 1 ][ ' username ' ] === ' friend2 ' ;
RecursiveItems implements RecursiveIterator
Vous pouvez donc utiliser par exemple les outils intégrés de PHP pour travailler sur RecursiveIterator
comme ceux-ci :
C'est une façon de traiter un élément du document JSON. Voir le JSON Pointer RFC 6901. C'est très pratique, car parfois la structure JSON va plus loin et vous souhaitez parcourir un sous-arbre, pas le niveau principal. Il vous suffit donc de spécifier le pointeur vers le tableau ou l'objet JSON (ou même vers une valeur scalaire) que vous souhaitez parcourir et c'est parti. Lorsque l'analyseur atteint la collection que vous avez spécifiée, l'itération commence. Vous pouvez le transmettre comme option pointer
dans toutes les fonctions Items::from*
. Si vous spécifiez un pointeur vers une position inexistante dans le document, une exception est levée. Il peut également être utilisé pour accéder à des valeurs scalaires. JSON Le pointeur lui-même doit être une chaîne JSON valide . Une comparaison littérale des jetons de référence (les parties entre les barres obliques) est effectuée par rapport aux clés/noms de membre du document JSON.
Quelques exemples :
Valeur du pointeur JSON | Parcourra |
---|---|
(chaîne vide - par défaut) | ["this", "array"] ou {"a": "this", "b": "object"} sera itéré (niveau principal) |
/result/items | {"result": {"items": ["this", "array", "will", "be", "iterated"]}} |
/0/items | [{"items": ["this", "array", "will", "be", "iterated"]}] (prend en charge les indices de tableau) |
/results/-/status | {"results": [{"status": "iterated"}, {"status": "also iterated"}]} (un trait d'union comme caractère générique d'index de tableau) |
/ (je t'ai eu ! - une barre oblique suivie d'une chaîne vide, voir la spécification) | {"":["this","array","will","be","iterated"]} |
/quotes" | {"quotes"": ["this", "array", "will", "be", "iterated"]} |
Les options peuvent modifier la façon dont un JSON est analysé. Le tableau d'options est le deuxième paramètre de toutes Items::from*
. Les options disponibles sont :
pointer
- Une chaîne de pointeur JSON qui indique la partie du document que vous souhaitez parcourir.decoder
- Une instance de l'interface ItemDecoder
.debug
- true
ou false
pour activer ou désactiver le mode débogage. Lorsque le mode débogage est activé, les données telles que la ligne, la colonne et la position dans le document sont disponibles lors de l'analyse ou dans les exceptions. Garder le débogage désactivé ajoute un léger avantage en termes de performances. Une réponse API de flux ou tout autre flux JSON est analysé exactement de la même manière qu'un fichier. La seule différence est que vous utilisez Items::fromStream($streamResource)
pour cela, où $streamResource
est la ressource de flux avec le document JSON. Le reste est le même que pour l'analyse des fichiers. Voici quelques exemples de clients http populaires prenant en charge les réponses en continu :
Guzzle utilise ses propres flux, mais ils peuvent être reconvertis en flux PHP en appelant GuzzleHttpPsr7StreamWrapper::getResource()
. Transmettez le résultat de cette fonction à la fonction Items::fromStream
et vous êtes configuré. Voir l'exemple fonctionnel de GuzzleHttp.
Une réponse de flux de Symfony HttpClient fonctionne comme itérateur. Et comme JSON Machine est basé sur des itérateurs, l'intégration avec Symfony HttpClient est très simple. Voir l'exemple HttpClient.
debug
activé) L'analyse des documents volumineux peut prendre un certain temps. Appelez Items::getPosition()
dans votre foreach
pour obtenir le nombre actuel d'octets traités depuis le début. Le pourcentage est alors facile à calculer sous la forme position / total * 100
. Pour connaître la taille totale de votre document en octets, vous pouvez vérifier :
strlen($document)
si vous analysez une chaînefilesize($file)
si vous analysez un fichierContent-Length
si vous analysez une réponse de flux http Si debug
est désactivé, getPosition()
renvoie toujours 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 ) . ' % ' ;
}
Les fonctions Items::from*
acceptent également l’option decoder
. Il doit s'agir d'une instance de JsonMachineJsonDecoderItemDecoder
. Si aucun n’est spécifié, ExtJsonDecoder
est utilisé par défaut. Il nécessite que l'extension PHP ext-json
soit présente, car elle utilise json_decode
. Lorsque json_decode
ne fait pas ce que vous voulez, implémentez JsonMachineJsonDecoderItemDecoder
et créez le vôtre.
ExtJsonDecoder
- Par défaut. Utilise json_decode
pour décoder les clés et les valeurs. Le constructeur a les mêmes paramètres que json_decode
.
PassThruDecoder
- N'effectue aucun décodage. Les clés et les valeurs sont produites sous forme de chaînes JSON pures. Utile lorsque vous souhaitez analyser un élément JSON avec autre chose directement dans le foreach et que vous ne souhaitez pas implémenter JsonMachineJsonDecoderItemDecoder
. Depuis 1.0.0
n'utilise pas json_decode
.
Exemple:
<?php
use JsonMachine JsonDecoder PassThruDecoder ;
use JsonMachine Items ;
$ items = Items:: fromFile ( ' path/to.json ' , [ ' decoder ' => new PassThruDecoder ]);
ErrorWrappingDecoder
- Un décorateur qui encapsule les erreurs de décodage dans l'objet DecodingError
, vous permettant ainsi d'ignorer les éléments mal formés au lieu de mourir sur l'exception SyntaxError
. Exemple: <?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 );
}
Depuis la version 0.4.0, chaque exception étend JsonMachineException
, vous pouvez donc l'attraper pour filtrer toute erreur de la bibliothèque JSON Machine.
S'il y a une erreur n'importe où dans un flux json, l'exception SyntaxError
est levée. C'est très gênant, car s'il y a une erreur dans un élément json, vous ne pouvez pas analyser le reste du document à cause d'un élément mal formé. ErrorWrappingDecoder
est un décorateur de décodeur qui peut vous aider. Enveloppez un décodeur avec, et tous les éléments mal formés que vous itérez vous seront donnés dans le foreach via DecodingError
. De cette façon, vous pouvez les ignorer et continuer avec le document. Voir exemple dans Décodeurs disponibles. Les erreurs de syntaxe dans la structure d'un flux json entre les éléments itérés généreront toujours une exception SyntaxError
.
La complexité temporelle est toujours O(n)
TL;DR : La complexité de la mémoire est O(2)
JSON Machine lit un flux (ou un fichier) 1 élément JSON à la fois et génère 1 élément PHP correspondant à la fois. C'est le moyen le plus efficace, car si vous aviez, par exemple, 10 000 utilisateurs dans un fichier JSON et que vous vouliez l'analyser à l'aide json_decode(file_get_contents('big.json'))
, vous auriez toute la chaîne en mémoire ainsi que les 10 000 Structures PHP. Le tableau suivant montre la différence :
Chaîne d'éléments en mémoire à la fois | Éléments PHP décodés en mémoire à la fois | Total | |
---|---|---|---|
json_decode() | 10000 | 10000 | 20000 |
Items::from*() | 1 | 1 | 2 |
Cela signifie que JSON Machine est constamment efficace pour n'importe quelle taille de JSON traité. 100 Go pas de problème.
TL;DR : La complexité de la mémoire est O(n+1)
Il existe également une méthode Items::fromString()
. Si vous êtes obligé d'analyser une grosse chaîne et que le flux n'est pas disponible, JSON Machine peut être meilleur que json_decode
. La raison en est que contrairement à json_decode
, JSON Machine parcourt toujours la chaîne JSON un élément à la fois et ne charge pas simultanément toutes les structures PHP résultantes en mémoire.
Continuons avec l'exemple avec 10 000 utilisateurs. Cette fois, ils sont tous en chaîne en mémoire. Lors du décodage de cette chaîne avec json_decode
, 10 000 tableaux (objets) sont créés en mémoire, puis le résultat est renvoyé. JSON Machine, quant à lui, crée une structure unique pour chaque élément trouvé dans la chaîne et vous la renvoie. Lorsque vous traitez cet élément et passez au suivant, une autre structure unique est créée. C'est le même comportement qu'avec les flux/fichiers. Le tableau suivant met le concept en perspective :
Chaîne d'éléments en mémoire à la fois | Éléments PHP décodés en mémoire à la fois | Total | |
---|---|---|---|
json_decode() | 10000 | 10000 | 20000 |
Items::fromString() | 10000 | 1 | 10001 |
La réalité est encore meilleure. Items::fromString
consomme environ 5 fois moins de mémoire que json_decode
. La raison en est qu'une structure PHP prend beaucoup plus de mémoire que sa représentation JSON correspondante.
L'une des raisons peut être que les éléments sur lesquels vous souhaitez parcourir se trouvent dans une sous-clé telle que "results"
mais vous avez oublié de spécifier un pointeur JSON. Voir Analyse d'un sous-arbre.
L'autre raison peut être que l'un des éléments que vous itérez est lui-même si énorme qu'il ne peut pas être décodé en même temps. Par exemple, vous parcourez les utilisateurs et l'un d'eux contient des milliers d'objets "amis". La solution la plus efficace consiste à utiliser l’itération récursive.
Cela signifie probablement qu'une seule chaîne scalaire JSON elle-même est trop grande pour tenir en mémoire. Par exemple, un très gros fichier codé en base64. Dans ce cas, vous n'aurez probablement toujours pas de chance jusqu'à ce que JSON Machine prenne en charge la production de valeurs scalaires sous forme de flux PHP.
composer require halaxa/json-machine
Clonez ou téléchargez ce référentiel et ajoutez les éléments suivants à votre fichier d'amorçage :
spl_autoload_register ( require ' /path/to/json-machine/src/autoloader.php ' );
Clonez ce référentiel. Cette bibliothèque prend en charge deux approches de développement :
Exécutez composer run -l
dans le répertoire du projet pour voir les scripts de développement disponibles. De cette façon, vous pouvez exécuter certaines étapes du processus de construction telles que des tests.
Installez Docker et exécutez make
dans le répertoire du projet sur votre machine hôte pour voir les outils/commandes de développement disponibles. Vous pouvez exécuter toutes les étapes du processus de génération séparément ainsi que l'ensemble du processus de génération en même temps. Make exécute essentiellement les scripts de développement du compositeur dans des conteneurs en arrière-plan.
make build
: exécute la construction complète. La même commande est exécutée via GitHub Actions CI.
Aimez-vous cette bibliothèque? Mettez-le en vedette, partagez-le, montrez-le :) Les problèmes et les demandes de tirage sont les bienvenus.
Apache2.0
Élément roue dentée : les icônes créées par TutsPlus à partir de www.flaticon.com sont sous licence CC 3.0 BY
Table des matières générée avec markdown-toc