Jak przekonwertować dowolny prosty JSON na CSV za pomocą jq?

105

W jaki sposób za pomocą jq można przekonwertować dowolne kodowanie JSON tablicy płytkich obiektów na format CSV?

W tej witrynie jest wiele pytań i odpowiedzi, które obejmują określone modele danych, które trwale kodują pola, ale odpowiedzi na to pytanie powinny działać dla dowolnego formatu JSON, z jedynym ograniczeniem, że jest to tablica obiektów o właściwościach skalarnych (bez głębokich / złożonych / podobiekty, ponieważ spłaszczanie to inna kwestia). Wynik powinien zawierać wiersz nagłówka z nazwami pól. Preferowane będą odpowiedzi, które zachowują kolejność pól pierwszego obiektu, ale nie jest to wymagane. Wyniki mogą obejmować wszystkie komórki w cudzysłów lub tylko te, które wymagają cudzysłowu (np. „A, b”).

Przykłady

  1. Wejście:

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

    Możliwe wyjście:

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

    Możliwe wyjście:

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

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

    Możliwe wyjście:

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

    Możliwe wyjście:

    "name","value","level"
    "bang","!","0"
    "letters","a,b,c","0"
    "letters","x,y,z","1"
    "bang","""!""","1"
    
poza
źródło
Ponad trzy lata później ... generyczny json2csvjest na stackoverflow.com/questions/57242240/ ...
szczyt

Odpowiedzi:

159

Najpierw uzyskaj tablicę zawierającą wszystkie różne nazwy właściwości obiektów w danych wejściowych tablicy obiektów. To będą kolumny Twojego pliku CSV:

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

Następnie dla każdego obiektu w danych wejściowych tablicy obiektów przypisz nazwy kolumn, które uzyskasz, do odpowiednich właściwości w obiekcie. To będą rzędy Twojego pliku CSV.

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

Na koniec umieść nazwy kolumn przed wierszami jako nagłówek pliku CSV i przekaż wynikowy strumień wierszy do @csvfiltra.

$cols, $rows[] | @csv

Teraz wszyscy razem. Pamiętaj, aby użyć -rflagi, aby otrzymać wynik jako nieprzetworzony ciąg:

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

źródło
6
Fajnie, że Twoje rozwiązanie przechwytuje wszystkie nazwy właściwości ze wszystkich wierszy, a nie tylko pierwszy. Zastanawiam się jednak, jakie ma to konsekwencje dla wydajności w przypadku bardzo dużych dokumentów. PS Jeśli chcesz, możesz pozbyć się $rowsprzypisania zmiennej, po prostu wstawiając ją:(map(keys) | add | unique) as $cols | $cols, map(. as $row | $cols | map($row[.]))[] | @csv
Jordan Running
9
Dzięki, Jordan! Mam świadomość, że $rowsnie musi być przypisane do zmiennej; Po prostu pomyślałem, że przypisanie go do zmiennej sprawiło, że wyjaśnienie było przyjemniejsze.
3
rozważ konwersję wartości wiersza | ciąg znaków w przypadku zagnieżdżonych tablic lub map.
TJR
Dobra sugestia, @TJR. Może jeśli istnieją zagnieżdżone struktury, jq powinno się do nich powtórzyć i zamienić ich wartości również w kolumny
LS
Czym by się to różniło, gdyby JSON znajdował się w pliku i chciałbyś odfiltrować określone dane do CSV?
Neo
91

Chudy

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

lub:

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

Szczegóły

Na bok

Opisanie szczegółów jest trudne, ponieważ jq jest zorientowane strumieniowo, co oznacza, że ​​działa na sekwencji danych JSON, a nie na pojedynczej wartości. Strumień wejściowy JSON jest konwertowany na jakiś typ wewnętrzny, który jest przepuszczany przez filtry, a następnie kodowany w strumieniu wyjściowym na końcu programu. Typ wewnętrzny nie jest modelowany przez JSON i nie istnieje jako nazwany typ. Najłatwiej to wykazać, badając dane wyjściowe samego indeksu ( .[]) lub operatora przecinka (zbadanie tego bezpośrednio można wykonać za pomocą debugera, ale byłoby to pod względem wewnętrznych typów danych jq, a nie pojęciowych typów danych za JSON) .

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

Zauważ, że dane wyjściowe nie są tablicą (która byłaby ["a", "b"]). Kompaktowe dane wyjściowe ( -copcja) pokazują, że każdy element tablicy (lub argument ,filtru) staje się oddzielnym obiektem w wyniku (każdy znajduje się w osobnym wierszu).

Strumień jest podobny do sekwencji JSON , ale używa znaków nowej linii zamiast RS jako separatora danych wyjściowych po zakodowaniu. W konsekwencji do tego typu wewnętrznego w tej odpowiedzi odnosi się ogólny termin „sekwencja”, przy czym „strumień” jest zarezerwowany dla zakodowanego wejścia i wyjścia.

Konstruowanie filtra

Klucze pierwszego obiektu można wyodrębnić za pomocą:

.[0] | keys_unsorted

Klucze będą zazwyczaj przechowywane w oryginalnej kolejności, ale zachowanie dokładnej kolejności nie jest gwarantowane. W związku z tym będą musiały zostać użyte do indeksowania obiektów, aby uzyskać wartości w tej samej kolejności. Zapobiegnie to również umieszczaniu wartości w niewłaściwych kolumnach, jeśli niektóre obiekty mają inną kolejność kluczy.

Aby oba wyprowadzić klucze jako pierwszy wiersz i udostępnić je do indeksowania, są one przechowywane w zmiennej. Następny etap potoku odwołuje się następnie do tej zmiennej i używa operatora przecinka, aby dołączyć nagłówek do strumienia wyjściowego.

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

Wyrażenie po przecinku jest trochę zawiłe. Operator indeksu na obiekcie może przyjąć sekwencję ciągów (np. "name", "value"), Zwracając sekwencję wartości właściwości dla tych ciągów. $keysjest tablicą, a nie sekwencją, więc []jest stosowana do konwersji na sekwencję,

$keys[]

które można następnie przekazać .[]

.[ $keys[] ]

To także tworzy sekwencję, więc konstruktor tablicy jest używany do konwersji jej na tablicę.

[.[ $keys[] ]]

To wyrażenie ma być zastosowane do pojedynczego obiektu. map()służy do zastosowania go do wszystkich obiektów w tablicy zewnętrznej:

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

Na koniec na tym etapie jest to konwertowane na sekwencję, dzięki czemu każdy element staje się oddzielnym wierszem w wyniku.

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

Po co pakować sekwencję w tablicę maptylko po to, aby ją uwolnić? maptworzy tablicę; .[ $keys[] ]tworzy sekwencję. Zastosowanie mapdo sekwencji z .[ $keys[] ]utworzy tablicę sekwencji wartości, ale ponieważ sekwencje nie są typu JSON, więc zamiast tego otrzymasz spłaszczoną tablicę zawierającą wszystkie wartości.

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

Wartości z każdego obiektu należy przechowywać oddzielnie, aby w ostatecznym wyniku stały się oddzielnymi wierszami.

Na koniec sekwencja jest przekazywana przez @csvformater.

Alternatywny

Elementy można oddzielić raczej późno niż wcześnie. Zamiast używać operatora przecinka, aby uzyskać sekwencję (przekazując sekwencję jako prawy operand), sekwencja nagłówka ( $keys) może zostać umieszczona w tablicy i +użyta do dołączenia tablicy wartości. To nadal musi zostać przekonwertowane na sekwencję przed przekazaniem do @csv.

poza
źródło
3
Czy możesz użyć keys_unsortedzamiast, keysaby zachować kolejność kluczy od pierwszego obiektu?
Jordan Running
2
@outis - preambuła dotycząca strumieni jest nieco niedokładna. Prosty fakt jest taki, że filtry jq są zorientowane strumieniowo. Oznacza to, że każdy filtr może akceptować strumień jednostek JSON, a niektóre filtry mogą generować strumień wartości. Nie ma „nowej linii” ani żadnego innego separatora między elementami w strumieniu - wprowadzany jest separator tylko podczas ich drukowania. Aby się o tym przekonać, spróbuj: jq -n -c 'redukuj ("a", "b") as $ s ("";. + $ S)'
szczyt
2
@peak - zaakceptuj to jako odpowiedź, jest zdecydowanie najbardziej kompletna i wyczerpująca
btk
@btk - nie zadałem pytania i dlatego nie mogę go zaakceptować.
szczyt
1
@Wyatt: przyjrzyj się bliżej swoim danym i przykładowym danym wejściowym. Pytanie dotyczy tablicy obiektów, a nie pojedynczego obiektu. Spróbuj [{"a":1,"b":2,"c":3}].
wyjazd
6

Stworzyłem funkcję, która wyprowadza tablicę obiektów lub tablic do csv z nagłówkami. Kolumny byłyby w kolejności nagłówków.

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;

Więc możesz go użyć w ten sposób:

to_csv([ "code", "name", "level", "country" ])
Jeff Mercado
źródło
6

Poniższy filtr jest nieco inny, ponieważ zapewnia konwersję każdej wartości na ciąg. (Uwaga: użyj 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)

Filtr: 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
źródło
1
Działa to dobrze w przypadku prostego JSON, ale co z JSON z zagnieżdżonymi właściwościami, które spadają o wiele poziomów?
Amir
To oczywiście sortuje klucze. Również dane wyjściowe uniquesą sortowane, więc unique|sortmożna je uprościć do unique.
szczyt
1
@TJR Podczas korzystania z tego filtru konieczne jest włączenie surowego wyjścia za pomocą -ropcji. W przeciwnym razie wszystkie cudzysłowy "zostaną zastąpione dodatkowymi znakami ucieczki, co nie jest prawidłowym plikiem CSV.
tosh
Amir: zagnieżdżone właściwości nie są mapowane do CSV.
chrishmorris
2

Ten wariant programu Santiago jest również bezpieczny, ale zapewnia, że ​​nazwy kluczy w pierwszym obiekcie są używane jako nagłówki pierwszej kolumny, w tej samej kolejności, w jakiej pojawiają się w tym obiekcie:

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
szczyt
źródło