4859 links
  • GuiGui's Show

  • Home
  • Login
  • RSS Feed
  • Tag cloud
  • Picture wall
  • Daily
Links per page: 20 50 100
page 1 / 1
  • Manipuler du JSON avec le logiciel jq

    jq (paquet logiciel du même nom) est un outil en ligne de commande pour manipuler du JSON (quelle idée !).

    Il m'apparaît que le meilleur moyen d'apprendre à utiliser jq, c'est de pratiquer. C'est ce que nous allons faire dans ce shaarli.

    Fichiers utilisés dans les exemples ci-dessous : cables.json et cablesDefs.json.



    L'usage le plus simple de jq est de rendre lisible (indentation, coloration syntaxique, etc.) un bout de JSON qui ne l'est pas (on dit aussi prettifyer, forrmatter) : jq '.' cables.json.

    • Note : « . » est l'identité. Elle désigne l'élément / l'objet courant. Au début, elle pointe sur l'objet racine du fichier JSON, mais ça peut évoluer : lors d'une itération dans un tableau, par exemple, l'identité pointera sur l'élément actuellement parcouru.



    Afficher la valeur / le contenu de l'attribut « features » de l'entité / objet racine : jq '.features' cables.json.

    • Comme la valeur de cet attribut est un tableau, tout son contenu est affiché. Si l'on avait afficher la valeur de l'attribut « type » de l'objet racine avec jq '.type' cables.json, le retour serait une unique chaîne de caractères.



    Afficher uniquement l'attribut « slug » de l'attribut « properties » de chaque élément du tableau features « features » : jq '.features[].properties.slug' cables.json.

    • Sorte de grep.



    Afficher l'attribut « name » de l'attribut « properties » de chaque élément du tableau « features ». Afficher « n/a » (not available) si l'attribut n'existe pas ou a « null » comme valeur : jq '.features[].properties.name // "n/a"'.

    • Il s'agit de l'opérateur de remplacement de jq.



    Notons que jq propose des paramètres intéressants.

    • « -r » (raw) permet de ne pas afficher les guillemets autour d'une chaîne de caractères ;

    • « -a » (ascii) remplace les caractères non-ASCII par leur séquence échappée, ce qui est pratique pour identifier un problème lié à l'encodage.



    Ce que nous avons mis entre apostrophes dans les exemples précédents se nomme un filtre. Il est possible d'enchaîner les filtres. Pour ce faire, deux opérateurs existent :

    • « , » exécute les deux filtres et concatène leur résultat (comme « ; » dans un shell). Afficher le contenu de l'attribut « slug » de chaque élément du tableau « features » puis afficher le contenu de leur attribut « color » : jq '.features[].properties.slug , .features[].properties.color' cables.json ;

    • « | » a le même rôle que dans un shell : la sortie du filtre de gauche est l'entrée du filtre de droite. Travailler sur le slug de chaque élément du tableau : jq '.features[].properties.slug | .' cables.json.



    Tout peut être stocké dans une variable à tout moment : jq '.features[].properties as $props | .' cables.json.

    • Plus loin dans l'enchaînement de filtres, on pourra utiliser la variable avec $props | <traitement_ici> ou $props.<nom_attribut> (exemple : « $props.color »).

    • Forcément, si le résultat d'un filtre est stocké dans une variable, alors ce filtre enverra rien sur l'entrée d'un filtre qui lui serait chaîné. C'est le résultat du dernier filtre sans stockage dans une variable qui sera injecté dans l'entrée du filtre chaîné.



    Si un filtre retourne plusieurs résultats et qu'un ou plusieurs filtres lui sont chaînés, ces derniers filtres s'appliqueront / seront exécutés pour chaque résultat retourné par le premier filtre. Il est donc possible de travailler sur chaque élément d'un tableau. Nous le faisons depuis le début : jq '.features[].properties.slug' cables.json = jq '.features[] | .properties.slug' cables.json. Il est donc possible d'exécuter un traitement sur chaque élément du tableau, comme une sorte de grep | xargs.

    • Comme c'est confusant, je conseille vivement d'écrire tranquillement son filtre sur un seul objet à la fois avec jq '.features[0] | .properties.slug' cables.json (« [0] » demande à travailler sur le premier élément du tableau) avant de le généraliser ;

    • Si l'on fait jq '.features[] | ., c'est-à-dire « affiche-moi chaque élément du tableau « features » », on se rend compte que l'on perd l'entête (« { "type": "FeatureCollection", "features": [ ») et le pied (« ] } ») de notre fichier. Il faut comprendre qu'on les retrouvera jamais, qu'on peut jamais remonter au niveau supérieur. Ce serait comme utiliser grep essai | xargs <maCommande> et vouloir que maCommande accède à ce qui n'a pas été grep-é (retenu par grep). Si l'on tente d'enregistrer la racine dans une variable (« . as $racine ») et que l'on tente de l'afficher dans une itération (exemple : jq '. as $racine | .features[] | .properties.slug | $racine' cables.json), l'ensemble du fichier JSON sera affiché autant de fois qu'il y a d'éléments dans le tableau « features », ce qui n'est pas ce que l'on veut. Bref, n'essaye pas. On ne sort pas d'une itération, tout enchaînement de filtres après un filtre qui émet plusieurs éléments JSON sera exécuté autant de fois qu'il y a d'éléments ;

    • En revanche, on peut contenir une telle itération au sein d'une opération bornée, comme une affectation. Ainsi, tout le résultat produit par l'enchaînement des filtres de l'itération sera affecté à une variable. Nous verrons selon un peu plus loin, car il nous manque encore des connaissances.



    Recherche sur la valeur d'un attribut / afficher l'élément du tableau « features » dont la valeur de l'attribut « slug » est « adria-1 » : jq '.features[] | select(.properties.slug=="adria-1")' cables.json.

    • Afficher uniquement la valeur de l'attribut « properties.color » de cet objet ? jq '.features[] | select(.properties.slug=="adria-1") | .properties.color' cables.json ou, plus simplement : jq '.features[] | select(.properties.slug=="adria-1").properties.color' cables.json.

    • Rechercher une sous-chaîne dans la valeur d'un attribut avec une regex ? jq '.features[] | select(.properties.slug | test("adria-1")). Il est possible de positionner des drapeaux pour des recherches gourmandes (greedy), pour des recherches insensibles à la case, etc. Exemple concret d'utilisation.



    On peut mettre en forme /formater la sortie (afficher plusieurs attributs, ajouter du texte libre, etc.). Pour chaque élément du tableau « features », je veux afficher une ligne de la forme \*\*SLUG\*\*: <slug> ~ \*COLOR\*: <color>.. Les « * » sont du formatage Markdown (**gras**, *italique*). Le texte en majuscule, le caractère « ~ » et le caractère final (« . ») sont des bouts de texte arbitraires que je souhaite afficher. jq '.features[] | "**SLUG**: \(.properties.slug) ~ *COLOR*: \(.properties.color)."' cables.json.

    • Les guillemets permettent de saisir des chaînes de caractères. « \() » est l'opérateur qui indique à jq d'interpréter une expression située au milieu d'une chaîne de caractères.



    Et si je veux utiliser le contenu d'une variable shell dans un filtre ?

    • Normalement, on écrit un filtre jq entre apostrophes afin que le shell n'interprète pas la chaîne de caractères. Mais, on peut aussi l'écrire entre guillemets afin quil l'interpète. Exemple : maVariable='{ "test": "toto" }' ; jq ".features[].properties += $maVariable" cables.json. Cela signifie aussi que tu vas devoir échapper tout ce que le shell pourrait interpréter : le « $ » d'une variable interne à jq, les guillemets, etc. Pour peu que ton enchaînement de filtres jq soit un peu costaud, ça va le rendre illisible. Je déconseille ;

    • jq propose les arguments --arg <nom_variable_interne> <contenu> (le contenu sera enregistré comme une chaîne de caractères) et --argjson <nom_variable_interne> <bout_de_JSON> (attention : le bout de JSON doit avoir une syntaxe valide !). Reprise de l'exemple précédent : maVariable='{ "test": "toto" }' ; jq --argjson varInterne "$maVariable" '.features[].properties += $varInterne' cables.json.



    Et si l'on veut modifier le flux JSON ? Il existe les opérateurs « |= » et « += ». Le premier modifie la valeur d'un attribut en écrasant l'exisant. Le second ajoute un élément dans la valeur d'un attribut.

    • Je veux changer / remplacer la valeur de l'attribut « properties.color » de chaque élément du tableau « features » pour « yellow » : jq '.features[] | .properties.color |= "yellow"' cables.json ;

    • Je veux ajouter un attribut « toto » avec la valeur « titi » dans l'attribut « properties » de chaque élément du tableau « features » : jq '.features[] | .properties += { "toto": "titi" } ' cables.json ;

    • Je veux renommer l'attribut « properties.color » de chaque élément du tableau « features » en « properties.toto » tout en conservant sa valeur : jq '.features[] | .properties += { "toto": .properties.color } ' cables.json ;

    • Je veux déplacer l'attribut « properties.color » de chaque élément du tableau « features » dans un nouvel attribut « properties.toto » tout en conservant sa valeur : jq '.features[] | .properties += { "toto": { "color": .properties.color } }' cables.json.



    Normalement, si tu suis, tu me traites de tricheur : les deux dernières commandes du point précédent n'ont pas renommé / déplacé l'attribut, mais l'ont simplement copié. Pour finir le travail, jq propose la fonction native « del() » qui, comme son nom l'indique, permet de supprimer un attribut. Démo sur les deux exemples précédents : jq '.features[] | .properties += { "toto": .properties.color } | del(.properties.color)' cables.json et jq '.features[] | .properties += { "toto": { "color": .properties.color } } | del(.properties.color)' cables.json.

    • D'autres fonctions natives sont disponibles : select() qu'on a déjà vu, sub() (remplacer une portion de texte avec des expressions régulières ou non), split(), etc. On notera que la fonction gsub() (greedy sub) échoue lorsque certains caractères Unicode sont présents dans la chaîne de caractères à traiter (a priori, c'est corrigé dans la version 1.6 de jq).



    jq ne propose pas un argument (genre « -o ») permettant d'enregistrer le résultat de son traitement dans un fichier. Donc, si l'on veut enregistrer le résultat dans le fichier source / d'origine, il faut utiliser l'outil sponge (une redirection shell, >, viderait le fichier avant d'exécuter jq).



    Il est possible de travailler sur plusieurs fichiers JSON en même temps. Soit en les concaténant (cat fichier1 fichier2 | jq '.'). Oui, ça va générer un fichier JSON invalide (plusieurs entités racines…), mais jq est tolérant. Soit en ayant un fichier principal et des fichiers secondaires accessibles via des variables. Cette deuxième possibilité est très pratique quand on veut agrémenter / enrichir un fichier principal avec des informations disponibles dans un autre fichier (sans polluer le fichier principal avec une concaténation, donc).

    • Exemple pratique. Le fichier cables.json contient les coordonnées géographiques des éléments et le fichier cablesDefs.json contient des informations sur ces éléments (comme leur nom). Dans le fichier cablesDefs, on veut récupérer le nom de chaque élément du fichier cables.json afin de l'insérer dans un attribut « name » dans l'attribut « properties » de l'élément. On veut donc enrichir cables.json avec cablesDefs.json. Notons que les éléments ne sont pas forcément dans le même ordre dans les deux fichiers. Il y a une correspondance id = slug. Démo : jq --slurpfile maVariable cablesDefs.json '.features[] | . as $cable | $maVariable[] | select(.id==$cable.properties.slug).name as $cableName | $cable | .properties += { "name": $cableName }' cables.json.

    • Explication : pour chaque élément du tableau « features », on le stocke dans la variable $cable puis, dans le contenu de maVariable (donc du fichier cableDefs.json, dont chaque élément racine a été importé comme case d'un tableau), on cherche un objet dont l'attribut « id » correspond à l'attribut « slug » de notre élément, on stocke la valeur de l'attribut « nom » de cet objet dans $cableName. On affiche l'élément de notre tableau « features » et on ajoute un attribut dans son attribut « properties ».



    Plus haut, j'écris que si l'on itère sur le contenu d'un attribut avec jq, alors on perd les attributs qui entourent celui sur lequel on itère. Avec ce "mode multi-fichiers", on peut reconstruire le JSON d'origine (sauf si trop d'élements sont passés à la trappe, bien entendu). Exemple (je reprends le cas cité ci-dessus malgré son absence de représentativité) :

    jq '.features[] | .' cables.json > /tmp/json.tmp # dans json.tmp, on aurait donc uniquement les éléments du tableau « features », tout le reste aura disparu
    cat <<EOF > /tmp/minJSONStruct # dans ce fichier, on met l'entête et le pied du fichier d'origine (cables.json), précisement ce qui a disparu
    {
      "type": "FeatureCollection",
      "features": [
      ]
    }
    EOF
    jq --slurpfile featuresList /tmp/json.tmp '.features += $featuresList' /tmp/minJSONStruct > cables.json # on ajoute les ex-éléments du tableau « features » au tableau « features » de l'entête/pied JSON, donc on reconstitue notre fichier cables.json d'origine.

    Oui, je préfère passer par un fichier temporaire que par une variable (avec « --arg ») afin d'éviter les problèmes d'échappement de caractères pour le shell.

    On peut aussi préserver la structure JSON autour du tableau. Pour ce faire, il faut contenir l'itération sur le tableau au sein d'une affectation. Exemple : pour renommer l'attribut « properties.color » d'un objet membre d'un tableau en « properties.toto », il faut que j'itère sur ce tableau sinon je ne peux pas récupérer la valeur de l'attribut afin de la transvaser. Pour que cette opération ne soit pas destructrice, je l'encadre dans une affectation : jq '. as $racine | .featuresDup |= $racine.features | del(.features) | .features += [ .featuresDup[] | .properties += { "toto": .properties.color } | del(.properties.color) ] | del(.featuresDup)' cables.json. Si je n'avais pas dupliqué l'attribut « features », alors ce tableau contiendrait deux fois chaque élément : l'original et la version modifiée. Cette copie me permet de vider l'attribut « features » et d'utiliser sa copie pour itérer dessus et remplir « features » avec les objets modifiés.



    Appliquer le même traitement à tous les éléments d'un tableau. Pour chaque élément du fichier cablesDefs.json, je veux récupérer une unique chaîne de caractères contenant tous les éléments du tableau « landing_points » sous la forme * <nom1>: <latitude+longitude>\n * <nom2>: <latitude+longitude>\n […]. Premier essai : jq '.landing_points[] | " * \(.name): \(.latlon)"' cablesDefs.json. Bien vu, mais on ne peut pas obtenir une unique chaîne avec join() puisque ce qui ressort du filtre, c'est des chaînes de caractères et qu'un join() ne peut être appliqué sur ce type d'objets.

    • Comme dans beaucoup de langage de programmation (Python, Ruby, etc.), la fonction map() permet d'appliquer un même traitement à chaque élément d'un tableau, donc de produire un tableau en sortie… tableau sur lequel on pourra utiliser join(). Démo : jq '.landing_points | map(" * \(.name) :\(.latlon)") | join("\n")' cablesDefs.json.



    Il reste encore beaucoup de choses à découvrir : les boucles, les conditions, les fonctions persos, etc. Voir le manuel officiel de jq. Pour ma part, j'ai pu réaliser ce que je voulais (construire une carte des câbles sous-marins d'Internet) donc je m'arrête là.

    Sun 05 Apr 2020 06:47:42 PM CEST - permalink -
    - http://shaarli.guiguishow.info/?KMmDNA
Links per page: 20 50 100
page 1 / 1
Mentions légales identiques à celles de mon blog | CC BY-SA 3.0

Shaarli - The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community - Help/documentation