cClaude.rocks ☕ Le blog

[Nouvelles technologies du libre, sciences et coups de gueule…]

Menu
😤 Ce billet a été édité le : 2024-02-23

On a déjà vu comment convertir un fichier CSV en JSON dans le billet ⭾ Convertir CSV vers du JSON, mais étudions cela plus en détail.

Ici, je propose une solution basée uniquement sur jq.



Le fichier « CSV » d’entrée

On va construire un flux CSV à l’aide de la commande echo :

echo -e 'a,b,c,d\naa,bb,cc,dd\naaa,bbb,ccc,ddd'

Notez l’utilisation du commutateur -e pour prendre en compte les sauts de lignes (\n).

a,b,c,d
aa,bb,cc,dd
aaa,bbb,ccc,ddd


La conversion étape par étape

En premier lieu, on utilise l’option --raw-input de jq, en effet le flux d’entrée ne correspond pas à un flux JSON, ni même à une chaîne JSON.

echo -e 'a,b,c,d\naa,bb,cc,dd\naaa,bbb,ccc,ddd' | jq --raw-input '.'

On obtient une chaîne par ligne :

"a,b,c,d"
"aa,bb,cc,dd"
"aaa,bbb,ccc,ddd"

Si on ajoute --slurp de jq

echo -e 'a,b,c,d\naa,bb,cc,dd\naaa,bbb,ccc,ddd' |
  jq --raw-input --slurp '.'

Cette fois, on a une seule chaîne au format JSON :

"a,b,c,d\naa,bb,cc,dd\naaa,bbb,ccc,ddd\n"

Il faut découper la chaîne d’entrée à l’aide du filtre jq : split("\n")

echo -e 'a,b,c,d\naa,bb,cc,dd\naaa,bbb,ccc,ddd' |
  jq --raw-input --slurp 'split("\n")'

Mise à jour

ou beaucoup mieux :

echo -e 'a,b,c,d\naa,bb,cc,dd\naaa,bbb,ccc,ddd' |
  jq --raw-input --null-input '[ inputs ]'

Cette solution résout le problème mentionné ci-après à savoir : l’ajout d’une ligne vide.

[
  "a,b,c,d",
  "aa,bb,cc,dd",
  "aaa,bbb,ccc,ddd",
  ""
]

On note un petit effet de bord non souhaité puisque cela ajoute une ligne vide non souhaitée.

On va supprimer la dernière ligne avec l’aide du filtre .[:(length -1)]:

echo -e 'a,b,c,d\naa,bb,cc,dd\naaa,bbb,ccc,ddd' |
  jq --raw-input --slurp 'split("\n") | .[:(length -1)]'
[
  "a,b,c,d",
  "aa,bb,cc,dd",
  "aaa,bbb,ccc,ddd"
]

C’est cette fois-ci conforme à notre attente.

L’étape suivante consiste à découper les champs CSV, cela se fait à l’aide de split(",") comme suit :

echo -e 'a,b,c,d\naa,bb,cc,dd\naaa,bbb,ccc,ddd' |
  jq --raw-input --slurp 'split("\n")
    | .[:(length -1)]
    | map(split(","))'

On y est presque…

Le résultat ci-dessous a été partiellement reformaté pour la lisibilité, donc vous obtiendrez presque :

[
  [ "a"  , "b"  , "c"  , "d"   ],
  [ "aa" , "bb" , "cc" , "dd"  ],
  [ "aaa", "bbb", "ccc", "ddd" ]
]

 Solution numéro 1

Il ne nous reste plus qu’à reconstruire l’élément souhaité à partir du sous-tableau.
On veut donc réécrire le tableau [ "a", "b", "c", "d" ] en { "A": "a", "B": "b", "C": "c", "D": "d" }.
Cela se fait simplement à l’aide de : { "A":.[0], "B":.[1], "C":.[2], "D": .[3] }

echo -e 'a,b,c,d\naa,bb,cc,dd\naaa,bbb,ccc,ddd' |
  jq --raw-input --slurp 'split("\n")
    | .[:(length -1)]
    | map(split(","))
    | map( { "A":.[0], "B":.[1], "C":.[2], "D": .[3] } )'

Le résultat ci-dessous a été partiellement reformaté pour la lisibilité, donc vous obtiendrez presque :

[
  { "A": "a",   "B": "b",   "C": "c",   "D": "d"   },
  { "A": "aa",  "B": "bb",  "C": "cc",  "D": "dd"  },
  { "A": "aaa", "B": "bbb", "C": "ccc", "D": "ddd" }
]


Considérations autour de l’optimisation

Solution numéro 2

Notez que l’écriture donne la solution numéro 1 donne exactement le même résultat que ce code :

echo -e 'a,b,c,d\naa,bb,cc,dd\naaa,bbb,ccc,ddd' |
  jq -c --raw-input '.' |
  jq --slurp 'map(split(",")) | map( { "A":.[0], "B":.[1], "C":.[2], "D":.[3] } )'

Quelles sont les différences de cette seconde solution ?

  • On ne crée pas de chaîne JSON contenant tout le fichier CSV (Avantage, surtout si le fichier d’entrer est gros)
  • On ne crée pas l’élément « en trop » que l’on doit supprimer dans la solution 1 (Avantage),
  • On a besoin d’exécuter 2 fois jq (Inconvénient).

Le résultat ci-dessous a été partiellement reformaté pour la lisibilité :

[
  { "A": "a",   "B": "b",   "C": "c",   "D": "d"   },
  { "A": "aa",  "B": "bb",  "C": "cc",  "D": "dd"  },
  { "A": "aaa", "B": "bbb", "C": "ccc", "D": "ddd" }
]

Comment se convaincre que le pipeline (|) après jq -c --raw-input '.' ne va pas consommer beaucoup de mémoire pour stocker le fichier d’entrée ?

Tout simplement en utilisant l’écriture suivante pour générer le flux CSV :

( echo -e 'a,b,c,d' ; sleep 5 ; echo -e 'aa,bb,cc,dd\naaa,bbb,ccc,ddd' )

Le sleep 5 est là pour simuler un gros fichier CSV ou un temps de construction long d’un flux.

Et lorsqu’on exécute avec jq, on s’aperçoit qu’on a la première ligne immédiatement, et 5 secondes après la suite :

( echo -e 'a,b,c,d' ; sleep 5 ; echo -e 'aa,bb,cc,dd\naaa,bbb,ccc,ddd' ) |
  jq -c --raw-input '.'

Solution numéro 3

Dans le cas d’un très gros fichier CSV, les deux premières solutions requièrent beaucoup de mémoire (et potentiellement trop).

On va essayer de faire un peu mieux…

On va écrire les lignes une part une et attendre 2 secondes entre chaque ligne pour voir ce qu’il se passe.

L’idée ici est de traiter ligne par ligne et au lieu de chercher à obtenir un tableau JSON avec toutes les lignes, on va se contenter d’une énumération d’objets JSON.

Concrètement au lieu d’obtenir :

[
  { …EL1… },
  { …EL2… }.
  …,
  { …ELn… }
]

on va obtenir :

{ …EL1… }
{ …EL2… }
…
{ …ELn… }

On traitera alors le résultat comme un flux JSON, ce format étant particulièrement bien adapté aux grosses quantités de données ou aux traitements temps réel.

Pour cela, il suffit juste de faire :

( echo 'a,b,c,d' ; sleep 2 ; echo 'aa,bb,cc,dd' ; sleep 2 ; echo 'aaa,bbb,ccc,ddd' ) |
   jq -c --raw-input 'split(",")'
["a","b","c","d"]
["aa","bb","cc","dd"]
["aaa","bbb","ccc","ddd"]

Que l’on finalisera comme suit :

( echo 'a,b,c,d' ; sleep 2 ; echo 'aa,bb,cc,dd' ; sleep 2 ; echo 'aaa,bbb,ccc,ddd' ) |
  jq -c --raw-input 'split(",")
    | { "A":.[0], "B":.[1], "C":.[2], "D":.[3] }'
{"A":"a","B":"b","C":"c","D":"d"}
{"A":"aa","B":"bb","C":"cc","D":"dd"}
{"A":"aaa","B":"bbb","C":"ccc","D":"ddd"}

Gestion des entêtes

Considérons le flux CSV suivant :

echo -e 'Les A,Les B,Les C,Les D\na,b,c,d\naa,bb,cc,dd\naaa,bbb,ccc,ddd'
Les A,Les B,Les C,Les D
a,b,c,d
aa,bb,cc,dd
aaa,bbb,ccc,ddd

Pour traiter la première ligne comme contenant les entêtes, on peut utiliser le fichier csv2json.jq donné dans le billet : ⭾ Convertir CSV vers du JSON

echo -e 'Les A,Les B, Les C, Les D\na,b,c,d\naa,bb,cc,dd\naaa,bbb,ccc,ddd' |
   jq -R -s -f csv2json.jq
[
  {
    "Les A": "a",
    "Les B": "b",
    "Les C": "c"
  },
  {
    "Les A": "aa",
    "Les B": "bb",
    "Les C": "cc"
  },
  {
    "Les A": "aaa",
    "Les B": "bbb",
    "Les C": "ccc"
  }
]


Liens

ᦿ


ℹ 2006 - 2024 | 🏠 Accueil du domaine | 🏡 Accueil du blog