如何使用 jq 将任意简单 JSON 转换为 CSV?
Posted
技术标签:
【中文标题】如何使用 jq 将任意简单 JSON 转换为 CSV?【英文标题】:How to convert arbitrary simple JSON to CSV using jq? 【发布时间】:2016-01-02 19:59:39 【问题描述】:使用jq,如何将任意JSON编码的浅对象数组转换为CSV?
这个网站上有很多问答,涵盖了对字段进行硬编码的特定数据模型,但是对于任何 JSON,这个问题的答案都应该有效,唯一的限制是它是一个具有标量属性的对象数组(没有深度/complex/sub-objects,因为展平这些是另一个问题)。结果应包含给出字段名称的标题行。将优先考虑保留第一个对象的字段顺序的答案,但这不是必需的。结果可以用双引号将所有单元格括起来,或仅将需要引用的单元格括起来(例如'a,b')。
示例
输入:
[
"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"
输入:
[
"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
位于***.com/questions/57242240/…
晚会 ;) 这是另一个允许反向转换的通用解决方案:***.com/questions/69230818/…
【参考方案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'
【讨论】:
很高兴您的解决方案捕获了所有行中的所有属性名称,而不仅仅是第一个。不过,我想知道这对非常大的文档有什么性能影响。附言如果你愿意,你可以通过内联去掉$rows
变量赋值:(map(keys) | add | unique) as $cols | $cols, map(. as $row | $cols | map($row[.]))[] | @csv
谢谢,乔丹!我知道 $rows
不必分配给变量;我只是认为将其分配给变量会使解释更好。
考虑转换行值 |字符串,以防有嵌套数组或映射。
好建议,@TJR。也许如果有嵌套结构,jq 应该递归到它们并将它们的值也放入列中
如果 JSON 在文件中并且您想将某些特定数据过滤到 CSV,这会有什么不同?【参考方案2】:
瘦子
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"]
)。压缩输出(-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
之前,这仍然需要转换为序列。
【讨论】:
您可以使用keys_unsorted
代替keys
来保留第一个对象的键顺序吗?
@outis - 关于流的序言有些不准确。一个简单的事实是 jq 过滤器是面向流的。也就是说,任何过滤器都可以接受 JSON 实体流,并且某些过滤器可以产生值流。流中的项目之间没有“新行”或任何其他分隔符——只有在打印它们时才会引入分隔符。要自己查看,请尝试: jq -n -c 'reduce ("a","b") as $s (""; . + $s)'
在写这篇文章和现在呈现不正确之间是否发生了什么?问题似乎出在地图上,它在一个玩具示例上达到平衡:$ echo '"a":1,"b":2,"c":3' |jq -r '(. | keys_unsorted) as $keys| $keys, map( [.[ $keys[] ] ])[] | @csv'
在 jq-1.5 上输出 "a","b","c" jq: error (at <stdin>:1): Cannot index number with string "a"
。
@Wyatt:仔细查看您的数据和示例输入。问题是关于对象数组,而不是单个对象。试试["a":1,"b":2,"c":3]
。
通过这个解决方案的细节工作教会了我很多关于 jq 的知识!对于其他在细节上苦苦挣扎的人,使用 "jq -cr '(.[0] | keys_unsorted) as $array_of_keys | $array_of_keys, (.[] | [ .[$array_of_keys[]] ]) 可能会有所帮助| .'",因为这就是地图过滤器的实现方式。请记住,“(foo) as $bar”变量赋值实际上充当了一个 for-each,它迭代 (foo) 表达式中的所有项目(在这种情况下不是问题,因为我们将键作为单个项目)。【参考方案3】:
我创建了一个函数,该函数将对象数组或数组输出到带有标题的 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" ])
【讨论】:
【参考方案4】:下面的过滤器略有不同,它将确保每个值都转换为字符串。 (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
【讨论】:
这对于简单的 JSON 很有效,但是对于嵌套属性会下降很多层的 JSON 呢? 这当然是对键进行排序。同样unique
的输出无论如何都会排序,所以unique|sort
可以简化为unique
。
@TJR 使用此过滤器时,必须使用-r
选项打开原始输出。否则,所有引号 "
都会被额外转义,这不是有效的 CSV。
Amir:嵌套属性不映射到 CSV。
@Amir:添加到 chrishmorris 的评论中,这个问题明确限于“具有标量属性的对象数组(没有深度/复杂/子对象,因为展平这些是另一个问题) "。【参考方案5】:
圣地亚哥程序的这个变体也是安全的,但确保了 第一个对象用作第一列标题,顺序与它们相同 出现在该对象中:
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
【讨论】:
以上是关于如何使用 jq 将任意简单 JSON 转换为 CSV?的主要内容,如果未能解决你的问题,请参考以下文章
如何在 jq 中将 JSON 对象转换为 key=value 格式?
我在jq中使用相同的语法来更改JSON值,但是有一个案例有效,而其他情况则转换为bash交互式,我该如何解决这个问题呢?