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

out*_*tis 84 csv json jq

使用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"}
    ]
    
    Run Code Online (Sandbox Code Playgroud)

    可能的输出:

    code,name,level,country
    NSW,New South Wales,state,AU
    AB,Alberta,province,CA
    ABD,Aberdeenshire,council area,GB
    AK,Alaska,state,US
    
    Run Code Online (Sandbox Code Playgroud)

    可能的输出:

    "code","name","level","country"
    "NSW","New South Wales","state","AU"
    "AB","Alberta","province","CA"
    "ABD","Aberdeenshire","council area","GB"
    "AK","Alaska","state","US"
    
    Run Code Online (Sandbox Code Playgroud)
  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}
    ]
    
    Run Code Online (Sandbox Code Playgroud)

    可能的输出:

    name,value,level
    bang,!,0
    letters,"a,b,c",0
    letters,"x,y,z",1
    bang,"""!""",0
    
    Run Code Online (Sandbox Code Playgroud)

    可能的输出:

    "name","value","level"
    "bang","!","0"
    "letters","a,b,c","0"
    "letters","x,y,z","1"
    "bang","""!""","1"
    
    Run Code Online (Sandbox Code Playgroud)

小智 134

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

(map(keys) | add | unique) as $cols
Run Code Online (Sandbox Code Playgroud)

然后,对于对象数组输入中的每个对象,将获取的列名映射到对象中的相应属性.这些将是您的CSV行.

map(. as $row | $cols | map($row[.])) as $rows
Run Code Online (Sandbox Code Playgroud)

最后,将列名放在行之前,作为CSV的标题,并将生成的行流传递给@csv过滤器.

$cols, $rows[] | @csv
Run Code Online (Sandbox Code Playgroud)

现在都在一起了.请记住使用该-r标志将结果作为原始字符串:

jq -r '(map(keys) | add | unique) as $cols | map(. as $row | $cols | map($row[.])) as $rows | $cols, $rows[] | @csv'
Run Code Online (Sandbox Code Playgroud)

  • 谢谢,乔丹!我知道`$ rows`不必分配给变量; 我只是想把它分配给变量使解释更好. (8认同)
  • 您的解决方案捕获所有行中的所有属性名称,而不仅仅是第一行,这很好.不过,我想知道这对于非常大的文档有什么影响.PS如果你想,你可以通过内联它来摆脱`$ rows`变量赋值:`(map(keys)| add | unique)as $ cols | $ cols,map(.as $ row | $ cols | map($ row [.]))[] | @ csv` (4认同)
  • 考虑转换行值 | 字符串以防有嵌套数组或映射。 (3认同)

out*_*tis 65

瘦子

jq -r '(.[0] | keys_unsorted) as $keys | $keys, map([.[ $keys[] ]])[] | @csv'
Run Code Online (Sandbox Code Playgroud)

要么:

jq -r '(.[0] | keys_unsorted) as $keys | ([$keys] + map([.[ $keys[] ]])) [] | @csv'
Run Code Online (Sandbox Code Playgroud)

细节

在旁边

描述细节是棘手的,因为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
Run Code Online (Sandbox Code Playgroud)

钥匙通常会保留原始订单,但不保证保留确切的订单.因此,需要使用它们来索引对象以获得相同顺序的值.如果某些对象具有不同的键顺序,这也将阻止值位于错误的列中.

要将键输出为第一行并使其可用于索引,它们将存储在变量中.然后,管道的下一个阶段引用此变量,并使用逗号运算符将标头添加到输出流.

(.[0] | keys_unsorted) as $keys | $keys, ...
Run Code Online (Sandbox Code Playgroud)

逗号之后的表达有点牵扯.对象上的索引运算符可以采用一系列字符串(例如"name", "value"),为这些字符串返回一系列属性值.$keys是一个数组,而不是一个序列,因此[]应用于将其转换为序列,

$keys[]
Run Code Online (Sandbox Code Playgroud)

然后可以传递给 .[]

.[ $keys[] ]
Run Code Online (Sandbox Code Playgroud)

这也产生一个序列,因此数组构造函数用于将其转换为数组.

[.[ $keys[] ]]
Run Code Online (Sandbox Code Playgroud)

此表达式将应用于单个对象.map()用于将其应用于外部数组中的所有对象:

map([.[ $keys[] ]])
Run Code Online (Sandbox Code Playgroud)

最后,对于此阶段,将其转换为序列,以便每个项目成为输出中的单独行.

map([.[ $keys[] ]])[]
Run Code Online (Sandbox Code Playgroud)

为什么要将序列捆绑到一个数组中,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"]
Run Code Online (Sandbox Code Playgroud)

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

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

备用

这些物品可以分开,而不是提前分开.而不是使用逗号运算符来获取序列(将序列作为右操作数传递),头序列($keys)可以包装在数组中,并+用于追加值数组.在传递之前,仍然需要将其转换为序列@csv.

  • 你能用`keys_unsorted`而不是`keys`来保存第一个对象的键顺序吗? (3认同)
  • @outis - 关于流的序言有些不准确。一个简单的事实是 jq 过滤器是面向流的。也就是说,任何过滤器都可以接受 JSON 实体流,而某些过滤器可以生成值流。流中的项目之间没有“换行”或任何其他分隔符——只有在打印它们时才会引入分隔符。要亲自查看,请尝试: jq -n -c 'reduce ("a","b") as $s (""; . + $s)' (2认同)
  • @peak-请接受这个作为答案,它是迄今为止最完整和最全面的 (2认同)

小智 27

$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"
Run Code Online (Sandbox Code Playgroud)


Jef*_*ado 7

我创建了一个函数,该函数将一组对象或数组输出到带有标题的 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;
Run Code Online (Sandbox Code Playgroud)

所以你可以像这样使用它:

to_csv([ "code", "name", "level", "country" ])
Run Code Online (Sandbox Code Playgroud)


TJR*_*TJR 7

以下过滤器略有不同,因为它将确保每个值都转换为字符串。(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]
Run Code Online (Sandbox Code Playgroud)

筛选: filter.jq

def tocsv:
    (map(keys)
        |add
        |unique
        |sort
    ) as $cols
    |map(. as $row
        |$cols
        |map($row[.]|tostring)
    ) as $rows
    |$cols,$rows[]
    | @csv;

tocsv
Run Code Online (Sandbox Code Playgroud)

  • @TJR 使用此过滤器时,必须使用 `-r` 选项打开原始输出。否则所有引号 `"` 都会被额外转义,这不是有效的 CSV。 (4认同)

jtp*_*yda 6

如果您愿意使用其他 Unix 工具,csvkit可以使用以下in2csv工具:

in2csv example.json
Run Code Online (Sandbox Code Playgroud)

使用您的样本数据:

> in2csv example.json
code,name,level,country
NSW,New South Wales,state,AU
AB,Alberta,province,CA
ABD,Aberdeenshire,council area,GB
AK,Alaska,state,US 
Run Code Online (Sandbox Code Playgroud)

我喜欢直接从以下位置进行管道传输的管道方法jq

cat example.json | in2csv -f json -
Run Code Online (Sandbox Code Playgroud)