Wie konvertiere ich beliebigen einfachen JSON mit jq in CSV?

104

Wie kann mit jq eine beliebige JSON-Codierung eines Arrays flacher Objekte in CSV konvertiert werden?

Auf dieser Website gibt es zahlreiche Fragen und Antworten, die sich auf bestimmte Datenmodelle beziehen, die die Felder hart codieren. Die Antworten auf diese Frage sollten jedoch bei jedem JSON funktionieren, mit der einzigen Einschränkung, dass es sich um ein Array von Objekten mit skalaren Eigenschaften handelt (keine tiefen / komplexen / Unterobjekte, da das Abflachen dieser eine andere Frage ist). Das Ergebnis sollte eine Kopfzeile mit den Feldnamen enthalten. Antworten, die die Feldreihenfolge des ersten Objekts beibehalten, werden bevorzugt, dies ist jedoch keine Voraussetzung. Die Ergebnisse können alle Zellen in doppelte Anführungszeichen oder nur diejenigen einschließen, die in Anführungszeichen gesetzt werden müssen (z. B. 'a, b').

Beispiele

  1. Eingang:

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

    Mögliche Ausgabe:

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

    Mögliche Ausgabe:

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

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

    Mögliche Ausgabe:

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

    Mögliche Ausgabe:

    "name","value","level"
    "bang","!","0"
    "letters","a,b,c","0"
    "letters","x,y,z","1"
    "bang","""!""","1"
outis
quelle
Über drei Jahre später ... ist ein Generikum json2csvunter stackoverflow.com/questions/57242240/…
Peak

Antworten:

157

Rufen Sie zunächst ein Array ab, das alle verschiedenen Objekteigenschaftsnamen in Ihrer Objektarray-Eingabe enthält. Dies sind die Spalten Ihrer CSV:

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

Ordnen Sie dann für jedes Objekt in der Objektarray-Eingabe die erhaltenen Spaltennamen den entsprechenden Eigenschaften im Objekt zu. Dies sind die Zeilen Ihrer CSV.

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

Fügen Sie abschließend die Spaltennamen als Überschrift für die CSV vor die Zeilen ein und übergeben Sie den resultierenden Zeilenstrom an den @csvFilter.

$cols, $rows[] | @csv

Jetzt alle zusammen. Denken Sie daran, das -rFlag zu verwenden, um das Ergebnis als Rohzeichenfolge zu erhalten:

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

quelle
6
Es ist schön, dass Ihre Lösung alle Eigenschaftsnamen aus allen Zeilen erfasst und nicht nur die ersten. Ich frage mich jedoch, welche Auswirkungen dies auf die Leistung bei sehr großen Dokumenten hat. PS Wenn Sie möchten, können Sie die $rowsVariablenzuweisung (map(keys) | add | unique) as $cols | $cols, map(. as $row | $cols | map($row[.]))[] | @csv
Jordan Running
9
Danke, Jordan! Mir ist bewusst, dass $rowsdies keiner Variablen zugeordnet werden muss; Ich dachte nur, die Zuordnung zu einer Variablen würde die Erklärung schöner machen.
3
Konvertieren Sie den Zeilenwert | Zeichenfolge für den Fall, dass verschachtelte Arrays oder Maps vorhanden sind.
TJR
Guter Vorschlag, @TJR. Wenn es verschachtelte Strukturen gibt, sollte jq möglicherweise in sie zurückgreifen und ihre Werte auch in Spalten
LS
Wie würde sich dies unterscheiden, wenn sich der JSON in einer Datei befindet und Sie bestimmte Daten in CSV herausfiltern möchten?
Neo
91

Die Dünne

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

oder:

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

Die Details

Beiseite

Das Beschreiben der Details ist schwierig, da jq streamorientiert ist, was bedeutet, dass es mit einer Folge von JSON-Daten und nicht mit einem einzelnen Wert arbeitet. Der Eingabe-JSON-Stream wird in einen internen Typ konvertiert, der durch die Filter geleitet und am Ende des Programms in einem Ausgabestream codiert wird. Der interne Typ wird nicht von JSON modelliert und existiert nicht als benannter Typ. Dies lässt sich am einfachsten anhand der Ausgabe eines Bare-Index ( .[]) oder des Komma-Operators demonstrieren (die direkte Überprüfung könnte mit einem Debugger erfolgen, dies würde sich jedoch eher auf die internen Datentypen von jq als auf die konzeptionellen Datentypen hinter JSON beziehen). .

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

Beachten Sie, dass die Ausgabe kein Array ist (was wäre ["a", "b"]). Die kompakte Ausgabe (die -cOption) zeigt, dass jedes Array-Element (oder Argument für den ,Filter) zu einem separaten Objekt in der Ausgabe wird (jedes befindet sich in einer separaten Zeile).

Ein Stream ähnelt einer JSON- Sequenz, verwendet jedoch bei der Codierung Zeilenumbrüche anstelle von RS als Ausgabetrennzeichen. Folglich wird dieser interne Typ in dieser Antwort mit dem Oberbegriff "Sequenz" bezeichnet, wobei "Stream" für die codierte Eingabe und Ausgabe reserviert ist.

Filter konstruieren

Die Schlüssel des ersten Objekts können extrahiert werden mit:

.[0] | keys_unsorted

Die Schlüssel werden im Allgemeinen in ihrer ursprünglichen Reihenfolge aufbewahrt, die genaue Reihenfolge wird jedoch nicht garantiert. Folglich müssen sie zum Indizieren der Objekte verwendet werden, um die Werte in derselben Reihenfolge zu erhalten. Dadurch wird auch verhindert, dass sich Werte in den falschen Spalten befinden, wenn einige Objekte eine andere Schlüsselreihenfolge haben.

Damit beide die Schlüssel als erste Zeile ausgeben und für die Indizierung verfügbar machen, werden sie in einer Variablen gespeichert. Die nächste Stufe der Pipeline verweist dann auf diese Variable und verwendet den Kommaoperator, um den Header dem Ausgabestream voranzustellen.

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

Der Ausdruck nach dem Komma ist ein wenig kompliziert. Der Indexoperator für ein Objekt kann eine Folge von Zeichenfolgen (z. B. "name", "value") annehmen und eine Folge von Eigenschaftswerten für diese Zeichenfolgen zurückgeben. $keysist ein Array, keine Sequenz, wird also []angewendet, um es in eine Sequenz zu konvertieren.

$keys[]

die dann weitergegeben werden kann .[]

.[ $keys[] ]

Auch dies erzeugt eine Sequenz, sodass der Array-Konstruktor verwendet wird, um sie in ein Array zu konvertieren.

[.[ $keys[] ]]

Dieser Ausdruck soll auf ein einzelnes Objekt angewendet werden. map()wird verwendet, um es auf alle Objekte im äußeren Array anzuwenden:

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

Zuletzt wird dies für diese Phase in eine Sequenz konvertiert, sodass jedes Element zu einer separaten Zeile in der Ausgabe wird.

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

Warum die Sequenz in einem Array bündeln, mapum sie nur außerhalb zu entbündeln? maperzeugt ein Array; .[ $keys[] ]erzeugt eine Sequenz. Das Anwenden mapauf die Sequenz von .[ $keys[] ]würde ein Array von Wertesequenzen erzeugen. Da Sequenzen jedoch kein JSON-Typ sind, erhalten Sie stattdessen ein abgeflachtes Array, das alle Werte enthält.

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

Die Werte von jedem Objekt müssen getrennt gehalten werden, damit sie in der endgültigen Ausgabe zu getrennten Zeilen werden.

Schließlich wird die Sequenz durch den @csvFormatierer geleitet.

Wechseln

Die Elemente können eher spät als früh getrennt werden. Anstatt den Komma-Operator zum Abrufen einer Sequenz zu verwenden (indem eine Sequenz als rechter Operand übergeben wird), kann die Header-Sequenz ( $keys) in ein Array eingeschlossen und +zum Anhängen des Wertearrays verwendet werden. Dies muss noch in eine Sequenz konvertiert werden, bevor es an übergeben wird @csv.

outis
quelle
3
Können Sie keys_unsortedanstelle von verwenden keys, um die Schlüsselreihenfolge vom ersten Objekt beizubehalten?
Jordan läuft
2
@outis - Die Präambel über Streams ist etwas ungenau. Die einfache Tatsache ist, dass jq-Filter stromorientiert sind. Das heißt, jeder Filter kann einen Strom von JSON-Entitäten akzeptieren, und einige Filter können einen Strom von Werten erzeugen. Es gibt keine "neue Zeile" oder ein anderes Trennzeichen zwischen den Elementen in einem Stream - nur wenn sie gedruckt werden, wird ein Trennzeichen eingeführt. Um sich selbst davon zu überzeugen, versuchen Sie: jq -n -c 'reduzieren ("a", "b") als $ s ("";. + $ S)'
Peak
2
@peak - bitte akzeptieren Sie dies als Antwort, es ist bei weitem am vollständigsten und umfassendsten
btk
@btk - Ich habe die Frage nicht gestellt und kann sie daher nicht akzeptieren.
Höhepunkt
1
@ Wyatt: Sehen Sie sich Ihre Daten und die Beispieleingabe genauer an. Die Frage bezieht sich auf ein Array von Objekten, nicht auf ein einzelnes Objekt. Versuchen Sie es [{"a":1,"b":2,"c":3}].
Outis
6

Der folgende Filter unterscheidet sich geringfügig darin, dass sichergestellt wird, dass jeder Wert in eine Zeichenfolge konvertiert wird. (Hinweis: Verwenden Sie 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)

Filter: 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(.)
TJR
quelle
1
Dies funktioniert gut für einfaches JSON, aber was ist mit JSON mit verschachtelten Eigenschaften, die viele Ebenen hinuntergehen?
Amir
Dies sortiert natürlich die Schlüssel. Auch die Ausgabe von uniqueist sowieso sortiert, unique|sortkann also vereinfacht werden unique.
Peak
1
@TJR Bei Verwendung dieses Filters muss die Rohausgabe mit der -rOption aktiviert werden . Andernfalls werden alle Anführungszeichen "extra maskiert, was keine gültige CSV ist.
tosh
Amir: Verschachtelte Eigenschaften werden nicht der CSV zugeordnet.
Chrismorris
5

Ich habe eine Funktion erstellt, die ein Array von Objekten oder Arrays mit Headern an csv ausgibt. Die Spalten würden in der Reihenfolge der Überschriften sein.

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;

Sie könnten es also so verwenden:

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

Diese Variante des Santiago-Programms ist ebenfalls sicher, stellt jedoch sicher, dass die Schlüsselnamen im ersten Objekt als erste Spaltenüberschriften in derselben Reihenfolge verwendet werden, in der sie in diesem Objekt angezeigt werden:

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
Gipfel
quelle