Làm cách nào để chuyển đổi JSON đơn giản tùy ý sang CSV bằng jq?


105

Sử dụng jq , làm cách nào để mã hóa JSON tùy ý một mảng các đối tượng cạn được chuyển đổi thành CSV?

Có rất nhiều Hỏi & Đáp trên trang web này đề cập đến các mô hình dữ liệu cụ thể mã hóa các trường, nhưng câu trả lời cho câu hỏi này sẽ hoạt động với bất kỳ JSON nào, với hạn chế duy nhất là đó là một mảng đối tượng có thuộc tính vô hướng (không sâu / phức tạp / các đối tượng phụ, vì làm phẳng chúng là một câu hỏi khác). Kết quả phải chứa một hàng tiêu đề cung cấp tên trường. Ưu tiên sẽ được đưa ra cho các câu trả lời bảo toàn thứ tự trường của đối tượng đầu tiên, nhưng nó không phải là một yêu cầu. Kết quả có thể bao gồm tất cả các ô bằng dấu ngoặc kép hoặc chỉ bao gồm những ô yêu cầu trích dẫn (ví dụ: 'a, b').

Ví dụ

  1. Đầu vào:

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

    Đầu ra có thể có:

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

    Đầu ra có thể có:

    "code","name","level","country"
    "NSW","New South Wales","state","AU"
    "AB","Alberta","province","CA"
    "ABD","Aberdeenshire","council area","GB"
    "AK","Alaska","state","US"
    
  2. Đầu vào:

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

    Đầu ra có thể có:

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

    Đầu ra có thể có:

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

Hơn ba năm sau ... một điểm chung json2csvstackoverflow.com/questions/57242240/…
cao điểm vào

Câu trả lời:


159

Đầu tiên, lấy một mảng chứa tất cả các tên thuộc tính đối tượng khác nhau trong đầu vào mảng đối tượng của bạn. Đó sẽ là các cột trong CSV của bạn:

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

Sau đó, đối với mỗi đối tượng trong đầu vào mảng đối tượng, hãy ánh xạ tên cột mà bạn thu được với các thuộc tính tương ứng trong đối tượng. Đó sẽ là các hàng trong CSV của bạn.

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

Cuối cùng, đặt tên cột trước các hàng, làm tiêu đề cho CSV và chuyển luồng hàng kết quả vào @csvbộ lọc.

$cols, $rows[] | @csv

Tất cả cùng nhau bây giờ. Hãy nhớ sử dụng -rcờ để nhận kết quả là một chuỗi thô:

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

6
Thật tuyệt khi giải pháp của bạn nắm bắt tất cả các tên thuộc tính từ tất cả các hàng, thay vì chỉ đầu tiên. Tuy nhiên, tôi tự hỏi ý nghĩa hiệu suất của điều này là gì đối với các tài liệu rất lớn. Tái bút Nếu bạn muốn, bạn có thể loại bỏ việc $rowsgán biến bằng cách nội dòng nó:(map(keys) | add | unique) as $cols | $cols, map(. as $row | $cols | map($row[.]))[] | @csv
Jordan Running

9
Cảm ơn, Jordan! Tôi biết rằng $rowskhông cần phải gán cho một biến; Tôi chỉ nghĩ rằng việc gán nó cho một biến làm cho lời giải thích tốt hơn.

3
xem xét chuyển đổi giá trị hàng | chuỗi trong trường hợp có các mảng hoặc bản đồ lồng nhau.
TJR

Đề xuất tốt, @TJR. Có lẽ nếu có cấu trúc lồng nhau, JQ nên recurse vào chúng và làm cho giá trị của chúng thành các cột cũng
LS

Điều này sẽ khác như thế nào nếu JSON nằm trong một tệp và bạn muốn lọc ra một số dữ liệu cụ thể cho CSV?
Neo

91

Người gầy

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

hoặc là:

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

Các chi tiết

Qua một bên

Việc mô tả chi tiết rất phức tạp bởi vì jq là hướng dòng, có nghĩa là nó hoạt động trên một chuỗi dữ liệu JSON, thay vì một giá trị duy nhất. Luồng JSON đầu vào được chuyển đổi thành một số kiểu nội bộ được chuyển qua các bộ lọc, sau đó được mã hóa trong luồng đầu ra ở cuối chương trình. Loại nội bộ không được JSON mô hình hóa và không tồn tại dưới dạng một loại được đặt tên. Nó dễ dàng được chứng minh nhất bằng cách kiểm tra đầu ra của một chỉ mục trống ( .[]) hoặc toán tử dấu phẩy (kiểm tra trực tiếp nó có thể được thực hiện bằng trình gỡ lỗi, nhưng điều đó sẽ là về các kiểu dữ liệu nội bộ của jq, chứ không phải là các kiểu dữ liệu khái niệm đằng sau JSON) .

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

Lưu ý rằng đầu ra không phải là một mảng (sẽ là ["a", "b"]). Đầu ra thu gọn ( -ctùy chọn) cho thấy mỗi phần tử mảng (hoặc đối số của ,bộ lọc) trở thành một đối tượng riêng biệt trong đầu ra (mỗi phần tử nằm trên một dòng riêng biệt).

Một luồng giống như một JSON-seq , nhưng sử dụng các dòng mới thay vì RS làm dấu phân tách đầu ra khi được mã hóa. Do đó, kiểu nội bộ này được gọi bằng thuật ngữ chung "chuỗi" trong câu trả lời này, với "luồng" được dành riêng cho đầu vào và đầu ra được mã hóa.

Cấu tạo bộ lọc

Các khóa của đối tượng đầu tiên có thể được trích xuất bằng:

.[0] | keys_unsorted

Các chìa khóa thường sẽ được giữ theo thứ tự ban đầu, nhưng không đảm bảo việc bảo toàn thứ tự chính xác. Do đó, chúng sẽ cần được sử dụng để lập chỉ mục các đối tượng để nhận các giá trị theo cùng một thứ tự. Điều này cũng sẽ ngăn các giá trị nằm trong cột sai nếu một số đối tượng có thứ tự khóa khác nhau.

Để vừa xuất các khóa dưới dạng hàng đầu tiên và làm cho chúng có sẵn để lập chỉ mục, chúng được lưu trữ trong một biến. Giai đoạn tiếp theo của đường ống sau đó tham chiếu đến biến này và sử dụng toán tử dấu phẩy để thêm tiêu đề vào luồng đầu ra.

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

Biểu thức sau dấu phẩy có một chút liên quan. Toán tử chỉ mục trên một đối tượng có thể nhận một chuỗi các chuỗi (ví dụ "name", "value"), trả về một chuỗi các giá trị thuộc tính cho các chuỗi đó. $keyslà một mảng, không phải là một chuỗi, vì vậy []được áp dụng để chuyển đổi nó thành một chuỗi,

$keys[]

sau đó có thể được chuyển cho .[]

.[ $keys[] ]

Điều này cũng tạo ra một chuỗi, vì vậy hàm tạo mảng được sử dụng để chuyển đổi nó thành một mảng.

[.[ $keys[] ]]

Biểu thức này sẽ được áp dụng cho một đối tượng. map()được sử dụng để áp dụng nó cho tất cả các đối tượng trong mảng bên ngoài:

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

Cuối cùng cho giai đoạn này, điều này được chuyển đổi thành một chuỗi để mỗi mục trở thành một hàng riêng biệt trong đầu ra.

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

Tại sao lại nhóm chuỗi thành một mảng trong mảng mapduy nhất để bỏ nhóm bên ngoài? maptạo ra một mảng; .[ $keys[] ]tạo ra một chuỗi. Việc áp dụng mapcho chuỗi từ .[ $keys[] ]sẽ tạo ra một mảng các chuỗi giá trị, nhưng vì chuỗi không phải là kiểu JSON, vì vậy thay vào đó bạn sẽ nhận được một mảng phẳng chứa tất cả các giá trị.

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

Các giá trị từ mỗi đối tượng cần được giữ riêng biệt để chúng trở thành các hàng riêng biệt trong kết quả cuối cùng.

Cuối cùng, trình tự được chuyển qua trình @csvđịnh dạng.

Luân phiên

Các mục có thể được tách ra muộn hơn là sớm. Thay vì sử dụng toán tử dấu phẩy để nhận một chuỗi (chuyển một chuỗi làm toán hạng bên phải), chuỗi tiêu đề ( $keys) có thể được bao bọc trong một mảng và +được sử dụng để nối thêm mảng giá trị. Điều này vẫn cần được chuyển đổi thành một chuỗi trước khi được chuyển đến @csv.


3
Bạn có thể sử dụng keys_unsortedthay vì keysđể bảo toàn thứ tự khóa từ đối tượng đầu tiên không?
Jordan Running

2
@outis - Lời mở đầu về luồng hơi không chính xác. Thực tế đơn giản là các bộ lọc jq là hướng dòng. Nghĩa là, bất kỳ bộ lọc nào cũng có thể chấp nhận một luồng các thực thể JSON và một số bộ lọc có thể tạo ra một luồng giá trị. Không có "dòng mới" hoặc bất kỳ dấu phân cách nào khác giữa các mục trong luồng - chỉ khi chúng được in ra thì dấu phân tách mới được đưa vào. Để tự mình xem, hãy thử: jq -n -c 'Reduce ("a", "b") as $ s ("";. + $ S)'
cao điểm

2
@peak - vui lòng chấp nhận điều này là câu trả lời, nó là của xa đầy đủ nhất và toàn diện
BTK

@btk - Tôi đã không đặt câu hỏi và do đó không thể chấp nhận nó.
cao điểm

1
@Wyatt: xem xét kỹ hơn dữ liệu của bạn và đầu vào ví dụ. Câu hỏi là về một mảng các đối tượng, không phải một đối tượng đơn lẻ. Cố gắng [{"a":1,"b":2,"c":3}].
outis

6

Tôi đã tạo một hàm xuất một mảng đối tượng hoặc mảng sang csv có tiêu đề. Các cột sẽ theo thứ tự của tiêu đề.

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;

Vì vậy, bạn có thể sử dụng nó như vậy:

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

6

Bộ lọc sau hơi khác ở chỗ nó sẽ đảm bảo mọi giá trị được chuyển đổi thành chuỗi. (Lưu ý: sử dụng 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)

Bộ lọc: 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
Điều này hoạt động tốt cho JSON đơn giản nhưng còn JSON với các thuộc tính lồng nhau đi xuống nhiều cấp thì sao?
Amir

Tất nhiên điều này sắp xếp các phím. Ngoài ra, đầu ra của uniquevẫn được sắp xếp, vì vậy unique|sortcó thể được đơn giản hóa thành unique.
cao điểm

1
@TJR Khi sử dụng bộ lọc này, bắt buộc phải bật -rtùy chọn đầu ra thô bằng cách sử dụng . Nếu không, tất cả các dấu ngoặc kép "trở nên thừa thoát và không phải là CSV hợp lệ.
tosh

Amir: các thuộc tính lồng nhau không ánh xạ tới CSV.
chrishmorris

2

Biến thể này của chương trình Santiago cũng an toàn nhưng đảm bảo rằng các tên khóa trong đối tượng đầu tiên được sử dụng làm tiêu đề cột đầu tiên, theo thứ tự giống như chúng xuất hiện trong đối tượng đó:

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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.