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


105

使用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. 输入:

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

三年多以后...通用代码json2csvstackoverflow.com/questions/57242240/…–
高峰

Answers:


159

首先,获取一个包含对象数组输入中所有不同对象属性名称的数组。这些将是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'

6
很好,您的解决方案可以捕获所有行中的所有属性名称,而不仅仅是第一行。但是,我想知道这对于非常大的文档的性能影响如何。PS:如果需要,您可以$rows通过内联来摆脱变量赋值:(map(keys) | add | unique) as $cols | $cols, map(. as $row | $cols | map($row[.]))[] | @csv
乔丹(Jordan)

9
谢谢,乔丹!我知道$rows不必将其分配给变量;我只是认为将其分配给变量可以使解释更好。

3
考虑转换行值 如果存在嵌套数组或映射,则为字符串。
TJR

好建议,@ TJR。也许如果有嵌套结构,则jq应该递归到它们中,并将它们的值也分成列
LS

如果JSON位于文件中并且您想将某些特定数据过滤到CSV,这会有什么不同?

91

瘦的

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”]'
“一个”
“ b”
$ jq -cn'“ a”,“ b”'
“一个”
“ 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产生一个数组;.[ $keys[] ]产生一个序列。将mapfrom 应用于from .[ $keys[] ]会产生一个值序列数组,但是由于sequence不是JSON类型,因此您将获得一个包含所有值的扁平数组。

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

每个对象的值需要保持分开,以便它们在最终输出中成为单独的行。

最后,序列通过@csv格式化程序传递。

备用

这些项目可以分开而不是提早。代替使用逗号运算符来获取序列(将序列作为正确的操作数传递),标头序列($keys)可以包装在数组中,并+用于附加值数组。在传递给之前,仍然需要将其转换为序列@csv


3
您可以使用keys_unsorted而不是keys保留第一个对象的键顺序吗?
乔丹

2
@outis-有关流的序言有些不准确。一个简单的事实是jq过滤器是面向流的。也就是说,任何过滤器都可以接受JSON实体流,而某些过滤器可以产生值流。流中的项目之间没有“换行符”或任何其他分隔符-仅在打印它们时才引入分隔符。若要亲自查看,请尝试:jq -n -c'reduce(“ a”,“ b”)as $ s(“”;。+ $ s)'
高峰

2
@peak-请接受此作为答案,它是迄今为止最完整和最全面的
btk

@btk-我没有提出问题,因此无法接受。
达到峰值

1
@Wyatt:仔细查看您的数据和示例输入。问题是关于对象数组,而不是单个对象。尝试[{"a":1,"b":2,"c":3}]
outis

6

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

6

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

tocsv(.)

1
这对于简单的JSON很有用,但是具有嵌套属性的JSON在很多级别上又如何呢?
阿米尔(Amir)

当然,这会对键进行排序。此外,的输出unique仍会进行排序,因此unique|sort可以简化为unique
达到峰值

1
@TJR使用此过滤器时,必须使用-r选项打开原始输出。否则,所有引号都会"被转义,这是无效的CSV。
托什

阿米尔:嵌套属性不会映射到CSV。
chrishmorris

2

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

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.