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

使用 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"
    
  2. Input:

    [
    {"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"
    
157883 次浏览

首先,获取一个数组,该数组包含对象数组输入中的所有不同对象属性名称。这些将是你的 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 建模,也不作为命名类型存在。通过检查一个裸索引(.[])或逗号操作符的输出(直接检查可以通过调试器完成,但那将是根据 jq 的内部数据类型,而不是 JSON 背后的概念数据类型) ,可以很容易地证明这一点。

$jq-c’. []’< <’[“ a”,“ b”]’
“ A”
B
$jq-cn’a,b’
“ 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之外将其解绑?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之前,这仍然需要转换为一个序列。

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

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

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

下面的过滤器稍有不同,它将确保每个值都转换为字符串

# 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

一个简单的方法就是使用字符串连接:

# 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'
$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"