Bagaimana cara mengonversi JSON sederhana sewenang-wenang ke CSV menggunakan jq?

105

Menggunakan jq , bagaimana JSON yang menyandikan larik objek dangkal dapat dikonversi ke CSV?

Ada banyak Tanya Jawab di situs ini yang membahas model data tertentu yang melakukan hard-code pada kolom tersebut, tetapi jawaban atas pertanyaan ini akan berfungsi jika diberikan JSON apa pun, dengan satu-satunya batasan bahwa itu adalah array objek dengan properti skalar (tidak ada deep / complex / sub-objek, karena meratakan ini adalah pertanyaan lain). Hasilnya harus berisi baris tajuk yang memberi nama bidang. Preferensi akan diberikan pada jawaban yang mempertahankan urutan bidang objek pertama, tetapi ini bukan persyaratan. Hasil mungkin menyertakan semua sel dengan tanda kutip ganda, atau hanya menyertakan yang memerlukan kutipan (mis. 'A, b').

Contoh

  1. Memasukkan:

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

    Output yang memungkinkan:

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

    Output yang memungkinkan:

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

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

    Output yang memungkinkan:

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

    Output yang memungkinkan:

    "name","value","level"
    "bang","!","0"
    "letters","a,b,c","0"
    "letters","x,y,z","1"
    "bang","""!""","1"
outis
sumber
Tiga lebih tahun kemudian ... generik json2csvada di stackoverflow.com/questions/57242240/…
puncak

Jawaban:

159

Pertama, dapatkan larik yang berisi semua nama properti objek yang berbeda dalam masukan larik objek Anda. Itu akan menjadi kolom CSV Anda:

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

Kemudian, untuk setiap objek dalam input larik objek, petakan nama kolom yang Anda peroleh ke properti terkait dalam objek tersebut. Itu akan menjadi baris CSV Anda.

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

Terakhir, letakkan nama kolom sebelum baris, sebagai header untuk CSV, dan teruskan aliran baris yang dihasilkan ke @csvfilter.

$cols, $rows[] | @csv

Semua bersama Sekarang. Ingatlah untuk menggunakan -rbendera untuk mendapatkan hasil sebagai string mentah:

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

sumber
6
Sangat menyenangkan bahwa solusi Anda menangkap semua nama properti dari semua baris, bukan hanya yang pertama. Saya ingin tahu apa implikasi kinerja ini untuk dokumen yang sangat besar. PS Jika Anda mau, Anda dapat menyingkirkan $rowstugas variabel hanya dengan membuat inline:(map(keys) | add | unique) as $cols | $cols, map(. as $row | $cols | map($row[.]))[] | @csv
Jordan Menjalankan
9
Terima kasih, Jordan! Saya sadar bahwa $rowstidak harus ditugaskan ke variabel; Saya hanya berpikir menugaskannya ke variabel membuat penjelasannya lebih bagus.
3
pertimbangkan untuk mengubah nilai baris | string jika ada array atau peta bersarang.
TJR
Saran bagus, @TJR. Mungkin jika ada struktur bersarang, jq harus mengulanginya dan membuat nilainya menjadi kolom juga
LS
Apa bedanya jika JSON ada dalam file dan Anda ingin memfilter beberapa data tertentu ke CSV?
Neo
91

Si Kurus

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

atau:

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

Rinciannya

Ke samping

Mendeskripsikan detailnya rumit karena jq berorientasi pada aliran, artinya ia beroperasi pada urutan data JSON, bukan pada satu nilai. Aliran JSON masukan diubah menjadi beberapa jenis internal yang dilewatkan melalui filter, kemudian dikodekan dalam aliran keluaran di akhir program. Tipe internal tidak dimodelkan oleh JSON, dan tidak ada sebagai tipe bernama. Ini paling mudah ditunjukkan dengan memeriksa output dari indeks telanjang ( .[]) atau operator koma (memeriksanya secara langsung dapat dilakukan dengan debugger, tetapi itu akan menjadi dalam hal tipe data internal jq, daripada tipe data konseptual di belakang JSON) .

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

Perhatikan bahwa outputnya bukan array (yang akan menjadi ["a", "b"] ). Output ringkas ( -copsi) menunjukkan bahwa setiap elemen array (atau argumen ke ,filter) menjadi objek terpisah dalam output (masing-masing berada pada baris terpisah).

Aliran seperti JSON-seq , tetapi menggunakan baris baru daripada RS sebagai pemisah keluaran saat dienkode. Akibatnya, tipe internal ini dirujuk oleh istilah umum "urutan" dalam jawaban ini, dengan "aliran" dicadangkan untuk masukan dan keluaran yang dikodekan.

Membangun Filter

Kunci objek pertama dapat diekstraksi dengan:

.[0] | keys_unsorted

Biasanya kunci akan disimpan dalam urutan aslinya, tetapi urutan yang tepat tidak dijamin. Akibatnya, mereka perlu digunakan untuk mengindeks objek untuk mendapatkan nilai dalam urutan yang sama. Ini juga akan mencegah nilai berada di kolom yang salah jika beberapa objek memiliki urutan kunci yang berbeda.

Untuk mengeluarkan kedua kunci sebagai baris pertama dan membuatnya tersedia untuk pengindeksan, mereka disimpan dalam variabel. Tahap berikutnya dari pipeline kemudian mereferensikan variabel ini dan menggunakan operator koma untuk menambahkan header ke aliran keluaran.

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

Ekspresi setelah koma sedikit terlibat. Operator indeks pada suatu objek dapat mengambil urutan string (misalnya "name", "value"), mengembalikan urutan nilai properti untuk string tersebut. $keysadalah array, bukan urutan, jadi []diterapkan untuk mengubahnya menjadi urutan,

$keys[]

yang kemudian dapat diteruskan ke .[]

.[ $keys[] ]

Ini juga menghasilkan urutan, sehingga konstruktor array digunakan untuk mengubahnya menjadi array.

[.[ $keys[] ]]

Ekspresi ini akan diterapkan ke satu objek. map()digunakan untuk menerapkannya ke semua objek dalam larik terluar:

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

Terakhir untuk tahap ini, ini diubah menjadi urutan sehingga setiap item menjadi baris terpisah di keluaran.

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

Mengapa menggabungkan urutan menjadi array di dalam mapsatu - satunya untuk memisahkannya di luar? mapmenghasilkan sebuah array; .[ $keys[] ]menghasilkan urutan. Menerapkan mapurutan dari .[ $keys[] ]akan menghasilkan larik urutan nilai, tetapi karena urutan bukan tipe JSON, jadi Anda mendapatkan larik pipih yang berisi semua nilai.

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

Nilai dari setiap objek harus dipisahkan, sehingga menjadi baris terpisah pada hasil akhir.

Akhirnya, urutan dilewatkan melalui @csvpemformat.

Bergantian

Item dapat dipisahkan terlambat, bukan lebih awal. Alih-alih menggunakan operator koma untuk mendapatkan urutan (meneruskan urutan sebagai operan kanan), urutan header ( $keys) bisa dibungkus dalam array, dan +digunakan untuk menambahkan array nilai. Ini masih perlu diubah menjadi urutan sebelum diteruskan ke @csv.

outis
sumber
3
Dapatkah Anda menggunakan keys_unsortedalih-alih keysmempertahankan urutan kunci dari objek pertama?
Jordan Tayang
2
@outis - Pembukaan tentang aliran agak tidak akurat. Fakta sederhananya adalah bahwa filter jq berorientasi pada aliran. Artinya, filter apa pun dapat menerima aliran entitas JSON, dan beberapa filter dapat menghasilkan aliran nilai. Tidak ada "baris baru" atau pemisah lainnya di antara item dalam aliran - hanya saat dicetak barulah pemisah diperkenalkan. Untuk melihat sendiri, coba: jq -n -c 'reduce ("a", "b") sebagai $ s ("";. + $ S)'
puncak
2
@peak - terima ini sebagai jawabannya, sejauh ini paling lengkap dan komprehensif
btk
@btk - Saya tidak mengajukan pertanyaan dan karena itu tidak dapat menerimanya.
puncak
1
@Wyatt: lihat lebih dekat data Anda dan contoh masukan. Pertanyaannya adalah tentang larik objek, bukan satu objek. Coba [{"a":1,"b":2,"c":3}].
keluar
6

Saya membuat fungsi yang menampilkan array objek atau array ke csv dengan header. Kolom akan berada dalam urutan tajuk.

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;

Jadi Anda bisa menggunakannya seperti ini:

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

Filter berikut sedikit berbeda karena akan memastikan setiap nilai diubah menjadi string. (Catatan: gunakan 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)

Saring: 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
sumber
1
Ini berfungsi baik untuk JSON sederhana tetapi bagaimana dengan JSON dengan properti bersarang yang turun banyak level?
Amir
Ini tentu saja menyortir kuncinya. Juga keluaran dari uniquediurutkan, sehingga unique|sortdapat disederhanakan menjadi unique.
puncak
1
@TJR Saat menggunakan filter ini, wajib untuk mengaktifkan keluaran mentah menggunakan -ropsi. Jika tidak, semua tanda kutip "menjadi extra-escaped yang bukan merupakan CSV yang valid.
tosh
Amir: properti bertingkat tidak dipetakan ke CSV.
chrishmorris
2

Varian program Santiago ini juga aman tetapi memastikan bahwa nama kunci di objek pertama digunakan sebagai tajuk kolom pertama, dalam urutan yang sama seperti yang muncul di objek itu:

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
puncak
sumber