Comment convertir un JSON simple arbitraire en CSV à l'aide de jq?


105

En utilisant jq , comment le codage JSON arbitraire d'un tableau d'objets 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 restriction qu'il s'agit d'un tableau d'objets avec des propriétés scalaires (pas de deep / complex / sous-objets, car les aplatir est une autre question). Le résultat doit contenir une ligne d'en-tête donnant les noms de champ. La préférence sera donnée aux réponses qui préservent l'ordre des champs du premier objet, mais ce n'est pas une exigence. Les résultats peuvent entourer toutes les cellules avec des guillemets doubles, ou ne contenir que celles qui nécessitent des guillemets (par exemple «a, b»).

Exemples

  1. Contribution:

    [
        {"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. Contribution:

    [
        {"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"

Plus de trois ans plus tard ... un générique json2csvest à stackoverflow.com/questions/57242240/…
pic le

Réponses:


159

Tout d'abord, obtenez un tableau contenant tous les différents noms de propriétés d'objet dans votre entrée de tableau d'objets. Ce seront les colonnes de votre CSV:

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

Ensuite, pour chaque objet de l'entrée du tableau d'objets, mappez les noms de colonne que vous avez obtenus aux propriétés correspondantes de l'objet. Ce seront les lignes de votre CSV.

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

Enfin, placez les noms de colonnes avant les lignes, en tant qu'en-tête pour le CSV, et transmettez le flux de lignes résultant au @csvfiltre.

$cols, $rows[] | @csv

Tous ensemble maintenant. N'oubliez pas d'utiliser l' -rindicateur 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'

6
C'est bien que votre solution capture tous les noms de propriété de toutes les lignes, plutôt que seulement la première. Je me demande cependant quelles en sont les conséquences sur les performances pour les très gros documents. PS Si vous le souhaitez, vous pouvez vous débarrasser de l' $rowsaffectation de variable en l'incrustant simplement:(map(keys) | add | unique) as $cols | $cols, map(. as $row | $cols | map($row[.]))[] | @csv
Jordan Running

9
Merci, Jordan! Je suis conscient que $rowscela ne doit pas être attribué à une variable; Je pensais juste que l'assigner à une variable rendait l'explication plus agréable.

3
envisagez de convertir la valeur de ligne | string au cas où il y aurait des tableaux ou des cartes imbriqués.
TJR

Bonne suggestion, @TJR. Peut-être que s'il y a des structures imbriquées, jq devrait les récurer et transformer leurs valeurs en colonnes également
LS

En quoi cela différerait-il si le JSON était dans un fichier et que vous vouliez filtrer certaines données spécifiques au format CSV?
Neo

91

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é

Décrire les détails est délicat car jq est orienté flux, ce qui signifie qu'il fonctionne sur une séquence de données JSON, plutôt que sur une seule valeur. Le flux JSON d'entrée est converti en un type interne qui est passé à travers les filtres, puis codé dans un flux 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é. Il est plus facile de le démontrer en examinant la sortie d'un index nu ( .[]) ou de l'opérateur virgule (l'examiner directement pourrait être fait avec un débogueur, mais ce serait en termes de types de données internes de jq, plutôt que de types de données 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 (ce qui serait ["a", "b"]). La sortie compacte (l' -coption) montre que chaque élément du tableau (ou argument du ,filtre) devient un objet distinct dans la sortie (chacun est sur une ligne distincte).

Un flux est comme un JSON-seq , mais utilise des nouvelles lignes 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, «flux» étant réservé pour l'entrée et la sortie codées.

Construire le filtre

Les clés du premier objet 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 objets pour obtenir les valeurs dans le même ordre. Cela empêchera également les valeurs d'être dans les mauvaises colonnes si certains objets ont un ordre de clé différent.

Pour afficher les clés dans la 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 virgule pour ajouter l'en-tête au flux de sortie.

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

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

$keys[]

qui peut ensuite être passé à .[]

.[ $keys[] ]

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

[.[ $keys[] ]]

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

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

Enfin, pour cette étape, cela est converti 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 au sein du mapseul pour la dégrouper à l'extérieur? mapproduit un tableau; .[ $keys[] ]produit une séquence. Appliquer mapà la séquence from .[ $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 objet doivent être conservées séparément, afin qu'elles deviennent des lignes distinctes dans la sortie finale.

Enfin, la séquence est passée par le @csvformateur.

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 comme opérande de droite), la séquence d'en-tête ( $keys) peut être enveloppée dans un tableau et +utilisée pour ajouter le tableau de valeurs. Celui-ci doit encore être converti en séquence avant d'être transmis à @csv.


3
Pouvez-vous utiliser à la keys_unsortedplace de keyspour conserver l'ordre des clés du premier objet?
Jordan Running

2
@outis - Le préambule sur les flux est quelque peu inexact. Le fait est que les filtres jq sont orientés flux. Autrement dit, n'importe quel filtre peut accepter un flux d'entités JSON, et certains filtres peuvent produire un flux de valeurs. Il n'y a pas de "nouvelle ligne" ou tout autre séparateur entre les éléments d'un flux - ce n'est que lorsqu'ils sont imprimés qu'un séparateur est introduit. Pour voir par vous-même, essayez: jq -n -c 'réduire ("a", "b") comme $ s ("";. + $ S)'
pic

2
@peak - veuillez accepter ceci comme réponse, c'est de loin le plus complet et le plus complet
btk

@btk - Je n'ai pas posé la question et je ne peux donc pas l'accepter.
pic le

1
@Wyatt: regardez de plus près vos données et l'exemple d'entrée. La question concerne un tableau d'objets, pas un seul objet. Essayez [{"a":1,"b":2,"c":3}].
sortie

6

J'ai créé une fonction qui génère un tableau d'objets ou de tableaux en csv avec des 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;

Vous pouvez donc l'utiliser comme ceci:

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

6

Le filtre suivant est légèrement différent en ce qu'il garantira que chaque valeur est convertie en chaîne. (Remarque: 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[.]|tostring)
    ) as $rows
    |$cols,$rows[]
    | @csv;

tocsv(.)

1
Cela fonctionne bien pour le JSON simple, mais qu'en est-il du JSON avec des propriétés imbriquées qui descendent de plusieurs niveaux?
Amir

Cela trie bien sûr les clés. De même, la sortie de uniqueest triée de toute façon, elle unique|sortpeut donc être simplifiée en unique.
pic le

1
@TJR Lors de l'utilisation de ce filtre, il est obligatoire d'activer la sortie brute à l'aide de l' -roption. Sinon, tous les guillemets "deviennent extra-échappés, ce qui n'est pas un CSV valide.
tosh

Amir: les propriétés imbriquées ne correspondent pas au CSV.
chrishmorris

2

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

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
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.