Cédric Temple

Administrateur système en logiciels libres

JQ, outil de parsing et d'analyse de JSON

2018-11-04 » notes pour plus tard

Notes sur JQ

Introduction

C’est la mode des micro-services et des API. Le XML tend à disparaître, remplacé par le format JSON pour les données et le format YAML pour les fichiers de configuration. On ne va pas faire une analyse ici de ces changements, quand bien même ce serait très intéressant, nous allons juste prendre en compte ce changement. L’un des meilleurs outils pour traiter le JSON est JQ car :

  1. il est utilisable en ligne de commande
  2. il rend lisible pour un humain un JSON écrit pour une machine ; il fait de la coloration syntaxique
  3. il permet de filtrer les données (je n’affiche que la partie qui m’intéresse)
  4. il permet de modifier le contenu (par un calcul par exemple)
  5. … (et c’est ça le plus important).

Il est tellement puissant que ce serait une faute pour toute personne (DEV, OPS, adminSys) utilisant des API de ne pas le maîtriser.

Dans les exemples, j’utilise l’API Geo fournie par api.gouv.fr accessible et documentée par le lien https://geo.api.gouv.fr/. Je l’utilise à titre de démonstration uniquement et parce que son utilisation est libre sans conditions.

Remarque : la mise en valeur (la coloration syntaxique) du JSON présenté ici n’est pas le fait de JQ mais le fait du moteur de ce blog. Cependant, les 2 sont tous aussi bien.

Utilisation de base

Ici, je récupère les informations d’une commune, par son code postal :

curl 'https://geo.api.gouv.fr/communes/?codePostal=91330' | jq '.'
[
  {
    "nom": "Yerres",
    "code": "91691",
    "codeDepartement": "91",
    "codeRegion": "11",
    "codesPostaux": [
      "91330"
    ],
    "population": 28797
  }
]

On peut se poser une question : pourquoi l’API renvoie un tableau d’éléments au lieu d’un seul élément ? C’est parce que le code postal n’est pas unique : plusieurs communes françaises peuvent avoir le même code postal. Exemple :

curl 'https://geo.api.gouv.fr/communes/?codePostal=59111' | jq '.'
[
  {
    "nom": "Bouchain",
    "code": "59092",
    "codeDepartement": "59",
    "codeRegion": "32",
    "codesPostaux": [
      "59111"
    ],
    "population": 4047
  },
  {
    "nom": "Hordain",
    "code": "59313",
    "codeDepartement": "59",
    "codeRegion": "32",
    "codesPostaux": [
      "59111"
    ],
    "population": 1450
  },
  {
    "nom": "Lieu-Saint-Amand",
    "code": "59348",
    "codeDepartement": "59",
    "codeRegion": "32",
    "codesPostaux": [
      "59111"
    ],
    "population": 1336
  },
  {
    "nom": "Wavrechain-sous-Faulx",
    "code": "59652",
    "codeDepartement": "59",
    "codeRegion": "32",
    "codesPostaux": [
      "59111"
    ],
    "population": 372
  }
]

Afficher le Nième élément d’un tableau :

# usage général, où N est le Nième élément du tableau :
... | jq '.[N]'
# exemple :
curl 'https://geo.api.gouv.fr/communes/?codePostal=59111' | jq '.[1]'
{
  "nom": "Hordain",
  "code": "59313",
  "codeDepartement": "59",
  "codeRegion": "32",
  "codesPostaux": [
    "59111"
  ],
  "population": 1450
}

Sélectionner une partie du tableau

# usage général :
... | jq '.[N:M]'
# exemple : on récupère les éléments 1 à 5 du tableau
curl 'https://geo.api.gouv.fr/departements/' 2>/dev/null | jq '.[1:5]'

Afficher une propriété d’un élément avec l’utilisation du “.” :

# usage général : on va afficher le nom de la propriété de la Nième réponse
... | jq '.[N].propriété'
# exemple : 
# ici, on affiche le nom de la commune de la première réponse :
curl 'https://geo.api.gouv.fr/communes/?codePostal=59111' | jq '.[1].nom'
"Hordain"

Itérer sur les éléments d’un tableau pour afficher un élément :

# ici, on affiche le nom de toutes les communes :
curl 'https://geo.api.gouv.fr/communes/?codePostal=59111' | jq '.[].nom'
"Bouchain"
"Hordain"
"Lieu-Saint-Amand"
"Wavrechain-sous-Faulx"

Noter la syntaxe pour parcourir le tableau : .[] Dès lors, l’opération “derrière” va être faite sur tous les éléments du tableau.

Enchaînement de commmandes

Il est possible avec JQ d’enchaîner les commandes de la même manière qu’en shell, avec le même caractère. Cela signifie qu’au lieu d’enchaîner les appels à JQ, on demande à JQ d’enchaîner lui même plusieurs commandes. L’avantage est que c’est plus efficace : un seul processus JQ est créé et traîte toutes les commandes les unes après les autres. Exemple, en reprenant le but de la commande précédente :

# ici, on affiche le nom de toutes les communes :
curl 'https://geo.api.gouv.fr/communes/?codePostal=59111' | jq '.[] | .nom'
"Bouchain"
"Hordain"
"Lieu-Saint-Amand"
"Wavrechain-sous-Faulx"

Remarquez la syntaxe suivante :

... | jq '.[] | .nom'

Attention : le pipe (caractère “|”) est dans la commande JQ ! Il ne faut pas confondre le pipe du shell (celui entre l’appel à la commande curl et l’appel à la commande JQ) et le pipe dans la commande JQ. Là, on demande à JQ :

  1. parcours tous les éléments du tableau et transforme en flux
  2. passe le flux dans un pipe
  3. pour chaque élément arrivant dans le pipe, affiche la propriété “nom”

Sélectionner une partie des propriétés d’un tableau d’éléments

Exemple : on dispose d’un tableau d’éléments, chaque élément contenant plusieurs propriétés mais on souhaite n’afficher que quelques proriétés.

curl 'https://geo.api.gouv.fr/departements/' | jq '.[1:5] | .[] | {nom,codeRegion}'
{
  "nom": "Aisne",
  "codeRegion": "32"
}
{
  "nom": "Allier",
  "codeRegion": "84"
}
{
  "nom": "Alpes-de-Haute-Provence",
  "codeRegion": "93"
}
{
  "nom": "Hautes-Alpes",
  "codeRegion": "93"
}

Transformer une séquence d’élements en tableau

Là, pas de miracle, il faut enchaîner 2 appels à JQ ! Et pour transformer la séquence d’éléments en tableau, on utilise l’option -s sur la ligne de commande.

curl 'https://geo.api.gouv.fr/departements/' | jq '.[1:5] | .[] | {nom,codeRegion}' | jq -s '.'
[
  {
    "nom": "Aisne",
    "codeRegion": "32"
  },
  {
    "nom": "Allier",
    "codeRegion": "84"
  },
  {
    "nom": "Alpes-de-Haute-Provence",
    "codeRegion": "93"
  },
  {
    "nom": "Hautes-Alpes",
    "codeRegion": "93"
  }
]

Notez bien la différence entre l’appel de la commande précédente (“Sélectionner une partie des propriétés d’un tableau d’éléments”) qui affiche une séquence d’objets et la réponse de cette commande qui affiche un tableau d’éléments. La sortie est totalement différente.

Obtenir un CSV

Pour obtenir un CSV, il faut d’abord transformer la sortie en “séquence de tableaux”. Le premier tableau est transformé en colonne : chaque élément de ce premier tableau va constituer une colonne. À chaque nouveau tableau (et seulement dans ce cas), une nouvelle ligne est créée. Cependant, comme JQ échappe le caractère doubles-quotes, on ne peut pas le faire en une seule commande. Il faut au moins 2 appels à JQ avec le 2e qui n’échappe pas les doubles-quotes (option -r). Exemple :

  1. tout d’abord, transformons la sortie en séquence de tableaux:
    curl 'https://geo.api.gouv.fr/departements/' | jq '.[1:5] | .[] | [.nom,.codeRegion]'
    
    [
      "Aisne",
      "32"
    ]
    [
      "Allier",
      "84"
    ]
    [
      "Alpes-de-Haute-Provence",
      "93"
    ]
    [
      "Hautes-Alpes",
      "93"
    ]
    
  2. maintenant, faisons appel à la transformation CSV. Rappel : on doit faire cela en 2 appels à JQ avec forcément l’option -r lors du 2e appel !
    curl 'https://geo.api.gouv.fr/departements/' | jq '.[1:5] | .[] | [.nom,.codeRegion]' | jq -r '@csv'
    
    "Aisne","32"
    "Allier","84"
    "Alpes-de-Haute-Provence","93"
    "Hautes-Alpes","93"
    

    Remarque : si l’on veut des tabulations à la place des virgules, on fait appel à @tsv en lieu et place de @csv.

Filtres

Parfois, on souhaite filtrer les données. Par exemple, on souhaite retirer certaines entrées pour se faciliter la vie. Par exemple, je ne veux afficher que les codes qui sont des entiers. En effet, on peut vouloir ne pas afficher les régions dont le code est 2A et 2B car l’on peut pas les transformer en entier pour les filtrer plus efficacement ensuite. La fonction select permet de filtrer les données. Cependant, il n’est pas possible de faire des relations logiques dans le filtre. On doit donc enchaîner les appels à select :

curl 'https://geo.api.gouv.fr/departements/' | jq '.[] | select(.code != "2A") | select(.code != "2B")'

Transformation de données

Parfois, on souhaite transformer des données pour mieux les filtrer. Exemple : ici, on souhaite filtrer les entrées pour n’afficher que les départements entre 59 et 62. Problème : code est une chaîne de caractères, pas un nombre. On va donc transformer cette chaîne en nombre. Auparavant, on aura retirer les 2 régions qui ont un code en “2A” et “2B” et qui sont bien des chaînes de caractères :

# on reprend tel que nom et .codeRegion (donc sans mettre de "." devant
# par contre, on reconstruit code en transformant les données : il faut donc préciser le nom et le transformer en nombre
# ensuite on filtre sur code qui doit être entre 59 et 62
curl 'https://geo.api.gouv.fr/departements/' | jq '.[] | select(.code != "2A") | select(.code != "2B") | {nom,"code": .code | tonumber,codeRegion} | select(.code>=59) | select (.code<=62)'
{
  "nom": "Nord",
  "code": 59,
  "codeRegion": "32"
}
{
  "nom": "Oise",
  "code": 60,
  "codeRegion": "32"
}
{
  "nom": "Orne",
  "code": 61,
  "codeRegion": "28"
}
{
  "nom": "Pas-de-Calais",
  "code": 62,
  "codeRegion": "32"
}

Voilà, il y aurait encore beaucoup à dire sur JQ mais la documentation officielle est pas trop mal faite, elle devrait vous aider.

© Cédric Temple - - Powered by Jekyll.