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

时间:2015-10-06 02:37:52

标签: json csv jq

使用jq,如何将浅层对象数组的任意JSON编码转换为CSV?

这个网站上有很多Q& As涵盖了对字段进行硬编码的特定数据模型,但是这个问题的答案应该适用于任何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"
    

5 个答案:

答案 0 :(得分:105)

First, obtain an array containing all the different object property names in your object array input. Those will be the columns of your CSV:

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

Then, for each object in the object array input, map the column names you obtained to the corresponding properties in the object. Those will be the rows of your CSV.

map(. as $row | $cols | map($row[.])) as $rows

Finally, put the column names before the rows, as a header for the CSV, and pass the resulting row stream to the @csv filter.

$cols, $rows[] | @csv

All together now. Remember to use the -r flag to get the result as a raw string:

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

答案 1 :(得分:52)

瘦子

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' s内部数据类型,而不是JSON背后的概念数据类型。

$ jq -c '.[]' <<<'["a", "b"]'
"a"
"b"
$ jq -cn '"a", "b"'
"a"
"b"

请注意,输出不是数组(可能是["a", "b"])。压缩输出(-c选项)显示每个数组元素(或,过滤器的参数)在输出中成为一个单独的对象(每个都在一个单独的行上)。

流类似于JSON-seq,但在编码时使用换行而不是RS作为输出分隔符。因此,这种内部类型由通用术语&#34;序列&#34;在这个答案中,用&#34; stream&#34;被保留用于编码输入和输出。

构建过滤器

可以使用以下方法提取第一个对象的键:

.[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之前,仍然需要将其转换为序列。

答案 2 :(得分: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($x):
    $x
    |(map(keys)
        |add
        |unique
        |sort
    ) as $cols
    |map(. as $row
        |$cols
        |map($row[.]|tostring)
    ) as $rows
    |$cols,$rows[]
    | @csv;

tocsv(.)

答案 3 :(得分: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 :(得分:1)

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

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