Journal Aujourd'hui on ne parle pas de k-pop, mais de JSON !

Posté par  (site web personnel) . Licence CC By‑SA.
Étiquettes :
20
24
juin
2023

Sommaire

ATTENTION, sous ses faux airs d’impartialité, ce journal est une pub éhontée pour mon projet json-search

Bonjour, il y a quelques temps, j'ai posté ce lien
dans lequel on m'a demandé "pourquoi ne pas écrire un journal ?"
Au lieu de ne parler que de mon projet, je pop donc une comparaison de plusieurs outils pour chercher des éléments dans un fichier JSON.
(C'est donc de la j-pop)

J'ai pris ce fichier (un peu impartialement) car je voulais un fichier open source appartenant à un projet un peu connu. Et quoi de mieux qu'un module pulse audio pour cet article j-pop. (Ok, j'ai pris le 1er json flathub de plus de 20 lignes…)

Les outils dont je vais parler :

json-search

gron

jq

Des 3 commandes dont je parle ici, jq est de loin la plus puissante, et pour être honnête, on peut tout faire avec.
Je connais assez mal la syntaxe, mais je vais quand même essayer d'en expliquer l'utilisation que je sais en faire.
Et notez que je manque sûrement de vocabulaire et qu'il va donc peut-être y avoir des imprécisions dans mes explications.

Comment chercher dans les valeurs tout ce qui concerne des libs:

Avec json-search:

[uso]$  cat org.freedesktop.LinuxAudio.Plugins.Yoshimi.json | json-search -Vsil "lib"
<stdin> - .modules[4].post_install[1]: "mv ${FLATPAK_DEST}/lib/lv2/yoshimi.lv2 ${FLATPAK_DEST}/lv2"
<stdin> - .modules[4].build-options.env.LDFLAGS: "-L${FLATPAK_DEST}/lib -lpng16 -lXrender -lXcursor -lXfixes -lXext -lXft -lfontconfig -lXinerama -lpthread -ldl -lm  -lX11"
<stdin> - .modules[1].cleanup[2]: "/lib/pkgconfig"
<stdin> - .modules[0].cleanup[5]: "/lib/pkgconfig"
<stdin> - .cleanup[4]: "/lib/lv2"
<stdin> - .cleanup[3]: "/lib/jack"
<stdin> - .cleanup[2]: "/lib/libjack*"
<stdin> - .build-options.prepend-ld-library-path: "/app/extensions/Plugins/Yoshimi/lib"
<stdin> - .build-options.prepend-pkg-config-path: "/app/extensions/Plugins/Yoshimi/lib/pkgconfig"

Les options utilisées sont :

  • '-V' pour chercher dans les valeurs et pas dans les clés
  • '-s' pour matcher des bouts de chaîne de caractère et pas une comparaison exacte
  • '-i' pour être insensible à la casse
  • '-l' pour avoir la "localisation" des éléments, par exemple " .modules[1].cleanup[2]:" avant "/lib/libjack*"

Avec gron :

[uso]$ gron org.freedesktop.LinuxAudio.Plugins.Yoshimi.json | grep -i lib
json.cleanup[2] = "/lib/libjack*";
json.cleanup[3] = "/lib/jack";
json.cleanup[4] = "/lib/lv2";
json.modules[0].cleanup[5] = "/lib/pkgconfig";
json.modules[1].cleanup[2] = "/lib/pkgconfig";
json.modules[4].post_install[1] = "mv ${FLATPAK_DEST}/lib/lv2/yoshimi.lv2 ${FLATPAK_DEST}/lv2";
json.modules[4]["build-options"].env.LDFLAGS = "-L${FLATPAK_DEST}/lib -lpng16 -lXrender -lXcursor -lXfixes -lXext -lXft -lfontconfig -lXinerama -lpthread -ldl -lm  -lX11";
json["build-options"]["prepend-ld-library-path"] = "/app/extensions/Plugins/Yoshimi/lib";
json["build-options"]["prepend-pkg-config-path"] = "/app/extensions/Plugins/Yoshimi/lib/pkgconfig";

'gron FILE' permet de transformer le fichier json en une sortie grepable. Et 'grep' matche toutes les lignes contenant "lib" avec -i pour être insensible à la casse.

Plus d'infos sur 'build-options'

comment avoir un tableau json de tous les objets 'build-options':

[uso]$ cat org.freedesktop.LinuxAudio.Plugins.Yoshimi.json | json-search build-options

note que j'ai utilisé ... pour simplifier le résultat

[
{
  "env":{
    "LV2_PATH":"${FLATPAK_DEST}/lv2",
    "LDFLAGS":"-L${FLATPAK_DEST}/lib -lpng16 -lXrender -lXcursor -lXfixes -lXext -lXft -lfontconfig -lXinerama -lpthread -ldl -lm  -lX11"
  },
  "arch":{
    "x86_64":{
      "config_opts":[
        "-DBuildOptions_X86_64Core2=YES"
      ]
    },
    "aarch64":{...}
  }
},
{"cflags":"-fPIC"},
{...},
{...}
]

comment reconstruire l'arbre json en ne gardant que les éléments build option :

[uso]$ gron org.freedesktop.LinuxAudio.Plugins.Yoshimi.json | grep 'build-options' | gron -u

"gron org.freedesktop.LinuxAudio.Plugins.Yoshimi.json" en fait une sortie grepable. Grep prend ce qui concerne "build-options" et "-u" reconstruit le json.

ici aussi j'utilise ... pour simplifier

{
  "build-options": {...},
  "modules": [
    {
      "build-options": {
        "cflags": "-fPIC",
        "cxxflags": "-fPIC"
      }
    },
    {
        "build-options": {...}
    },
     null,
     null,
    {
      "build-options": {
        "arch": ...,
          "x86_64": ...,
        },
        "env": {...}
      }
    }
  ]
}

récupérer 'build-options' à la racine

[uso]$ cat org.freedesktop.LinuxAudio.Plugins.Yoshimi.json | jq '."build-options"'
[uso]$ cat org.freedesktop.LinuxAudio.Plugins.Yoshimi.json | json-search  -M 1 build-options

Sortent le même résultat: (bon jq est en couleur)

{
  "prefix": "/app/extensions/Plugins/Yoshimi",
  "prepend-pkg-config-path": "/app/extensions/Plugins/Yoshimi/lib/pkgconfig",
  "prepend-ld-library-path": "/app/extensions/Plugins/Yoshimi/lib"
}

jq prend un filtre en paramètre (ici '."build-options"'),
Pour des manipulations simples, on peut retenir que . permet de récupérer un élément d'un objet/array.
Pour récupérer une valeur d'un objet, on va soit utiliser .KEY soit ."MY-KEY" si on a des valeurs qui ont besoin d'être entre quotes.
Pour récupérer une valeur d'un tableau, on peut utiliser .[IDX].
On peut chaîner ces .KEY/.[IDX] pour obtenir un genre de chemin. (example .KEY0.[0])

# Cherchons une "build-options" plus imbriquée :

Qui serait à la place de TARGET dans ce json: '{"modules": [{},{},{},{"build-options": TARGET} ] }'

  cat org.freedesktop.LinuxAudio.Plugins.Yoshimi.json | jq '.modules[4]."build-options"' 
  cat org.freedesktop.LinuxAudio.Plugins.Yoshimi.json | jq '.modules[4]' | json-search build-options 
  cat org.freedesktop.LinuxAudio.Plugins.Yoshimi.json | json-search build-options | json-search -p env
  cat org.freedesktop.LinuxAudio.Plugins.Yoshimi.json | json-search build-options |  jq '.[0]'

Les 4 commandes ont le même résultat

  {
  "env":{
    "LV2_PATH":"${FLATPAK_DEST}/lv2",
    "LDFLAGS":"-L${FLATPAK_DEST}/lib -lpng16 -lXrender -lXcursor -lXfixes -lXext -lXft -lfontconfig -lXinerama -lpthread -ldl -lm  -lX11"
  },
  "arch":{
    "x86_64":{
      "config_opts":[
        "-DBuildOptions_X86_64Core2=YES"
      ]
    },
    "aarch64":{
      "config_opts":[
        "-DBuildOptions_RasPi4=YES"
      ]
    }
  }
}

La 1ère commande met le chemin en entier, par contre il faut faire attention à mettre les "doubles quotes" autour de build-options, car le '-' casse sinon.
La 2ème commande fait un mélange de jq et json-search, bien que plus lourde syntaxiquement, je la trouve plus simple à retenir.
La 3ème n'utilise que json-search, mais triche un peu. En premier, je cherche tous les 'build-options', puis je cherche "env", mais n'utilise que l'option -p pour afficher l'élément parent trouvé.
Enfin la dernière commande : je réutilise json-search pour avoir tous les 'build-options', mais après jq, pour avoir le 1er élément du tableau affiché par json-search.

Si on veut mélanger gron et jq, on peut aussi faire :

  cat org.freedesktop.LinuxAudio.Plugins.Yoshimi.json | jq '.modules[4]' | gron | grep build-options | gron -u

Pour avoir un résultat assez similaire (mais pas exactement) :

Frequent… fre…

Eh non

uso (Q)> Mais pourquoi ne pas juste utiliser jq ?
uso (A)> Bah en fait les 2 outils sont complémentaires, mais jq fait mal à la tête.

uso (Q)> En vrai gron fait le taff, ça ne sert donc à rien ton truc
uso (A)> Alors oui, mais c'est plus simple d'utilisation. Bon, j'ai peut-être connu l'existence de gron il y a ~2 semaines et comme tout bon dev, je code avant de réfléchir.

uso (Q)> Non, mais le C, c'est un très mauvais choix de langages, y'a le rust, c'est pas sécure, y'a le rust, plus personne en fait, y'a le rust.
uso (A)> Je pourrais mentir à dire que le C est un langages très peu verboses, mentir à dire que j'aime beaucoup json-c comme bibliothèque, mentir a dire que la performance m’intéresse. Tous ça pour ne pas dire, que sur un projets personnel, je prend les langages amusant.

uso (Q)> Mais pourquoi la licence WTFPL, il y a Fuck dans le nom ?
uso (A)> Car il y a Fuck dans le nom ¯_(ツ)_/¯

uso (Q)> GPLu d'idée de questions, pourquoi pas du copyleft ?
uso (A)> Bah j'aurais tendance à utiliser du copyleft pour des projets d'une plus grosse envergure

Conclusion:

Bon bah vous avez pleins d'exemples sur des trucs à faire avec json-search, jq et gron.
Ça reste une utilisation très simple, et je n'ai absolument pas parlé de comment modifier ces json, qui pourrait valoir un autre journal juste pour ça… Mais je ne sais pas faire.

J'aurais aussi pu parler de mlr, mais ça à l'air de faire mal à la tête et ça ne me semble être utile que pour une utilisation beaucoup plus puissante.

benchmark

car comme dirait l'autre: "L’optimisation tardive est la source de tous l'émo" (à moins que ça soit les émeus, je ne me rappelle plus de la citation exacte) :

[uso]$ time ./bench-json-search.sh ; time ./bench-gron.sh  ; time ./bench-jq.sh                  
./bench-json-search.sh  0.72s user 1.68s system 135% cpu 1.782 total
./bench-gron.sh  3.96s user 5.78s system 178% cpu 5.450 total
./bench-jq.sh  13.47s user 1.71s system 104% cpu 14.559 total
[uso]$ % cat bench-*
for i in {1..1000}
do
    cat org.freedesktop.LinuxAudio.Plugins.Yoshimi.json | gron | grep build-options | gron -u > /dev/null
done
for i in {1..1000}
do
    cat org.freedesktop.LinuxAudio.Plugins.Yoshimi.json | jq '."build-options"'  > /dev/null
done
for i in {1..1000}
do
    cat org.freedesktop.LinuxAudio.Plugins.Yoshimi.json | json-search build-options  > /dev/null
done
  • # lien avec la K-pop ?

    Posté par  . Évalué à 2. Dernière modification le 25 juin 2023 à 11:26.

    Je ne suis pas certain de saisir le lien entre la K-pop/J-pop et Json , pourrais tu expliciter ? et aussi pourquoi un fichier J-pop serait "mieux" ? pour l'usage de Json ?

    • [^] # Re: lien avec la K-pop ?

      Posté par  (site web personnel) . Évalué à 2. Dernière modification le 25 juin 2023 à 12:02.

      le lien entre la K-pop/J-pop et Json ?

      bin ça dépend si tu préfères le Japon avec la musique J-pop ou la Corée du Sud avec la musique K-pop :-)

      plus sérieusement, c'est indiqué dans l'intro du nourjal, je cite (le gras est de moi) :

      Au lieu de ne parler que de mon projet, je pop donc une comparaison de plusieurs outils pour chercher des éléments dans un fichier JSON.
      (C'est donc de la j-pop)

      J'ai pris ce fichier (un peu impartialement) car je voulais un fichier open source appartenant à un projet un peu connu. Et quoi de mieux qu'un module pulse audio pour cet article j-pop. (Ok, j'ai pris le 1er json flathub de plus de 20 lignes…)

      • [^] # Re: lien avec la K-pop ?

        Posté par  . Évalué à 2. Dernière modification le 25 juin 2023 à 13:38.

        Oui j'ai lu cette partie d'ou mon commentaire avec J-pop, sur le fond je veux dire au niveau du contenu de la K-pop c'est pas évident du tout le lien que tu tentes d'établir. j'ai eu l'impression de m'être "fait eu" :D je m'attendais à un lien réelle entre Kpop et Json.

        • [^] # Re: lien avec la K-pop ?

          Posté par  (site web personnel) . Évalué à 1.

          En vrais c'est un jeu de mot pourrit K-pop/J-Son (JSON).
          Mais je l'ai un peu reprit dans le début du journal comme la expliqué BAud, pour renforcer ce lien.

          • [^] # Re: lien avec la K-pop ?

            Posté par  . Évalué à 5.

            Quand on en est à essayer de laborieusement expliquer les jeux de mots, K-son nous. /o\

            ( non c’est parce que le cas et le son, c’est la dernière combinaison qui manque, alors je … oui, quoi, la sortie ? … euh oui … merci ==>[] )

  • # gron patapouf

    Posté par  . Évalué à 3.

    J'aime bien gron parce qu'il permet d'utiliser des outils de traitement ligne à ligne comme AWK.

    gron in.json | awk '{truc de ouf}' | gron -u out.json

    Par contre il est

    1. très mal optimisé, en RAM comme en CPU
    2. non stable dans le sens ou les objets ne sont pas restitués dans l'ordre d'origine mais triés par clés. Je sais que c'est conforme à la spec. Mais dans les faits un peu pénible quand on veut faire un traitement qui utilise des données sensées arriver "avant".
    • [^] # gron'telnatif

      Posté par  (site web personnel, Mastodon) . Évalué à 4. Dernière modification le 25 juin 2023 à 16:08.

      De prime abord, ce que réalise gron in.json (i.e. déplier le JSON pour que ce soit manipulable par les outils qui manipulent des lignes de texte brut) s'obtient aussi avec :

      • jq . in.json avec en prime l'indentation et ça s'en sort mieux en terme de consommation mémoire (cf. l'autre message) Mais parfois il n'est pas présent et je ne peux pas l'installer non plus, donc on regarder du côté des langages de script présents sur la bécane (cas ci-après.)
      • python -m json.tool in.json dans le cas-ci, mais si on a besoin d'aller plus loin c'est possible
        • python3 -c "import sys, json; print(json.load(sys.stdin)" in.json
        • export PYTHONIOENCODING=utf8 python2 -c "import sys, json; print json.load(sys.stdin)" in.json
      • ruby -r json -e 'jj JSON.parse gets' < in.json avec le Gem JSON qu'il faut installer avant s'il ne l'est pas déjà. Il y a aussi jazor qui est sympa
      • côté PERL…
        • json_xs -t null < in.json en utilisant le module JSON::XS qui fait appel à du code C (XS pour eXtension System)
        • sinon on peut se rabattre sur json_pp (PP pour Pur Perl) qui utilise le module JSON:PP présent dans la plupart des installations systèmes.
        • Il y a aussi un autre module JSON tout court qui permet d'écrire perl -MJSON -nE 'say JSON->new->pretty->encode(from_json $_)' in.json ou perl -MJSON -e 'print encode_json(decode_json(<STDIN>), {pretty => 1})' < in.json par exemple
      • PHP n'est pas en reste depuis la version 5.2 : php -r 'print_r(json_decode(fgets(STDIN)))' (ça fait longtemps, je l'ai fait de tête sans tester)
      • En Go…
      • bat -p -l json in.json (cet alternatif à cat est parfois surprenant)
      • Avec miller quelque chose comme mlr --json --ojson in.json
      • Il me semble qu'avec Node.JS qu'on peut faire quelque chose comme node -pe 'JSON.parse(process.argv[1])' in.json
      • PowerShell a aussi son ConvertFrom-Json

      Autres outils qui peuvent valoir le détour : jshon, jwalk, fx, jsawk, etc.

      P.S. Je n'ai pas mentionné json-flatten
      P.S.2 Mentionnez/référencez votre astuce aussi

      “It is seldom that liberty of any kind is lost all at once.” ― David Hume

      • [^] # Re: gron'telnatif

        Posté par  . Évalué à 3.

        Je n'ai pas testé toutes tes options mais rien qu'en partant de la première jq ., je ne sais pas si on parle de la même chose.

        $ jq '.' <<< '{"a":{"b":"c"}}'
        {
          "a": {
            "b": "c"
          }
        }
        

        vs

        $ gron <<< '{"a":{"b":"c"}}'
        json = {};
        json.a = {};
        json.a.b = "c";
        

        Jq indente alors que gron donne le chemin complet pour chaque ligne (l'idée de "flatten", mettre à plat).

        Donc avec gron je peux faire une décision local (si chemin est "a.b", faire un uppercase à la valeur). Alors que pour jq, je vais devoir maintenir un état (pile ?) contenant le chemin actuel.

        D'autre part, cf "ti bench", jq ne fonctionne pas en flux, par défaut, il charge tout en mémoire.

        Le mode flux existe et est efficace (21MB de RAM dans mon "ti bench"), mais difficilement exploitable sans post-traitement :

        jq --stream '.' <<< '{"a":{"b":"c"}}'
        [
          [
            "a",
            "b"
          ],
          "c"
        ]
        [
          [
            "a",
            "b"
          ]
        ]
        [
          [
            "a"
          ]
        ]
        

        Bref, je cherche un gron rapide, économe en RAM et avec une sortie stable.

        Y a ça dans tes options ?

      • [^] # Re: gron'telnatif

        Posté par  . Évalué à 2.

        De prime abord, ce que réalise gron in.json (i.e. déplier le JSON pour que ce soit manipulable par les outils qui manipulent des lignes de texte brut) s'obtient aussi avec :

        Non ça n'a pas grand chose à voir. gron permet de manipuler des lignes qui n'ont pas besoin de contexte et c'est en ça qu'ils permettent d'utiliser ensuite grep ou sed de manière fiable et simple.

        Si tu veux un exemple, si j'ai ce json :

        {
          "foo": 11,
          "elem": {
            "foo": 12,
            "bar": "foo"
          }
        }

        et que je cherche la valeur associée au champ foo de l'objet elem.

        Avec jq le mieux c'est de rester entièrement en syntaxe jq .elem.foo.

        Avec gron tu va pouvoir ne pas apprendre une nouvelle syntaxe et utiliser awk comme awk '"json.elem.foo" == $1{sub(/;/,"");print $NF}' (oui c'est un cas où c'est bien plus compliqué, mais la syntaxe de jq peut devenir assez ésotérique et awk a plus d'usage que jq.

        Il me semble que la plupart des autres commandes ne font rien d'autres que formater du json. Ce qui est loin de l'objectif de gron

        https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll

  • # ti bench

    Posté par  . Évalué à 4. Dernière modification le 25 juin 2023 à 12:27.

    Je m'étais lancé dans un benchmark sur gros fichiers, fut un temps. J'ajoute une ligne.

    Sample file

    • 74MB
    • 2.7M tokens
    • 267k objects

    result

    tool            RAM (KB)    time
    --------------- ----------- ------
    wc (baseline)       1'924    0.34
    jq                247'364    2.36
    gron            1'103'992    5.47
    nron              263'824    1.19
    json-flatten      402'708    3.18
    json-search       413'196    0.96

     analyse

    • gron, j'en ai parlé juste au dessus
    • jq s'en sort pas si mal compte tenu de tout ce qu'il sait faire
    • nron, c'est une tentative de ma part de réimplémenter gron en nim. Je n'y suis pas encore.
    • json-flatten en une implémentation en python
    • json-search est sacrément véloce

    Tous sont très gourmands en RAM. Alors que de ma compréhension ils font un traitement local - O(1) - qui ne devrait pas dépendre de la taille des données - O(N). Au pire de la profondeur des données (longueur du path). Surement les parseurs sous-jacents qui seraient à revoir.

    • [^] # Re: ti bench

      Posté par  . Évalué à 1.

      Super intéressant !

      Effectivement, je suis tout à fait d'accord avec toi, l'usage de la mémoire semble exponentiel, comme s'il y avait un parsing complet avec stockage de tous les composants avant de faire la recherche en elle même.

      Il serait possible de partager les scripts et fichiers que tu utilises ?

      • [^] # Re: ti bench

        Posté par  . Évalué à 3.

        Je dirai que l'empreinte mémoire est linéaire - O(N) - au lieu d'être constante - O(1), comme c'est le cas pour wc qui compte les lignes.

        Concernant les scripts, rien de bien folichon, je me contente mesurer une lecture complète d'un gros fichier : /usr/bin/time -f %M,%P,%e,%U,%S,%W,%c,%w,%x <command> < big.json > /dev/null. Pour chaque commande.

        Concernant les données, j'utilise des trucs qui traînent sur mon DD mais dont je ne me rappelle pas trop la provenance et que j'ai pas trop la possibilité de rediffuser mais je suppose qu'en cherchant un peu sur le web on peut trouver des gros json. Ou peut être les générer.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.