Comment convertir arbirtrary JSON simple en CSV en utilisant jq?

En utilisant jq , comment un codage JSON arbitraire d’un tableau d’objects peu profonds peut-il être converti en CSV?

Il y a beaucoup de questions et réponses sur ce site qui couvrent des modèles de données spécifiques qui codent en dur les champs, mais les réponses à cette question devraient fonctionner avec n’importe quel JSON, avec la seule ressortingction qu’il s’agit d’un tableau d’objects avec propriétés scalaires des sous-objects, car aplatir ces derniers est une autre question). Le résultat doit contenir une ligne d’en-tête donnant les noms de champs. La préférence sera donnée aux réponses qui préservent l’ordre des champs du premier object, mais ce n’est pas une exigence. Les résultats peuvent inclure toutes les cellules avec des guillemets doubles, ou inclure uniquement ceux qui nécessitent des guillemets (par exemple, ‘a, b’).

Exemples

  1. Consortingbution:

    [ {"code": "NSW", "name": "New South Wales", "level":"state", "country": "AU"}, {"code": "AB", "name": "Alberta", "level":"province", "country": "CA"}, {"code": "ABD", "name": "Aberdeenshire", "level":"council area", "country": "GB"}, {"code": "AK", "name": "Alaska", "level":"state", "country": "US"} ] 

    Sortie possible:

     code,name,level,country NSW,New South Wales,state,AU AB,Alberta,province,CA ABD,Aberdeenshire,council area,GB AK,Alaska,state,US 

    Sortie possible:

     "code","name","level","country" "NSW","New South Wales","state","AU" "AB","Alberta","province","CA" "ABD","Aberdeenshire","council area","GB" "AK","Alaska","state","US" 
  2. Consortingbution:

     [ {"name": "bang", "value": "!", "level": 0}, {"name": "letters", "value": "a,b,c", "level": 0}, {"name": "letters", "value": "x,y,z", "level": 1}, {"name": "bang", "value": "\"!\"", "level": 1} ] 

    Sortie possible:

     name,value,level bang,!,0 letters,"a,b,c",0 letters,"x,y,z",1 bang,"""!""",0 

    Sortie possible:

     "name","value","level" "bang","!","0" "letters","a,b,c","0" "letters","x,y,z","1" "bang","""!""","1" 

Tout d’abord, obtenez un tableau contenant tous les différents noms de propriétés d’object dans votre tableau d’objects. Ce seront les colonnes de votre CSV:

 (map(keys) | add | unique) as $cols 

Ensuite, pour chaque object de l’entrée du tableau d’objects, mappez les noms de colonnes que vous avez obtenus aux propriétés correspondantes dans l’object. Ce seront les lignes de votre CSV.

 map(. as $row | $cols | map($row[.])) as $rows 

Enfin, placez les noms de colonne avant les lignes, en tant qu’en-tête du @csv CSV, puis transmettez le stream de lignes résultant au filtre @csv .

 $cols, $rows[] | @csv 

Tous ensemble maintenant. N’oubliez pas d’utiliser l’ -r pour obtenir le résultat sous forme de chaîne brute:

 jq -r '(map(keys) | add | unique) as $cols | map(. as $row | $cols | map($row[.])) as $rows | $cols, $rows[] | @csv' 

Le maigre

 jq -r '(.[0] | keys_unsorted) as $keys | $keys, map([.[ $keys[] ]])[] | @csv' 

ou:

 jq -r '(.[0] | keys_unsorted) as $keys | ([$keys] + map([.[ $keys[] ]])) [] | @csv' 

Les détails

De côté

La description des détails est délicate car jq est orienté stream, ce qui signifie qu’il fonctionne sur une séquence de données JSON plutôt que sur une valeur unique. Le stream JSON d’entrée est converti en un type interne qui est transmis via les filtres, puis codé dans un stream de sortie à la fin du programme. Le type interne n’est pas modélisé par JSON et n’existe pas en tant que type nommé. Cela est plus facile à démontrer en examinant la sortie d’un index nu ( .[] ) Ou de l’opérateur virgule (en l’examinant directement, on peut le faire avec un débogueur, mais en termes de types de données internes de jq plutôt que de types conceptuels) derrière JSON).

 $ jq -c '. []' <<< '["a", "b"]'
 "une"
 "b"
 $ jq -cn '"a", "b"'
 "une"
 "b"

Notez que la sortie n'est pas un tableau (qui serait ["a", "b"] ). La sortie compacte (option -c ) montre que chaque élément du tableau (ou argument du filtre) devient un object distinct dans la sortie (chacun se trouvant sur une ligne distincte).

Un stream est comme un seq-JSON , mais utilise newlines plutôt que RS comme séparateur de sortie lorsqu'il est encodé. Par conséquent, ce type interne est désigné par le terme générique "séquence" dans cette réponse, "stream" étant réservé à l'entrée et à la sortie codées.

Construire le filtre

Les clés du premier object peuvent être extraites avec:

 .[0] | keys_unsorted 

Les clés seront généralement conservées dans leur ordre d'origine, mais la préservation de l'ordre exact n'est pas garantie. Par conséquent, ils devront être utilisés pour indexer les objects afin d'obtenir les valeurs dans le même ordre. Cela empêchera également que les valeurs soient dans les mauvaises colonnes si certains objects ont un ordre de clé différent.

Pour générer les clés en tant que première ligne et les rendre disponibles pour l'indexation, elles sont stockées dans une variable. L'étape suivante du pipeline fait alors référence à cette variable et utilise l'opérateur de virgule pour append l'en-tête au stream de sortie.

 (.[0] | keys_unsorted) as $keys | $keys, ... 

L'expression après la virgule est un peu impliquée. L'opérateur d'index sur un object peut prendre une séquence de chaînes (par exemple "name", "value" ), en renvoyant une séquence de valeurs de propriété pour ces chaînes. $keys est un tableau, pas une séquence, donc [] est appliqué pour le convertir en une séquence,

 $keys[] 

qui peut alors être passé à .[]

 .[ $keys[] ] 

Cela produit également une séquence, le constructeur de tableau est donc utilisé pour le convertir en tableau.

 [.[ $keys[] ]] 

Cette expression doit être appliquée à un seul object. map() est utilisé pour l'appliquer à tous les objects du tableau externe:

 map([.[ $keys[] ]]) 

Enfin, pour cette étape, celle-ci est convertie en une séquence afin que chaque élément devienne une ligne distincte dans la sortie.

 map([.[ $keys[] ]])[] 

Pourquoi regrouper la séquence dans un tableau de la map uniquement pour la dégrouper à l'extérieur? map produit un tableau; .[ $keys[] ] produit une séquence. Appliquer map à la séquence à partir de .[ $keys[] ] produirait un tableau de séquences de valeurs, mais comme les séquences ne sont pas de type JSON, vous obtenez à la place un tableau aplati contenant toutes les valeurs.

 ["NSW","AU","state","New South Wales","AB","CA","province","Alberta","ABD","GB","council area","Aberdeenshire","AK","US","state","Alaska"] 

Les valeurs de chaque object doivent être séparées afin qu'elles deviennent des lignes distinctes dans la sortie finale.

Enfin, la séquence est passée à @csv formateur.

Alterner

Les articles peuvent être séparés tardivement plutôt que tôt. Au lieu d'utiliser l'opérateur virgule pour obtenir une séquence (en passant une séquence en tant qu'opérande droite), la séquence d'en-tête ( $keys ) peut être encapsulée dans un tableau et + pour append le tableau de valeurs. Cela doit encore être converti en une séquence avant d'être transmis à @csv .

Le filtre suivant est légèrement différent en ce sens qu’il garantit que chaque valeur est convertie en chaîne. (Note: utilisez jq 1.5+)

 # For an array of many objects jq -f filter.jq (file) # For many objects (not within array) jq -s -f filter.jq (file) 

Filtre: filter.jq

 def tocsv($x): $x |(map(keys) |add |unique |sort ) as $cols |map(. as $row |$cols |map($row[.]|tossortingng) ) as $rows |$cols,$rows[] | @csv; tocsv(.) 

J’ai créé une fonction qui génère un tableau d’objects ou de tableaux vers CSV avec les en-têtes. Les colonnes seraient dans l’ordre des en-têtes.

 def to_csv($headers): def _object_to_csv: ($headers | @csv), (.[] | [.[$headers[]]] | @csv); def _array_to_csv: ($headers | @csv), (.[][:$headers|length] | @csv); if .[0]|type == "object" then _object_to_csv else _array_to_csv end; 

Donc, vous pouvez l’utiliser comme ça:

 to_csv([ "code", "name", "level", "country" ]) 

Cette variante du programme de Santiago est également sûre mais garantit que les noms de clé dans le premier object sont utilisés comme premiers en-têtes de colonne, dans le même ordre qu’ils apparaissent dans cet object:

 def tocsv: if length == 0 then empty else (.[0] | keys_unsorted) as $keys | (map(keys) | add | unique) as $allkeys | ($keys + ($allkeys - $keys)) as $cols | ($cols, (.[] as $row | $cols | map($row[.]))) | @csv end ; tocsv