Come convertire un semplice JSON arbitrario in CSV usando jq?


105

Utilizzando jq , come è possibile convertire in CSV la codifica JSON arbitraria di un array di oggetti superficiali?

Ci sono molte domande e risposte su questo sito che coprono modelli di dati specifici che codificano i campi, ma le risposte a questa domanda dovrebbero funzionare con qualsiasi JSON, con l'unica restrizione che si tratta di un array di oggetti con proprietà scalari (no deep / complex / sotto-oggetti, poiché l'appiattimento di questi è un'altra domanda). Il risultato dovrebbe contenere una riga di intestazione che fornisce i nomi dei campi. Sarà data preferenza alle risposte che conservano l'ordine dei campi del primo oggetto, ma non è un requisito. I risultati possono racchiudere tutte le celle tra virgolette o racchiudere solo quelle che richiedono virgolette (ad esempio "a, b").

Esempi

  1. Ingresso:

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

    Possibile output:

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

    Possibile output:

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

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

    Possibile output:

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

    Possibile output:

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

Più di tre anni dopo ... un generico json2csvè su stackoverflow.com/questions/57242240/…
picco del

Risposte:


159

Per prima cosa, ottieni un array contenente tutti i diversi nomi di proprietà dell'oggetto nell'input dell'array di oggetti. Queste saranno le colonne del tuo CSV:

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

Quindi, per ogni oggetto nell'input dell'array di oggetti, mappare i nomi delle colonne ottenuti alle proprietà corrispondenti nell'oggetto. Quelle saranno le righe del tuo CSV.

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

Infine, inserisci i nomi delle colonne prima delle righe, come intestazione per il CSV, e passa il flusso di righe risultante al @csvfiltro.

$cols, $rows[] | @csv

Tutti insieme ora. Ricorda di utilizzare il -rflag per ottenere il risultato come stringa non elaborata:

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

6
È bello che la tua soluzione acquisisca tutti i nomi di proprietà da tutte le righe, anziché solo la prima. Tuttavia, mi chiedo quali siano le implicazioni sulle prestazioni per documenti molto grandi. PS Se vuoi, puoi sbarazzarti dell'assegnazione delle $rowsvariabili semplicemente incorporandola:(map(keys) | add | unique) as $cols | $cols, map(. as $row | $cols | map($row[.]))[] | @csv
Jordan Running

9
Grazie, Jordan! Sono consapevole che $rowsnon deve essere assegnato a una variabile; Ho solo pensato che assegnarlo a una variabile rendesse la spiegazione più piacevole.

3
considera la conversione del valore della riga | stringa nel caso in cui siano presenti array o mappe annidati.
TJR

Buon suggerimento, @TJR. Forse se ci sono strutture nidificate, jq dovrebbe ricorrere in esse e trasformare anche i loro valori in colonne
LS

In che modo sarebbe diverso se il JSON fosse in un file e volessi filtrare alcuni dati specifici in CSV?
Neo

91

Lo magro

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

o:

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

I dettagli

A parte

Descrivere i dettagli è complicato perché jq è orientato al flusso, il che significa che opera su una sequenza di dati JSON, piuttosto che su un singolo valore. Il flusso JSON di input viene convertito in un tipo interno che viene passato attraverso i filtri, quindi codificato in un flusso di output alla fine del programma. Il tipo interno non è modellato da JSON e non esiste come tipo denominato. È più facilmente dimostrato esaminando l'output di un semplice index ( .[]) o dell'operatore virgola (esaminarlo direttamente potrebbe essere fatto con un debugger, ma ciò sarebbe in termini di tipi di dati interni di jq, piuttosto che i tipi di dati concettuali dietro JSON) .

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

Nota che l'output non è un array (che sarebbe ["a", "b"]). L'output compatto (l' -copzione) mostra che ogni elemento dell'array (o argomento del ,filtro) diventa un oggetto separato nell'output (ognuno si trova su una riga separata).

Un flusso è come un JSON-seq , ma utilizza le nuove righe anziché RS come separatore di output quando codificato. Di conseguenza, questo tipo interno viene indicato con il termine generico "sequenza" in questa risposta, con "flusso" riservato per l'input e l'output codificati.

Costruire il filtro

Le chiavi del primo oggetto possono essere estratte con:

.[0] | keys_unsorted

Le chiavi vengono generalmente mantenute nell'ordine originale, ma non è garantito il mantenimento dell'ordine esatto. Di conseguenza, dovranno essere utilizzati per indicizzare gli oggetti per ottenere i valori nello stesso ordine. Ciò impedirà inoltre che i valori si trovino nelle colonne sbagliate se alcuni oggetti hanno un ordine di chiave diverso.

Per generare entrambe le chiavi come prima riga e renderle disponibili per l'indicizzazione, vengono memorizzate in una variabile. La fase successiva della pipeline fa quindi riferimento a questa variabile e utilizza l'operatore virgola per anteporre l'intestazione al flusso di output.

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

L'espressione dopo la virgola è un po 'complicata. L'operatore indice su un oggetto può accettare una sequenza di stringhe (ad esempio "name", "value"), restituendo una sequenza di valori di proprietà per quelle stringhe. $keysè un array, non una sequenza, quindi []viene applicato per convertirlo in una sequenza,

$keys[]

che può quindi essere passato a .[]

.[ $keys[] ]

Anche questo produce una sequenza, quindi il costruttore di array viene utilizzato per convertirlo in un array.

[.[ $keys[] ]]

Questa espressione deve essere applicata a un singolo oggetto. map()viene utilizzato per applicarlo a tutti gli oggetti nell'array esterno:

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

Infine, per questa fase, questo viene convertito in una sequenza in modo che ogni elemento diventi una riga separata nell'output.

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

Perché raggruppare la sequenza in un array all'interno mapdell'unico per separarla all'esterno? mapproduce un array; .[ $keys[] ]produce una sequenza. L'applicazione mapalla sequenza da .[ $keys[] ]produrrebbe una matrice di sequenze di valori, ma poiché le sequenze non sono di tipo JSON, si ottiene invece una matrice appiattita contenente tutti i valori.

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

I valori di ogni oggetto devono essere tenuti separati, in modo che diventino righe separate nell'output finale.

Infine, la sequenza viene passata attraverso il @csvformattatore.

Alternato

Gli elementi possono essere separati in ritardo, anziché in anticipo. Invece di utilizzare l'operatore virgola per ottenere una sequenza (passando una sequenza come operando a destra), la sequenza di intestazione ( $keys) può essere racchiusa in un array e +utilizzata per aggiungere l'array di valori. Questo deve ancora essere convertito in una sequenza prima di essere passato a @csv.


3
Puoi usare keys_unsortedinvece di keysper preservare l'ordine delle chiavi dal primo oggetto?
Jordan in esecuzione il

2
@outis - Il preambolo sugli stream è alquanto impreciso. Il semplice fatto è che i filtri jq sono orientati al flusso. Ovvero, qualsiasi filtro può accettare un flusso di entità JSON e alcuni filtri possono produrre un flusso di valori. Non c'è una "nuova riga" o qualsiasi altro separatore tra gli elementi in un flusso: è solo quando vengono stampati che viene introdotto un separatore. Per vedere di persona, prova: jq -n -c 'reduce ("a", "b") as $ s ("";. + $ S)'
picco

2
@peak - per favore accetta questa come risposta, è di gran lunga la più completa e completa
btk

@btk - Non ho posto la domanda e quindi non posso accettarla.
picco del

1
@ Wyatt: dai un'occhiata più da vicino ai tuoi dati e all'input di esempio. La domanda riguarda una serie di oggetti, non un singolo oggetto. Prova [{"a":1,"b":2,"c":3}].
uscita il

6

Ho creato una funzione che restituisce un array di oggetti o array in csv con intestazioni. Le colonne sarebbero nell'ordine delle intestazioni.

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;

Quindi potresti usarlo in questo modo:

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

6

Il seguente filtro è leggermente diverso in quanto garantisce che ogni valore venga convertito in una stringa. (Nota: usa 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)

Filtro: 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
Funziona bene per JSON semplice, ma per quanto riguarda JSON con proprietà nidificate che scendono di molti livelli?
Amir

Questo ovviamente ordina le chiavi. Anche l'output di uniqueviene ordinato comunque, quindi unique|sortpuò essere semplificato in unique.
picco del

1
@TJR Quando si utilizza questo filtro è obbligatorio attivare l'output grezzo utilizzando l' -ropzione. Altrimenti tutte le virgolette "diventano con caratteri di escape extra, il che non è un CSV valido.
tosh

Amir: le proprietà nidificate non vengono mappate a CSV.
chrishmorris

2

Anche questa variante del programma di Santiago è sicura, ma garantisce che i nomi delle chiavi nel primo oggetto vengano utilizzati come intestazioni della prima colonna, nello stesso ordine in cui appaiono in quell'oggetto:

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.