วิธีการแปลง JSON อย่างง่ายตามอำเภอใจเป็น CSV โดยใช้ jq


106

การใช้jqการเข้ารหัส JSON โดยพลการอาร์เรย์ของวัตถุตื้นจะแปลงเป็น CSV ได้อย่างไร

มี Q & As มากมายบนไซต์นี้ที่ครอบคลุมโมเดลข้อมูลเฉพาะที่ฮาร์ดโค้ดฟิลด์ แต่คำตอบสำหรับคำถามนี้ควรใช้งานได้กับ JSON ใด ๆ โดยมีข้อ จำกัด เพียงอย่างเดียวคืออาร์เรย์ของออบเจ็กต์ที่มีคุณสมบัติสเกลาร์ (ไม่มีลึก / ซับซ้อน / วัตถุย่อยเนื่องจากการทำให้แบนเป็นอีกคำถามหนึ่ง) ผลลัพธ์ควรมีแถวส่วนหัวที่ให้ชื่อเขตข้อมูล ค่ากำหนดจะได้รับสำหรับคำตอบที่คงลำดับฟิลด์ของอ็อบเจ็กต์แรกไว้ แต่ไม่ใช่ข้อกำหนด ผลลัพธ์อาจล้อมรอบเซลล์ทั้งหมดด้วยเครื่องหมายอัญประกาศคู่หรือใส่เฉพาะเซลล์ที่ต้องการการอ้างอิงเท่านั้น (เช่น 'a, b')

ตัวอย่าง

  1. อินพุต:

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

    ผลลัพธ์ที่เป็นไปได้:

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

    ผลลัพธ์ที่เป็นไปได้:

    "code","name","level","country"
    "NSW","New South Wales","state","AU"
    "AB","Alberta","province","CA"
    "ABD","Aberdeenshire","council area","GB"
    "AK","Alaska","state","US"
  2. อินพุต:

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

    ผลลัพธ์ที่เป็นไปได้:

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

    ผลลัพธ์ที่เป็นไปได้:

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

สามปีต่อมา ... คนทั่วไปjson2csvอยู่ที่stackoverflow.com/questions/57242240/…
สูงสุด

คำตอบ:


160

ขั้นแรกให้รับอาร์เรย์ที่มีชื่อคุณสมบัติของอ็อบเจ็กต์ที่แตกต่างกันทั้งหมดในอินพุตอาร์เรย์อ็อบเจ็กต์ของคุณ ซึ่งจะเป็นคอลัมน์ของ CSV ของคุณ:

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

จากนั้นสำหรับแต่ละอ็อบเจ็กต์ในอินพุตอาร์เรย์อ็อบเจ็กต์ให้แมปชื่อคอลัมน์ที่คุณได้รับกับคุณสมบัติที่เกี่ยวข้องในอ็อบเจ็กต์ ซึ่งจะเป็นแถวของไฟล์ CSV ของคุณ

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

สุดท้ายใส่ชื่อคอลัมน์ก่อนแถวเป็นส่วนหัวของ CSV และส่งสตรีมแถวผลลัพธ์ไปยัง@csvตัวกรอง

$cols, $rows[] | @csv

ด้วยกันทั้งหมด. อย่าลืมใช้-rแฟล็กเพื่อให้ได้ผลลัพธ์เป็นสตริงดิบ:

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

6
เป็นเรื่องดีที่โซลูชันของคุณรวบรวมชื่อคุณสมบัติทั้งหมดจากแถวทั้งหมดแทนที่จะเป็นชื่อแรก ฉันสงสัยว่าผลกระทบด้านประสิทธิภาพของสิ่งนี้คืออะไรสำหรับเอกสารขนาดใหญ่มาก ป.ล. ถ้าคุณต้องการคุณสามารถกำจัดการ$rowsกำหนดตัวแปรได้โดยการใส่ไว้ใน:(map(keys) | add | unique) as $cols | $cols, map(. as $row | $cols | map($row[.]))[] | @csv
Jordan Running

9
ขอบคุณจอร์แดน! ฉันทราบว่า$rowsไม่จำเป็นต้องกำหนดให้กับตัวแปร ฉันแค่คิดว่าการกำหนดให้กับตัวแปรทำให้คำอธิบายดีขึ้น

3
พิจารณาการแปลงค่าแถว | สตริงในกรณีที่มีอาร์เรย์หรือแผนที่ซ้อนกัน
TJR

ข้อเสนอแนะที่ดี @TJR บางทีถ้ามีโครงสร้างที่ซ้อนกัน jq ก็ควรเรียกคืนเข้าไปและทำให้ค่าเป็นคอลัมน์ด้วย
LS

สิ่งนี้จะแตกต่างกันอย่างไรหาก JSON อยู่ในไฟล์และคุณต้องการกรองข้อมูลเฉพาะบางส่วนออกเป็น CSV
นีโอ

92

ผอม

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

หรือ:

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

รายละเอียด

นอกจากนี้

การอธิบายรายละเอียดเป็นเรื่องยุ่งยากเนื่องจาก jq เป็นแบบสตรีมซึ่งหมายความว่ามันทำงานตามลำดับของข้อมูล JSON แทนที่จะเป็นค่าเดียว อินพุต JSON สตรีมจะถูกแปลงเป็นประเภทภายในบางประเภทซึ่งส่งผ่านตัวกรองจากนั้นเข้ารหัสในสตรีมเอาต์พุตเมื่อสิ้นสุดโปรแกรม ประเภทภายในไม่ได้ถูกจำลองโดย JSON และไม่มีอยู่ในประเภทที่ตั้งชื่อ แสดงให้เห็นได้ง่ายที่สุดโดยการตรวจสอบผลลัพธ์ของดัชนีเปล่า ( .[]) หรือตัวดำเนินการลูกน้ำ (การตรวจสอบโดยตรงสามารถทำได้ด้วยดีบักเกอร์ แต่จะเป็นในแง่ของประเภทข้อมูลภายในของ jq แทนที่จะเป็นประเภทข้อมูลแนวความคิดที่อยู่เบื้องหลัง JSON) .

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

โปรดทราบว่าผลลัพธ์ไม่ใช่อาร์เรย์ (ซึ่งจะเป็น["a", "b"]) เอาต์พุตแบบกระชับ ( -cตัวเลือก) แสดงให้เห็นว่าแต่ละองค์ประกอบอาร์เรย์ (หรืออาร์กิวเมนต์ไปยัง,ตัวกรอง) กลายเป็นวัตถุที่แยกจากกันในเอาต์พุต (แต่ละรายการอยู่ในบรรทัดแยกกัน)

สตรีมเปรียบเสมือนJSON-seqแต่ใช้บรรทัดใหม่แทนRSเป็นตัวคั่นเอาต์พุตเมื่อเข้ารหัส ดังนั้นประเภทภายในนี้จึงถูกอ้างถึงโดยคำทั่วไป "ลำดับ" ในคำตอบนี้โดย "สตรีม" จะสงวนไว้สำหรับอินพุตและเอาต์พุตที่เข้ารหัส

การสร้างตัวกรอง

สามารถแยกคีย์ของวัตถุแรกได้ด้วย:

.[0] | keys_unsorted

โดยทั่วไปคีย์จะถูกเก็บไว้ในลำดับเดิม แต่ไม่รับประกันการรักษาลำดับที่แน่นอน ดังนั้นพวกเขาจะต้องใช้ในการทำดัชนีวัตถุเพื่อให้ได้ค่าในลำดับเดียวกัน นอกจากนี้ยังจะป้องกันไม่ให้ค่าอยู่ในคอลัมน์ที่ไม่ถูกต้องหากวัตถุบางอย่างมีลำดับคีย์ที่แตกต่างกัน

ในการส่งออกคีย์ทั้งสองเป็นแถวแรกและทำให้พร้อมใช้งานสำหรับการจัดทำดัชนีคีย์เหล่านั้นจะถูกเก็บไว้ในตัวแปร ขั้นตอนต่อไปของไปป์ไลน์จะอ้างอิงตัวแปรนี้และใช้ตัวดำเนินการลูกน้ำเพื่อนำหน้าส่วนหัวไปยังสตรีมเอาต์พุต

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

นิพจน์หลังเครื่องหมายจุลภาคมีส่วนเกี่ยวข้องเล็กน้อย ตัวดำเนินการดัชนีบนวัตถุสามารถใช้ลำดับของสตริง (เช่น"name", "value") ส่งคืนลำดับของค่าคุณสมบัติสำหรับสตริงเหล่านั้น $keysเป็นอาร์เรย์ไม่ใช่ลำดับดังนั้นจึง[]ใช้เพื่อแปลงเป็นลำดับ

$keys[]

ซึ่งสามารถส่งผ่านไปยัง .[]

.[ $keys[] ]

สิ่งนี้ก็สร้างลำดับเช่นกันดังนั้นตัวสร้างอาร์เรย์จึงถูกใช้เพื่อแปลงเป็นอาร์เรย์

[.[ $keys[] ]]

นิพจน์นี้จะใช้กับอ็อบเจ็กต์เดียว map()ใช้เพื่อนำไปใช้กับวัตถุทั้งหมดในอาร์เรย์ภายนอก:

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

สุดท้ายสำหรับขั้นตอนนี้สิ่งนี้จะถูกแปลงเป็นลำดับดังนั้นแต่ละรายการจึงกลายเป็นแถวแยกกันในเอาต์พุต

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

เหตุใดจึงรวมลำดับไว้ในอาร์เรย์ภายในmapเพื่อคลายการรวมกลุ่มภายนอก mapสร้างอาร์เรย์ .[ $keys[] ]สร้างลำดับ การนำmapไปใช้กับลำดับจาก.[ $keys[] ]จะสร้างอาร์เรย์ของลำดับของค่า แต่เนื่องจากลำดับไม่ใช่ประเภท JSON ดังนั้นคุณจึงได้รับอาร์เรย์แบบแบนที่มีค่าทั้งหมดแทน

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

ค่าจากแต่ละออบเจ็กต์จะต้องแยกจากกันเพื่อให้กลายเป็นแถวที่แยกจากกันในผลลัพธ์สุดท้าย

ในที่สุดลำดับจะถูกส่งผ่าน @csvฟอร์แมตเตอร์

สำรอง

รายการสามารถแยกออกจากกันช้ากว่าที่จะเร็ว แทนที่จะใช้ตัวดำเนินการลูกน้ำเพื่อรับลำดับ (ส่งผ่านลำดับเป็นตัวถูกดำเนินการด้านขวา) ลำดับส่วนหัว ( $keys) สามารถรวมอยู่ในอาร์เรย์และ+ใช้เพื่อต่อท้ายอาร์เรย์ของค่า @csvนี้ยังคงต้องการที่จะถูกแปลงเป็นลำดับก่อนที่จะถูกส่งผ่านไปยัง


3
คุณสามารถใช้keys_unsortedแทนkeysเพื่อรักษาลำดับคีย์จากออบเจ็กต์แรกได้หรือไม่
Jordan Running

2
@outis - คำนำเกี่ยวกับสตรีมค่อนข้างไม่ถูกต้อง ข้อเท็จจริงง่ายๆก็คือตัวกรอง jq นั้นเน้นสตรีม นั่นคือตัวกรองใด ๆ สามารถรับสตรีมของเอนทิตี JSON และตัวกรองบางตัวสามารถสร้างกระแสของค่าได้ ไม่มี "บรรทัดใหม่" หรือตัวคั่นอื่นใดระหว่างรายการในสตรีม - เฉพาะเมื่อมีการพิมพ์ตัวคั่นเท่านั้น หากต้องการดูตัวเองลอง: jq -n -c 'ลด ("a", "b") เป็น $ s ("";. + $ s)'
สูงสุด

2
@peak - โปรดยอมรับสิ่งนี้เป็นคำตอบโดยสมบูรณ์และครอบคลุมที่สุด
btk

@btk - ฉันไม่ได้ถามคำถามจึงรับไม่ได้
สูงสุด

1
@Wyatt: ดูข้อมูลของคุณและข้อมูลตัวอย่างให้ละเอียดยิ่งขึ้น คำถามเกี่ยวกับอาร์เรย์ของวัตถุไม่ใช่วัตถุเดียว ลอง[{"a":1,"b":2,"c":3}].
outis

6

ฉันสร้างฟังก์ชันที่ส่งออกอาร์เรย์ของวัตถุหรืออาร์เรย์ไปยัง csv ด้วยส่วนหัว คอลัมน์จะอยู่ในลำดับของส่วนหัว

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;

คุณสามารถใช้มันได้ดังนี้:

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

6

ตัวกรองต่อไปนี้แตกต่างกันเล็กน้อยเพื่อให้แน่ใจว่าทุกค่าจะถูกแปลงเป็นสตริง (หมายเหตุ: ใช้ 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.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
สิ่งนี้ใช้ได้ดีกับ JSON ธรรมดา แต่แล้ว JSON ที่มีคุณสมบัติซ้อนกันที่ลงไปหลายระดับล่ะ?
Amir

แน่นอนว่านี่คือกุญแจสำคัญ นอกจากนี้ผลลัพธ์ของuniqueยังถูกเรียงลำดับดังนั้นจึงunique|sortสามารถทำให้ง่ายuniqueขึ้นได้
สูงสุด

1
@TJR เมื่อใช้ตัวกรองนี้จำเป็นต้องเปิดเอาต์พุตดิบโดยใช้-rตัวเลือก มิฉะนั้นเครื่องหมายคำพูดทั้งหมด"จะกลายเป็นค่า Escape พิเศษซึ่งไม่ใช่ CSV ที่ถูกต้อง
tosh

Amir: คุณสมบัติที่ซ้อนกันไม่ได้จับคู่กับ CSV
chrishmorris

2

โปรแกรมของ Santiago ที่แตกต่างกันนี้ยังปลอดภัย แต่ให้แน่ใจว่าชื่อคีย์ในออบเจ็กต์แรกถูกใช้เป็นส่วนหัวคอลัมน์แรกตามลำดับเดียวกับที่ปรากฏในออบเจ็กต์นั้น:

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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.