Programing

jq를 사용하여 임의의 간단한 JSON을 CSV로 변환하는 방법은 무엇입니까?

lottogame 2020. 10. 7. 07:13
반응형

jq를 사용하여 임의의 간단한 JSON을 CSV로 변환하는 방법은 무엇입니까?


jq를 사용하여 얕은 객체 배열을 인코딩하는 임의의 JSON을 어떻게 CSV로 변환 할 수 있습니까?

이 사이트에는 필드를 하드 코딩하는 특정 데이터 모델을 다루는 많은 Q & A가 있지만,이 질문에 대한 답변은 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"
    

먼저, 객체 배열 입력에서 모든 다른 객체 속성 이름을 포함하는 배열을 가져옵니다. 다음은 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'

스키니

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으로 모델링되지 않으며 명명 된 유형으로 존재하지 않습니다. 베어 인덱스 ( .[]) 또는 쉼표 연산자 의 출력을 검사하여 가장 쉽게 설명 할 수 있습니다 (디버거로 직접 검사 할 수 있지만 JSON 뒤에있는 개념적 데이터 유형이 아닌 jq의 내부 데이터 유형 측면에서 수행됨). .

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

출력은 배열이 아닙니다 ( ["a", "b"]). 압축 출력 ( -c옵션)은 각 배열 요소 (또는 ,필터 에 대한 인수 )가 출력에서 ​​별도의 개체가되는 것을 보여줍니다 (각각은 별도의 줄에 있음).

스트림은 JSON-seq 와 비슷하지만 인코딩시 출력 구분 기호로 RS가 아닌 개행 문자를 사용합니다 . 결과적으로이 내부 유형은이 답변에서 일반 용어 "시퀀스"로 참조되며 "스트림"은 인코딩 된 입력 및 출력용으로 예약되어 있습니다.

필터 구성

첫 번째 개체의 키는 다음을 사용하여 추출 할 수 있습니다.

.[0] | keys_unsorted

Keys will generally be kept in their original order, but preserving the exact order isn't guaranteed. Consequently, they will need to be used to index the objects to get the values in the same order. This will also prevent values being in the wrong columns if some objects have a different key order.

To both output the keys as the first row and make them available for indexing, they're stored in a variable. The next stage of the pipeline then references this variable and uses the comma operator to prepend the header to the output stream.

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

The expression after the comma is a little involved. The index operator on an object can take a sequence of strings (e.g. "name", "value"), returning a sequence of property values for those strings. $keys is an array, not a sequence, so [] is applied to convert it to a sequence,

$keys[]

which can then be passed to .[]

.[ $keys[] ]

This, too, produces a sequence, so the array constructor is used to convert it to an array.

[.[ $keys[] ]]

This expression is to be applied to a single object. map() is used to apply it to all objects in the outer array:

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

Lastly for this stage, this is converted to a sequence so each item becomes a separate row in the output.

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

Why bundle the sequence into an array within the map only to unbundle it outside? map produces an array; .[ $keys[] ] produces a sequence. Applying map to the sequence from .[ $keys[] ] would produce an array of sequences of values, but since sequences aren't a JSON type, so you instead get a flattened array containing all the values.

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

The values from each object need to be kept separate, so that they become separate rows in the final output.

Finally, the sequence is passed through @csv formatter.

Alternate

The items can be separated late, rather than early. Instead of using the comma operator to get a sequence (passing a sequence as the right operand), the header sequence ($keys) can be wrapped in an array, and + used to append the array of values. This still needs to be converted to a sequence before being passed to @csv.


The following filter is slightly different in that it will ensure every value is converted to a string. (Note: use 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(.)

I created a function that outputs an array of objects or arrays to csv with headers. The columns would be in the order of the headers.

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;

So you could use it like so:

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

This variant of Santiago's program is also safe but ensures that the key names in the first object are used as the first column headers, in the same order as they appear in that object:

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

참고URL : https://stackoverflow.com/questions/32960857/how-to-convert-arbitrary-simple-json-to-csv-using-jq

반응형