La comparaison de structures JSON complexes est une tâche courante, mais elle devient problématique lorsque les fichiers atteignent des volumes importants (plusieurs centaines de milliers de lignes). Si des outils standards ou des bibliothèques tierces existent, leur consommation de ressources et leur temps d'exécution peuvent s'avérer prohibitifs sur des fichiers de plus de 10 Mo.
Analyse des limites des bibliothèques tierces
L'utilisation de bibliothèques prêtes à l'emploi, comme jsondiff en Python, offre une simplicité d'implémentation mais souffre souvent d'un manque d'optimisation pour les flux de données masssifs. Lors de tests sur un fichier de 400 000 lignes (environ 11 Mo), une approche générique peut prendre plus de 25 minutes pour identifier une simple modification de valeur. Pour pallier cela, une implémentation basée uniquement sur les bibliothèques standards permet de mieux contrôler la récursivité et la gestion de la mémoire.
Implémentation optimisée en Python
Cette approche repose sur une fonction récursive qui parcourt les dictionnaires et les listes. En utilisant l'union des clés et une comparaison par index, on réduit darstiquement la complexité algorithmique perçue.
import json
import os
from dataclasses import dataclass
from typing import Any, List, Optional
@dataclass
class JsonDelta:
chemin: str
action: str # 'ajouté', 'supprimé', 'modifié'
valeur_initiale: Any = None
valeur_finale: Any = None
def detecter_ecarts(chemin_base: str, data_gauche: Any, data_droite: Any) -> List[JsonDelta]:
ecarts = []
# Cas des dictionnaires (objets JSON)
if isinstance(data_gauche, dict) and isinstance(data_droite, dict):
toutes_cles = set(data_gauche.keys()) | set(data_droite.keys())
for cle in toutes_cles:
nouveau_chemin = f"{chemin_base}.{cle}" if chemin_base else cle
if cle in data_gauche and cle not in data_droite:
ecarts.append(JsonDelta(nouveau_chemin, 'supprimé', valeur_initiale=data_gauche[cle]))
elif cle not in data_gauche and cle in data_droite:
ecarts.append(JsonDelta(nouveau_chemin, 'ajouté', valeur_finale=data_droite[cle]))
else:
ecarts.extend(detecter_ecarts(nouveau_chemin, data_gauche[cle], data_droite[cle]))
# Cas des listes (tableaux JSON)
elif isinstance(data_gauche, list) and isinstance(data_droite, list):
taille_max = max(len(data_gauche), len(data_droite))
for i in range(taille_max):
index_path = f"{chemin_base}[{i}]"
if i < len(data_gauche) and i >= len(data_droite):
ecarts.append(JsonDelta(index_path, 'supprimé', valeur_initiale=data_gauche[i]))
elif i >= len(data_gauche) and i < len(data_droite):
ecarts.append(JsonDelta(index_path, 'ajouté', valeur_finale=data_droite[i]))
else:
ecarts.extend(detecter_ecarts(index_path, data_gauche[i], data_droite[i]))
# Comparaison des valeurs scalaires
else:
if data_gauche != data_droite:
ecarts.append(JsonDelta(chemin_base, 'modifié', data_gauche, data_droite))
return ecarts
def charger_fichier(nom_fichier: str):
with open(nom_fichier, 'r', encoding='utf-8') as f:
return json.load(f)
if __name__ == "__main__":
original = charger_fichier("source.json")
cible = charger_fichier("destination.json")
resultats = detecter_ecarts("", original, cible)
for res in resultats:
print(f"[{res.action.upper()}] {res.chemin}: {res.valeur_initiale} -> {res.valeur_finale}")
Avec cette méthode, le temps de traitement chute à moins de 0,5 seconde pour le même volume de données, démontrant l'efficacité d'un parcours récursif simple sans surcharge de méta-données.
Implémentation haute performance avec Go
Go est particulièrement efficace pour ce type de tâche grâce à sa gestion typée des interfaces et sa rapidité de traitement des fichiers IO. L'utilisation de map[string]any permet de naviguer dynamiquement dans la structure JSON.
package main
import (
"encoding/json"
"fmt"
"os"
)
type Ecart struct {
Chemin string
Type string
Avant interface{}
Apres interface{}
}
func comparerStructures(chemin string, gauche, droite interface{}) []Ecart {
var modifications []Ecart
switch g := gauche.(type) {
case map[string]interface{}:
d, ok := droite.(map[string]interface{})
if !ok {
return []Ecart{{Chemin: chemin, Type: "MODIFIE", Avant: gauche, Apres: droite}}
}
// Vérification des clés de gauche
for k, vG := range g {
sousChemin := k
if chemin != "" {
sousChemin = chemin + "." + k
}
if vD, existe := d[k]; !existe {
modifications = append(modifications, Ecart{Chemin: sousChemin, Type: "SUPPRIME", Avant: vG})
} else {
modifications = append(modifications, comparerStructures(sousChemin, vG, vD)...)
}
}
// Vérification des ajouts à droite
for k, vD := range d {
if _, existe := g[k]; !existe {
sousChemin := k
if chemin != "" {
sousChemin = chemin + "." + k
}
modifications = append(modifications, Ecart{Chemin: sousChemin, Type: "AJOUTE", Apres: vD})
}
}
case []interface{}:
d, ok := droite.([]interface{})
if !ok {
return []Ecart{{Chemin: chemin, Type: "MODIFIE", Avant: gauche, Apres: droite}}
}
max := len(g)
if len(d) > max {
max = len(d)
}
for i := 0; i < max; i++ {
p := fmt.Sprintf("%s[%d]", chemin, i)
if i >= len(g) {
modifications = append(modifications, Ecart{Chemin: p, Type: "AJOUTE", Apres: d[i]})
} else if i >= len(d) {
modifications = append(modifications, Ecart{Chemin: p, Type: "SUPPRIME", Avant: g[i]})
} else {
modifications = append(modifications, comparerStructures(p, g[i], d[i])...)
}
}
default:
if gauche != droite {
modifications = append(modifications, Ecart{Chemin: chemin, Type: "MODIFIE", Avant: gauche, Apres: droite})
}
}
return modifications
}
func main() {
f1, _ := os.ReadFile("source.json")
f2, _ := os.ReadFile("destination.json")
var data1, data2 interface{}
json.Unmarshal(f1, &data1)
json.Unmarshal(f2, &data2)
diffs := comparerStructures("", data1, data2)
fmt.Printf("Nombre d'écarts trouvés : %d\n", len(diffs))
for _, d := range diffs {
fmt.Printf("%s: %s (%v -> %v)\n", d.Type, d.Chemin, d.Avant, d.Apres)
}
}
Comparaison des résultats
Sur un environnement de test standard, les performances observées sont les suivantes :
- Python (jsondiff) : ~1570 secondes - Consommation mémoire élevée.
- Python (Standard Lib) : ~0.46 secondes - Consommation mémoire modérée.
- Go : ~0.29 secondes - Consommation mémoire optimisée.
L'implémentation en Go offre le meilleur temps d'exécution, mais l'approche Python avec la bibliothèque standard reste extrêmement compétitive et plus simple à intégrer dans des scripts de maintenance rapide.