
Analyse JSON pour les charges utiles volumineuses : équilibre entre vitesse, mémoire et évolutivité
Introduction
La campagne que vous avez mise en place pour le Black Friday a été un énorme succès et les clients commencent à affluer sur votre site Web. Votre configuration Mixpanel, qui aurait généralement environ 1 000 événements clients par heure, finit par avoir des millions d’événements clients en une heure. Ainsi, votre pipeline de données est désormais chargé d’analyser de grandes quantités de données JSON et de les stocker dans votre base de données. Vous constatez que votre bibliothèque d’analyse JSON standard n’est pas en mesure de s’adapter à la croissance soudaine des données et que vos rapports d’analyse en temps quasi réel prennent du retard. C’est alors que vous réalisez l’importance d’une bibliothèque d’analyse JSON efficace. En plus de gérer des charges utiles volumineuses, les bibliothèques d’analyse JSON devraient être capables de sérialiser et de désérialiser des charges utiles JSON hautement imbriquées.
Dans cet article, nous explorons les bibliothèques d’analyse Python pour les charges utiles volumineuses. Nous examinons spécifiquement les capacités d’ujson, orjson et ijson. Nous évaluons ensuite la bibliothèque JSON standard (stdlib/json), ujson et orjson pour les performances de sérialisation et de désérialisation. Comme nous utilisons les termes sérialisation et désérialisation tout au long de l’article, voici un rappel sur les concepts. La sérialisation implique la conversion de vos objets Python en chaîne JSON, tandis que la désérialisation implique la reconstruction de la chaîne JSON à partir de vos structures de données Python.
Au fur et à mesure que nous progressons dans l’article, vous trouverez un diagramme de flux de décision pour vous aider à décider de l’analyseur à utiliser en fonction de votre flux de travail et de vos besoins d’analyse uniques. En plus de cela, nous explorons également NDJSON et les bibliothèques pour analyser les charges utiles NDJSON. Commençons.
StdlibJSON
Stdlib JSON prend en charge la sérialisation pour tous les types de données Python de base, y compris les dictionnaires, les listes et les tuples. Lorsque la fonction json.loads() est appelée, elle charge l’intégralité du JSON en mémoire en une seule fois. Cela convient aux charges utiles plus petites, mais pour les charges utiles plus volumineuses, json.loads() peut entraîner des problèmes de performances critiques tels que des erreurs de mémoire insuffisante et un étouffement des flux de travail en aval.
import json
with open("large_payload.json", "r") as f:
json_data = json.loads(f) #loads entire file into memory, all tokens at once
Ijson
Pour les charges utiles de l’ordre de plusieurs centaines de Mo, il est conseillé d’utiliser ijson. ijson, abréviation de « json itératif », lit les fichiers un jeton à la fois sans surcharge de mémoire. Dans le code ci-dessous, nous comparons json et ijson.
#The ijson library reads records one token at a time
import ijson
with open("json_data.json", "r") as f:
for record in ijson.items(f, "items.item"): #fetch one dict from the array
process(record)
Comme vous pouvez le voir, ijson récupère un élément à la fois du JSON et le charge dans un objet Python dict. Ceci est ensuite transmis à la fonction appelante, dans ce cas, la fonction process(record). Le fonctionnement global d’ijson a été présenté dans l’illustration ci-dessous.

Ujson

Ujson est une bibliothèque largement utilisée dans de nombreuses applications impliquant de grandes charges utiles JSON, car elle a été conçue pour être une alternative plus rapide au stdlib JSON en Python. La vitesse d’analyse est excellente puisque le code sous-jacent d’ujson a été écrit en C, avec des liaisons Python qui se connectent à l’interface Python. Les domaines qui devaient être améliorés dans la bibliothèque JSON standard ont été optimisés dans Ujson pour la vitesse et les performances. Mais Ujson n’est plus utilisé dans les projets plus récents, car les créateurs eux-mêmes l’ont mentionné sur PyPI que la bibliothèque a été placée en mode maintenance uniquement. Vous trouverez ci-dessous une illustration des processus d’ujson à un niveau élevé.
import ujson
taxonomy_data = '{"id":1, "genus":"Thylacinus", "species":"cynocephalus", "extinct": true}'
data_dict = ujson.loads(taxonomy_data) #Deserialize
with open("taxonomy_data.json", "w") as fh: #Serialize
ujson.dump(data_dict, fh)
with open("taxonomy_data.json", "r") as fh: #Deserialize
data = ujson.load(fh)
print(data)
Nous passons à la bibliothèque potentielle suivante nommée « orjson ».
orjson
Étant donné qu’Orjson est écrit en Rust, il est optimisé non seulement pour la vitesse, mais dispose également de mécanismes de sécurité de la mémoire pour empêcher les débordements de tampon auxquels les développeurs sont confrontés lorsqu’ils utilisent des bibliothèques JSON basées sur C comme ujson. De plus, Orjson prend en charge la sérialisation de plusieurs types de données supplémentaires au-delà des types de données Python standard, notamment les objets dataclass et datetime. Une autre différence clé entre orjson et les autres bibliothèques est que la fonction dumps() d’orjson renvoie un objet bytes, tandis que les autres renvoient une chaîne. Le renvoi des données sous forme d’objet octets est l’une des principales raisons du débit rapide d’orjson.
import orjson
book_payload = '{"id":1,"name":"The Great Gatsby","author":"F. Scott Fitzgerald","Publishing House":"Charles Scribner\'s Sons"}'
data_dict = orjson.loads(book_payload) #Deserialize
print(data_dict)
with open("book_data.json", "wb") as f: #Serialize
f.write(orjson.dumps(data_dict)) #Returns bytes object
with open("book_data.json", "rb") as f:#Deserialize
book_data = orjson.loads(f.read())
print(book_data)
Maintenant que nous avons exploré certaines bibliothèques d’analyse JSON, testons leurs capacités de sérialisation.
Test des capacités de sérialisation de JSON, ujson et orjson
Nous créons un exemple d’objet dataclass avec un entier, une chaîne et une variable datetime.
from dataclasses import dataclass
from datetime import datetime
@dataclass
class User:
id: int
name: str
created: datetime
u = User(id=1, name="Thomas", created=datetime.now())
Nous le transmettons ensuite à chacune des bibliothèques pour voir ce qui se passe. Nous commençons par le stdlib JSON.
import json
try:
print("json:", json.dumps(u))
except TypeError as e:
print("json error:", e)
Comme prévu, nous obtenons l’erreur suivante. (La bibliothèque JSON standard ne prend pas en charge la sérialisation des objets « dataclass » et des objets datetime.)

Ensuite, nous testons la même chose avec la bibliothèque ujson.
import ujson
try:
print("json:", ujson.dumps(u))
except TypeError as e:
print("json error:", e)

Comme nous le voyons ci-dessus, ujson n’est pas capable de sérialiser l’objet de classe de données et le type de données datetime. Enfin, nous utilisons la bibliothèque orjson pour la sérialisation.
import orjson
try:
print("orjson:", orjson.dumps(u))
except TypeError as e:
print("orjson error:", e)
Nous voyons qu’orjson a été capable de sérialiser à la fois les types de données dataclass et datetime.

Travailler avec NDJSON (Une mention spéciale)
Nous avons vu les bibliothèques pour l’analyse JSON, mais qu’en est-il de NDJSON ? NDJSON (Newline Delimited JSON), comme vous le savez peut-être, est un format dans lequel chaque ligne est un objet JSON. En d’autres termes, le délimiteur n’est pas une virgule mais un caractère de nouvelle ligne. A titre d’exemple, voici à quoi ressemble NDJSON.
{"id": "A13434", "name": "Ella"}
{"id": "A13455", "name": "Charmont"}
{"id": "B32434", "name": "Areida"}
NDJSON est principalement utilisé pour les journaux et les données en streaming. Par conséquent, les charges utiles NDJSON sont d’excellents candidats pour être analysées à l’aide de la bibliothèque ijson. Pour les charges utiles NDJSON petites à modérées, il est recommandé d’utiliser le JSON stdlib. Outre ijson et stdlib JSON, il existe une bibliothèque NDJSON dédiée. Vous trouverez ci-dessous des extraits de code montrant chaque approche.
NDJSON utilisant stdlib JSON et ijson
Comme NDJSON n’est pas délimité par des virgules, il n’est pas admissible à un chargement groupé, car stdlib json s’attend à voir une liste de dicts. En d’autres termes, l’analyseur de stdlib JSON recherche un seul élément JSON valide, mais reçoit à la place plusieurs éléments JSON dans le fichier de charge utile. Par conséquent, le fichier doit être analysé de manière itérative, ligne par ligne, et envoyé à la fonction appelante pour un traitement ultérieur.
import json
ndjson_payload = """{"id": "A13434", "name": "Ella"}
{"id": "A13455", "name": "Charmont"}
{"id": "B32434", "name": "Areida"}"""
#Writing NDJSON file
with open("json_lib.ndjson", "w", encoding="utf-8") as fh:
for line in ndjson_payload.splitlines(): #Split string into JSON obj
fh.write(line.strip() + "\n") #Write each JSON object as its line
#Reading NDJSON file using json.loads
with open("json_lib.ndjson", "r", encoding="utf-8") as fh:
for line in fh:
if line.strip(): #Remove new lines
item= json.loads(line) #Deserialize
print(item) #or send it to the caller function
Avec ijson, l’analyse se fait comme indiqué ci-dessous. Avec le JSON standard, nous n’avons qu’un seul élément racine, qui est soit un dictionnaire s’il s’agit d’un seul JSON, soit un tableau s’il s’agit d’une liste de dicts. Mais avec NDJSON, chaque ligne est son propre élément racine. L’argument « » dans ijson.items() indique à l’analyseur ijson d’examiner chaque élément racine. Les arguments « » et multiple_values=True permettent à l’analyseur ijson de savoir qu’il existe plusieurs éléments racine JSON dans le fichier et de récupérer une ligne (chaque JSON) à la fois.
import ijson
ndjson_payload = """{"id": "A13434", "name": "Ella"}
{"id": "A13455", "name": "Charmont"}
{"id": "B32434", "name": "Areida"}"""
#Writing the payload to a file to be processed by ijson
with open("ijson_lib.ndjson", "w", encoding="utf-8") as fh:
fh.write(ndjson_payload)
with open("ijson_lib.ndjson", "r", encoding="utf-8") as fh:
for item in ijson.items(fh, "", multiple_values=True):
print(item)
Enfin, nous avons la bibliothèque dédiée NDJSON. Il convertit essentiellement le format NDJSON en JSON standard.
import ndjson
ndjson_payload = """{"id": "A13434", "name": "Ella"}
{"id": "A13455", "name": "Charmont"}
{"id": "B32434", "name": "Areida"}"""
#writing the payload to a file to be processed by ijson
with open("ndjson_lib.ndjson", "w", encoding="utf-8") as fh:
fh.write(ndjson_payload)
with open("ndjson_lib.ndjson", "r", encoding="utf-8") as fh:
ndjson_data = ndjson.load(fh) #returns a list of dicts
Comme vous l’avez vu, les formats de fichiers NDJSON peuvent généralement être analysés à l’aide de stdlib json et ijson. Pour les très grosses charges utiles, ijson est le meilleur choix car il est économe en mémoire. Mais si vous cherchez à générer des charges utiles NDJSON à partir d’autres objets Python, la bibliothèque NDJSON est le choix idéal. En effet, la fonction ndjson.dumps() convertit automatiquement les objets python au format NDJSON sans avoir à parcourir ces structures de données.
Maintenant que nous avons exploré NDJSON, revenons à l’analyse comparative des bibliothèques stdlib json, ujson et orjson.
La raison pour laquelle IJSON n’est pas pris en compte pour l’analyse comparative
« ijson » étant un analyseur de streaming, il est très différent des analyseurs de masse que nous avons examinés. Si nous comparions ijson avec ces analyseurs en masse, nous comparerions des pommes avec des oranges. Même si nous comparions ijson aux autres analyseurs, nous aurions la fausse impression qu’ijson est le plus lent, alors qu’en fait il sert un objectif complètement différent. ijson est optimisé pour l’efficacité de la mémoire et a donc un débit inférieur à celui des analyseurs en masse.
Génération d’une charge utile JSON synthétique à des fins d’analyse comparative
Nous générons une grande charge utile JSON synthétique contenant 1 million d’enregistrements, en utilisant la bibliothèque « mimesis ». Ces données seront utilisées pour comparer les bibliothèques. Le code ci-dessous peut être utilisé pour créer la charge utile pour cette analyse comparative, si vous souhaitez la reproduire. Le fichier généré aurait une taille comprise entre 100 Mo et 150 Mo, ce qui, je pense, est suffisamment volumineux pour effectuer des tests d’analyse comparative.
from mimesis import Person, Address
import json
person_name = Person("en")
complete_address = Address("en")
#streaming to a file
with open("large_payload.json", "w") as fh:
fh.write("[") #JSON array
for i in range(1_000_000):
payload = {
"id": person_name.identifier(),
"name": person_name.full_name(),
"email": person_name.email(),
"address": {
"street": complete_address.street_name(),
"city": complete_address.city(),
"postal_code": complete_address.postal_code()
}
}
json.dump(payload, fh)
if i < 999_999: #To prevent a comma at the last entry
fh.write(",")
fh.write("]") #end JSON array
Vous trouverez ci-dessous un exemple de ce à quoi ressembleraient les données générées. Comme vous pouvez le constater, les champs d’adresse sont imbriqués pour garantir que le JSON n’est pas seulement de grande taille, mais qu’il représente également des JSON hiérarchiques réels.
[
{
"id": "8177",
"name": "Willia Hays",
"email": "[email protected]",
"address": {
"street": "Emerald Cove",
"city": "Crown Point",
"postal_code": "58293"
}
},
{
"id": "5931",
"name": "Quinn Greer",
"email": "[email protected]",
"address": {
"street": "Ohlone",
"city": "Bridgeport",
"postal_code": "92982"
}
}
]
Commençons par l’analyse comparative.
Conditions préalables à l’analyse comparative
Nous utilisons la fonction read() pour stocker le fichier JSON sous forme de chaîne. Nous utilisons ensuite la fonction loads() dans chacune des bibliothèques (json, ujson et orjson) pour désérialiser la chaîne JSON en un objet Python. Tout d’abord, nous créons l’objet payload_str à partir du texte JSON brut.
with open("large_payload1.json", "r") as fh:
payload_str = fh.read() #raw JSON text
Nous créons ensuite une fonction de benchmarking avec deux arguments. Le premier argument est la fonction testée. Dans ce cas, il s’agit de la fonctionloads(). Le deuxième argument est le payload_str construit à partir du fichier ci-dessus.
def benchmark_load(func, payload_str):
start = time.perf_counter()
for _ in range(3):
func(payload_str)
end = time.perf_counter()
return end - start
Nous utilisons la fonction ci-dessus pour tester les vitesses de sérialisation et de désérialisation.
Analyse comparative de la vitesse de désérialisation
Nous chargeons les trois bibliothèques testées. Nous exécutons ensuite la fonction benchmark_load() sur la fonctionloads() de chacune de ces bibliothèques.
import json, ujson, orjson, time
results = {
"json.loads": benchmark_load(json.loads, payload_str),
"ujson.loads": benchmark_load(ujson.loads, payload_str),
"orjson.loads": benchmark_load(orjson.loads, payload_str),
}
for lib, t in results.items():
print(f"{lib}: {t:.4f} seconds")
Comme nous pouvons le voir, orjson a pris le moins de temps pour la désérialisation.

Analyse comparative de la vitesse de sérialisation
Ensuite, nous testons la vitesse de sérialisation de ces bibliothèques.
import json
import ujson
import orjson
import time
results = {
"json.dumps": benchmark("json", json.dumps, payload_str),
"ujson.dumps": benchmark("ujson", ujson.dumps, payload_str),
"orjson.dumps": benchmark("orjson", orjson.dumps, payload_str),
}
for lib, t in results.items():
print(f"{lib}: {t:.4f} seconds")
En comparant les temps d’exécution, nous constatons qu’orjson prend le moins de temps pour sérialiser les objets Python en un objet JSON.

Choisir la meilleure bibliothèque JSON pour votre flux de travail

Astuces pour le presse-papiers et le flux de travail pour JSON
Supposons que vous souhaitiez afficher votre JSON dans un éditeur de texte tel que Notepad++ ou partager un extrait (d’une charge utile volumineuse) sur Slack avec un coéquipier. Vous rencontrerez rapidement des plantages du presse-papiers ou de l’éditeur de texte/IDE. Dans de telles situations, on pourrait utiliser Pyperclip ou Tkinter. Pyperclip fonctionne bien pour les charges utiles inférieures à 50 Mo, tandis que Tkinter fonctionne bien pour les charges utiles de taille moyenne. Pour les charges utiles volumineuses, vous pouvez écrire le JSON dans un fichier pour afficher les données.
Conclusion
JSON peut sembler simple, mais plus la charge utile est importante et plus elle est imbriquée, plus ces charges utiles peuvent rapidement se transformer en goulot d’étranglement en termes de performances. Cet article visait à mettre en évidence comment chaque bibliothèque d’analyse Python relève ce défi. Lors de la sélection des bibliothèques d’analyse JSON, la vitesse et le débit ne sont pas toujours les principaux critères. C’est le flux de travail qui détermine si le débit, l’efficacité de la mémoire ou l’évolutivité à long terme sont nécessaires pour analyser les charges utiles. En bref, l’analyse JSON ne devrait pas être une approche universelle.



