web-dev-qa-db-fra.com

Comment convertir un JSON simple arbitraire en CSV en utilisant jq?

Avec jq , comment un code JSON arbitraire encodant un tableau d'objets peu profonds peut-il être converti au format CSV?

Il y a beaucoup de questions-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'agisse d'un tableau d'objets avec des propriétés scalaires (pas de profondeur/complexe/sous-objets, car leur aplatissement est une autre question). Le résultat doit contenir une ligne d’en-tête indiquant les noms de champs. 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 inclure toutes les cellules avec des guillemets, ou seulement celles qui nécessitent une citation (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"
    
76
outis

Commencez par obtenir un tableau contenant tous les noms de propriétés d'objet différents dans votre entrée de tableau d'objet. Ce seront les colonnes de votre CSV:

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

Ensuite, pour chaque objet de l'entrée de tableau d'objets, mappez les noms de colonnes obtenus avec les 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 colonne avant les lignes, en tant qu’en-tête du fichier CSV, puis transmettez le flux de lignes résultant à la commande @csv filtre.

$cols, $rows[] | @csv

Tous ensemble maintenant. N'oubliez pas d'utiliser le -r drapeau 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'
121
user3899165

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 valeur unique. Le flux JSON d'entrée est converti en un type interne qui est passé à travers les filtres, puis encodé 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é. La meilleure façon de le démontrer consiste à examiner la sortie d’un index nue (.[]) Ou de l’opérateur de virgule (l’examiner directement pourrait s’effectuer avec un débogueur, mais ce serait en termes de types de données internes de jq, plutôt que le type de données utilisé. types de données conceptuels derrière JSON).

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

Notez que la sortie n'est pas un tableau (ce qui serait ["a", "b"]). La sortie compacte (l'option -c) Indique que chaque élément de tableau (ou argument du filtre ,) Devient un objet séparé dans la sortie (chacun se trouvant sur une ligne distincte).

Un flux ressemble à un JSON-seq , mais utilise des nouvelles lignes plutôt que RS en tant que séparateur de sortie lorsqu'il est codé. 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é à 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 sont 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 que les clés soient toutes deux sorties en tant que première ligne et rendues disponibles pour l'indexation, elles sont stockées dans une variable. L'étape suivante du pipeline référence ensuite cette variable et utilise l'opérateur de 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 impliquée. L'opérateur d'index sur un objet peut prendre une séquence de chaînes (par exemple, "name", "value"), En renvoyant une séquence de valeurs de propriétés pour ces chaînes. $keys Est un tableau et non une séquence. Par conséquent, [] Est appliqué pour le convertir en séquence.

$keys[]

qui peut ensuite être passé à .[]

.[ $keys[] ]

Cela aussi produit une séquence, le constructeur du tableau est donc utilisé pour la 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, cette étape 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 à l'intérieur de map uniquement pour la dégrouper à l'extérieur? map produit un tableau; .[ $keys[] ] Produit une séquence. Appliquer map à la séquence de .[ $keys[] ] Produirait un tableau de séquences de valeurs, mais comme les séquences ne sont pas de type JSON, vous obtenez donc 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 séparées, de sorte qu'elles deviennent des lignes distinctes dans la sortie finale.

Finalement, la séquence est passée à travers le formateur @csv.

Alterner

Les articles peuvent être séparés plus tard que tard. Au lieu d'utiliser l'opérateur virgule pour obtenir une séquence (en passant une séquence en tant qu'opérande correct), la séquence d'en-tête ($keys) Peut être encapsulée dans un tableau et + Être ajouté pour l'ajouter. des valeurs. Cela doit encore être converti en une séquence avant d'être passé à @csv.

59
outis

Le filtre suivant est légèrement différent en ce sens qu'il garantira 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[.]|tostring)
    ) as $rows
    |$cols,$rows[]
    | @csv;

tocsv(.)
4
TJR

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

Pour que vous puissiez l'utiliser comme suit:

to_csv([ "code", "name", "level", "country" ])
3
Jeff Mercado

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
1
peak