jq
Guy Lapalme, RALI-DIRO, Université de Montréal, décembre 2024
[English version of this document]
jq
Est un outil de ligne de commande qui permet de manipuler des fichiers .json
ou .jsonl
pour les valider, les formater, en extraire des informations ou les transformer. On peut le considérer comme l'équivalent de sed
ou awk
pour les fichiers de texte dont les informations des lignes sont séparées par des tabulations. Le système est rapide et efficace pour traiter de gros fichiers, y compris en mode streaming.
Si le fichier d'entrée contient plusieurs objets JSON (c'est typiquement le cas avec les fichiers .jsonl
), le programme est appliqué sur chaque objet et chaque résultat est produit séparément. Une option du programme (--slurp
ou -s
) permet de considérer tous les objets du fichier comme une liste d'objets. On peut donc y effectuer des opérations les combinant, par exemple un tri ou l'élimination de doublons.
La documentation officielle propose un tutoriel présentant quelques exemples ainsi qu’un manuel qui énumère toutes les fonctions illustrées par un court exemple, trop souvent simpliste. Cependant, le manuel n’explique pas comment enchaîner ces fonctions pour effectuer des tâches intéressantes.
La syntaxe des expressions jq
est déroutante au début, car elle s’appuie sur la composition de filtres (point-free programming) plutôt que sur la notation fonctionnelle habituelle. jq
est un langage de programmation doté de structures de contrôle, de variables, de fonctions et même de modules. Or, comme toutes mes applications jq
se sont limitées à une seule expression sur la ligne de commande, ce document se concentrera sur cet aspect du langage. Ces expressions permettent tout de même d'effectuer des tâches utiles.
Il existe plusieurs tutoriels pour jq
sur le web, dont celui-ci qui est excellent, mais maintenant truffé de publicités. Malheureusement, ces guides ne se concentrent pas suffisamment sur les aspects les plus utiles pour moi, à savoir l'extraction et la conversion de données JSON.
Ce document présente les concepts utilisés dans la programmation en jq
et une liste des fonctions qui m'ont été les plus utiles. Ensuite, je donne quelques exemples d'expressions appliquées sur deux organisations de fichier: un avec une seule structure JSON et un autre en JSONL qui comprend plusieurs structures JSON. Je termine avec une liste de références que j'ai trouvé intéressantes parmi les centaines qu'on trouve, mais qui reprennent trop souvent les mêmes exemples simples.
Ce texte me servira de rappel, bien que je sois moins susceptible de m'en servir après l’avoir écrit. J'espère néanmoins qu'il sera bénéfique pour d'autres.
Une expression jq
est un filtre qui transforme une entrée en une ou plusieurs sorties. Tous les éléments du langage jq
sont des filtres, même un nombre ou une chaîne qui peuvent être considérés comme des filtres qui ignorent leur entrée et produisent toujours la même valeur.
Une fois installé, jq
est appelé de la ligne de commande :
jq
[options] '
filtre'
fichier, le filtre étant entouré d'apostrophes simples pour éviter qu'il soit interprété par le shell avant d'être passé à jq
.
Un filtre est appliqué à un flot de données JSON. jq
analyse ce flot comme une séquence de valeurs JSON, chaque valeur étant séparée d’un espace. Chaque valeur est ensuite validée, puis transmise une à une. Le filtre renvoie ses résultats sur stdout, sous forme d’une séquence de valeurs JSON séparées par un retour chariot. Par défaut, la sortie JSON est indentée pour en faire ressortir la structure (pretty-print). Cette indentation n'est pas appliquée si l'option -c
ou --compact-output
est spécifiée lors de l'appel de jq
.
Rappelons qu'outre les valeurs primitives (nombre, chaîne de caractères, booléen, null
), JSON fournit deux structures de données:
objet : ensemble de couples clé:
valeur séparés par des virgules à l'intérieur d'accolades, les clés doivent être uniques à l'intérieur d'un même objet; il n'y a pas d'ordre prédéfini entre les couples.
tableau : valeurs séparées par des virgules à l'intérieur de crochets; on peut accéder aux éléments par indexage à partir de 0.
Voici ma sélection personnelle de fonctions parmi la centaine décrites dans le manuel.
.
: identité: retourne l'entrée telle quelle: ce filtre peut sembler inutile, mais si on l'applique sur un fichier JSON, il le valide et en sort une version indentée.
f1 | f2
: composition de filtres: redirige la sortie du filtre f1 vers l'entrée de f2.
f1 , f2
: concaténation de filtres: la sortie de f2 est ajoutée à la fin de la sortie de f1.
.["clé"]
: indice d'objet : appliqué à un objet, retourne la valeur associée à clé si elle figure dans l'objet, sinon null
. Si clé est un identificateur simple, on peut simplement écrire .clé
.
.[nombre]
: indice de tableau : appliqué à un tableau, retourne la valeur à la position indiquée par le nombre, nombre peut être négatif pour indicer à partir de la fin. On peut aussi utiliser des tranches de tableau n1:n2
. Si les indices sont des nombres entre virgules, on obtient des valeurs séparées; c'est en fait une application de la concaténation de filtres.
.[]
: génération de valeurs : appliqué à un objet, retourne toutes les valeurs de l'objet (sans les clés) comme résultats séparés; .clé | .[]
peut s'écrire aussi .clé[]
.
..
: descente récursive : produit toutes les valeurs imbriquées, souvent combiné avec |.clé?
pour récupérer les valeurs associées à clé
à tous les niveaux imbriqués d'objets.
{…}
: construction d'un objet : réunit les éléments du contenu qui doivent être de la forme clé:
valeur en un seul objet. clé est une chaîne de caractère, mais peut aussi être une expression (p.ex. un filtre) si on la met entre parenthèses.
[…]
: construction de tableau : réunit le contenu en un tableau
(…)
: groupement : groupe une sous-expression selon les conventions habituelles
select(f)
: sélection : ne garder de l'entrée que les éléments pour lesquels f
retourne true
strings
: ne garder de l'entrée que les valeurs qui sont des chaînes; il y a l'équivalent pour d'autres types comme nulls
, numbers
ou values
(non-null
).
keys
: clés d'un objet : retourne un tableau avec les clés de l'objet en entrée. Le tableau des valeurs d'un objet est obtenu avec .[]
(voir ci-haut).
to_entries
: couples clé-valeur d'un objet : transforme un objet {cléi:valeuri}
en un tableau d'objets de la forme {"key":cléi:"value":valeuri}
del(clé)
: suppression: enlever la clé et la valeur associée dans un objet
f1 + f2
: addition: dépend des types: nombres: addition arithmétique; chaîne ou tableau: concaténation; objets: fusion.
f1 - f2
: soustraction: dépend des types: nombres: soustraction arithmétique; tableau: enlève de f1 les éléments de f2.
reduce résultats as $e (init; combinaison impliquant $e)
: combine tous les éléments ($e) d'une série de résultats dans un seul objet initialisé avec init.
==
!=
>
>=
<
<=
: comparaison selon les conventions habituelles
and
or
not
: combinaison de booléens
test(regex)
: vérification avec une expression régulière
startswith(str)
, endswith(str)
: vérification : est-ce que le début ou la fin de chaîne correspond à str.
split(str)
, join(str)
: conversion chaîne <=>tableau : tableau des sous-chaînes entre les str, créer une chaîne à partir d'un tableau en ajoutant str entre chaque élément.
Conseil: essayez ces exemples avec jqTerm ou jqplay qui permettent de les faire exécuter depuis une page web et d'afficher les résultats immédiatement.
.json
[source]La structure JSON suivante représente un extrait d'un dictionnaire anglais dont les clés sont des mots. À chaque mot est associé un objet qui décrit des informations selon sa catégorie (A
: adjectif, N
: nom ou V
: verbe). À chaque catégorie est associée une table de flexions (tab
) et, pour certains, si le mot est courant (basic
). Un nom peut être comptable (yes
), non-comptable (no
) ou les deux (both
) .
1{"comfortable":{"A":{"tab":"a1"},
2 "basic":true},
3 "comic":{"A":{"tab":"a1"},
4 "N":{"tab":"n1",
5 "cnt":"yes"}},
6 "comical":{"A":{"tab":"a1"}},
7 "comically":{"Adv":{"tab":"b1"}},
8 "coming":{"A":{"tab":"a1"},
9 "N":{"tab":"n1",
10 "cnt":"yes"},
11 "basic":true},
12 "comity":{"N":{"tab":"n5",
13 "cnt":"no"}},
14 "comma":{"N":{"tab":"n1",
15 "cnt":"yes"}},
16 "command":{"N":{"tab":"n1",
17 "cnt":"both"},
18 "V":{"tab":"v1"},
19 "basic":true},
20 "commandant":{"N":{"tab":"n1",
21 "cnt":"yes"}},
22 "commandment":{"N":{"tab":"n1",
23 "cnt":"yes"}},
24 "commando":{"N":{"tab":"n2",
25 "cnt":"yes"}},
26 "commemorate":{"V":{"tab":"v3"}},
27 "commemoration":{"N":{"tab":"n1",
28 "cnt":"both"}},
29 "commemorative":{"A":{"tab":"a1"}}
30}
keys
: tableau de tous les mots ⇒
61[
2 "comfortable",
3 "comic",
4
5 "commemorative"
6]
to_entries[]|select(.value.basic)|.key
: liste des mots avec l'indicateur basic
. Résultat sur plusieurs lignes ⇒
31"comfortable"
2"coming"
3"command"
select
devant s'appliquer sur une série de valeurs, il faut d'abord transformer l'entrée en une suite d'entrées {"key":cléi:"value":valeuri}
; on ne garde que les entrées dont la partie value
comprend la clé basic
; le dernier filtre n'affiche que le champ key
.
to_entries[]|{(.key):.value|del(.basic)}
: liste de structures JSON dont la clé est le mot, mais où on a enlevé le champ basic
. ⇒
191{
2 "comfortable": {
3 "A": {
4 "tab": "a1"
5 }
6 }
7}
8{
9 "comic": {
10 "A": {
11 "tab": "a1"
12 },
13 "N": {
14 "tab": "n1",
15 "cnt": "yes"
16 }
17 }
18}
19 .
Pour chaque entrée de l'objet initial, on crée un objet dont la clé est le champ key
de l'entrée et la valeur est le champ value
mais duquel on enlève le champ basic
, s'il existe.
[ .. | .tab? | strings ] | unique
donne la liste des valeurs, sans répétition, du champ tab
utilisées dans le fichier. ⇒ (affiché avec l'option -c
)
11["a1","n1","n2","n5","v1","v3"]
On parcourt tous les niveaux de l'objet, si un champ tab
est présent, on retourne sa valeur. strings
ne conserve que les valeurs chaînes: on élimine ainsi les valeurs null
ou encore un objet associé à la clé "tab"
à un certain niveau. unique
élimine les doublons, mais comme il ne s'applique qu'à un tableau, les filtres précédents sont entourés de crochets pour créer un tableau avec le résultat de leur exécution.
reduce (.. | .tab? | strings) as $w ({}; .[$w]+=1)
retourne un objet dont les clés sont les valeurs différentes de la clé tab
dans le JSON source et la clé le nombre de fois que cette clé y apparaît.
91{
2 "a1": 5,
3 "n1": 7,
4 "b1": 1,
5 "n5": 1,
6 "v1": 1,
7 "n2": 1,
8 "v3": 1
9}
Combinaison par reduce des valeurs produites par l'exemple précédent dans un seul objet initialement vide. Pour chaque résultat $w on incrémente de 1 la valeur associée. Si elle n'existe pas, elle est créée et initialisée à 0 avant d'être incrémentée.
.jsonl
[source]Chaque ligne d'un fichier JSON Lines est un objet JSON décrivant un mot.
Note: Ce fichier aurait pu être créé à partir du précédent avec l'expression to_entries[] | {word:.key}+.value
.
141{"word":"comfortable","A":{"tab":"a1"},"basic":true}
2{"word":"comic","A":{"tab":"a1"},"N":{"tab":"n1","cnt":"yes"}}
3{"word":"comical","A":{"tab":"a1"}}
4{"word":"comically","Adv":{"tab":"b1"}}
5{"word":"coming","A":{"tab":"a1"},"N":{"tab":"n1","cnt":"yes"},"basic":true}
6{"word":"comity","N":{"tab":"n5","cnt":"no"}}
7{"word":"comma","N":{"tab":"n1","cnt":"yes"}}
8{"word":"command","N":{"tab":"n1","cnt":"both"},"V":{"tab":"v1"},"basic":true}
9{"word":"commandant","N":{"tab":"n1","cnt":"yes"}}
10{"word":"commandment","N":{"tab":"n1","cnt":"yes"}}
11{"word":"commando","N":{"tab":"n2","cnt":"yes"}}
12{"word":"commemorate","V":{"tab":"v3"}}
13{"word":"commemoration","N":{"tab":"n1","cnt":"both"}}
14{"word":"commemorative","A":{"tab":"a1"}}
Comme il y a plusieurs objets JSON dans le même fichier, l'expression jq
est appliquée à chacun et chaque résultat est concaténé sur la sortie. Un objet JSON peut chevaucher une ou plusieurs lignes pourvu que sa structure soit correcte.
.word
: tous les mots ⇒
51"comfortable"
2"comic"
3"comical"
4"comically"
5
select(.basic)|.word
: les mots avec l'indicateur basic
⇒
31"comfortable"
2"coming"
3"command"
{(.word):.|del(.basic)}
: les structures JSON dont la clé est le mot, mais où on a enlevé le champ basic
. ⇒ sortie avec l'option (-c
ou --compact-output
) qui n'applique pas d'indentation.
31{"comfortable":{"word":"comfortable","A":{"tab":"a1"}}}
2{"comic":{"word":"comic","A":{"tab":"a1"},"N":{"tab":"n1","cnt":"yes"}}}
3
.. | .tab? | strings
: toutes les valeurs du champ tab
51"a1"
2"a1"
3"n1"
4"a1"
5
Notons que, dans cette sortie, toutes les valeurs sont indépendantes les unes des autres, ayant été produites par l'application de l'expression à chaque objet JSON du fichier. Pour ne garder que les valeurs uniques, il faut que le fichier JSON soit considéré comme un seul objet, soit une liste composée des objets JSON du fichier, ce qu'on peut obtenir en spécifiant l'option --slurp
(-s
).
[.[] | .. | .tab? | strings ] | unique
appelé avec -s
, ceci produit la liste des valeurs, sans répétition, du champ tab
utilisées dans le fichier. ⇒ (affiché avec l'option -c
)
11["a1","n1","n2","n5","v1","v3"]
reduce (.. | .tab? | strings) as $w ({}; .[$w]+=1)
retourne un objet dont les clés sont les valeurs différentes de la clé tab dans le JSON et la clé le nombre de fois que cette clé y apparaît. Cette expression combinant plusieurs valeurs doit être appelée avec l'option -s
.
91{
2 "a1": 5,
3 "n1": 7,
4 "b1": 1,
5 "n5": 1,
6 "v1": 1,
7 "n2": 1,
8 "v3": 1
9}
Combinaison par reduce des valeurs produites par l'exemple précédent dans un seul objet initialement vide. Pour chaque résultat $w on incrémente de 1 la valeur associée. Si elle n'existe pas, elle est créée et initialisée à 0 avant d'être incrémentée.
Note: Pour les curieux: avec l'option -s
, l'expression
11reduce .[] as $elem ({}; .+ {($elem|.word) : $elem|del(.word) })
appliquée sur le fichier .jsonl
recrée la structure JSON de la section précédente en partant d'un objet vide de la façon suivante: pour chaque élément du tableau, on fusionne un objet ayant pour clé la valeur de la clé word
et, pour valeur, toutes les propriétés de l'élément sauf le champ word
.
Exploring jq : Conseils de haut niveau pour l'utilisation de jq
en conjonction avec d'autres systèmes.
jq Language Description: définition plus formelle du langage jq
avec plusieurs explications intéressantes.
The Ultimate Interactive JQ Guide : 25 exemples interactifs très bien expliqués
Exemples de programmes jq
plus complexes:
Tests en ligne