如何使用jq将任意简单JSON转换为CSV?

zyfwsgd6  于 2022-12-15  发布在  其他
关注(0)|答案(7)|浏览(190)

使用jq,如何将任意JSON编码的浅层对象数组转换为CSV?
本网站上有大量的问答,涉及硬编码字段的特定数据模型,但是对于任何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"

1.输入:

[
    {"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"
flvtvl50

flvtvl501#

首先,获取一个包含对象数组输入中所有不同对象属性名称的数组,这些名称将成为CSV的列:

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

然后,对于对象数组输入中的每个对象,将获得的列名Map到对象中相应的属性,这些属性将成为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'
e4yzc0pl

e4yzc0pl2#

瘦子

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"]'
"a"
"b"
$ jq -cn '"a", "b"'
"a"
"b"

注意,输出不是数组(["a", "b"])。紧凑输出(-c选项)显示每个数组元素(或,过滤器的参数)成为输出中的一个单独对象(每个对象在单独的行上)。
流类似于JSON-seq,但在编码时使用换行符而不是RS作为输出分隔符。因此,在本答案中,这种内部类型被称为通用术语“sequence”,“stream”被保留用于编码的输入和输出。

构造过滤器

第一对象的密钥可以用以下方式提取:

.[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) Package 在数组中,并使用+附加值数组。在传递给@csv之前,仍需要将其转换为序列。

3pmvbmvn

3pmvbmvn3#

下面的过滤器稍有不同,因为它将确保每个值都转换为字符串。(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:
    (map(keys)
        |add
        |unique
        |sort
    ) as $cols
    |map(. as $row
        |$cols
        |map($row[.]|tostring)
    ) as $rows
    |$cols,$rows[]
    | @csv;

tocsv
nzk0hqpo

nzk0hqpo4#

$cat test.json
[
    {"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"}
]

$ jq -r '["Code", "Name", "Level", "Country"], (.[] | [.code, .name, .level, .country]) | @tsv ' test.json
Code    Name    Level   Country
NSW New South Wales state   AU
AB  Alberta province    CA
ABD Aberdeenshire   council area    GB
AK  Alaska  state   US

$ jq -r '["Code", "Name", "Level", "Country"], (.[] | [.code, .name, .level, .country]) | @csv ' test.json
"Code","Name","Level","Country"
"NSW","New South Wales","state","AU"
"AB","Alberta","province","CA"
"ABD","Aberdeenshire","council area","GB"
"AK","Alaska","state","US"
iecba09b

iecba09b5#

我创建了一个函数,它输出一个对象数组或数组到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" ])

kninwzqo

kninwzqo6#

圣地亚哥的程序的这个变体也是安全的,但是确保了第一个对象中的键名被用作第一列标题,其顺序与它们在该对象中出现的顺序相同:

def tocsv:
  if length == 0 then empty
  else
    (.[0] | keys_unsorted) as $firstkeys
    | (map(keys) | add | unique) as $allkeys
    | ($firstkeys + ($allkeys - $firstkeys)) as $cols
    | ($cols, (.[] as $row | $cols | map($row[.])))
    | @csv
  end ;

tocsv
myss37ts

myss37ts7#

一个简单的方法就是使用字符串连接。如果你的输入是一个合适的数组:

# filename.txt
[
  {"field1":"value1", "field2":"value2"},
  {"field1":"value1", "field2":"value2"},
  {"field1":"value1", "field2":"value2"}
]

然后使用.[]

cat filename.txt | jq -r '.[] | .field1 + ", " + .field2'

或者是一行一行的物体

# filename.txt
{"field1":"value1", "field2":"value2"}
{"field1":"value1", "field2":"value2"}
{"field1":"value1", "field2":"value2"}

只要这样做:

cat filename.txt | jq -r '.field1 + ", " + .field2'

相关问题