JSON转换为CSV:每行可变的列数

时间:2019-06-12 19:34:06

标签: json windows csv command-line jq

我需要将JSON转换为CSV,其中JSON具有可变长度的数组,例如:

JSON对象:

{"labels": ["label1"]}
{"labels": ["label2", "label3"]}
{"labels": ["label1", "label4", "label5"]}

生成的CSV:

labels,labels,labels
"label1",,
"label2","label3",
"label1","label4","label5"

源JSON中还有许多其他属性,为简单起见,这只是摘录。

此外,我不得不说该过程必须将JSON作为流使用,因为源JSON可能非常大(> 1GB)。

我想使用jq进行两次传递,第一次传递将收集'labels'数组的最大长度,第二次传递将创建CSV,因为此时已知结果列数。但是jq没有全局变量的概念,因此我不知道可以在哪里存储运行总计。

我希望能够在Windows上通过CLI做到这一点。 预先谢谢你。

2 个答案:

答案 0 :(得分:2)

该问题显示了JSON对象流,因此以下解决方案假定输入文件已经是如图所示的序列。这些解决方案也可以轻松调整,以涵盖输入文件包含大量对象(例如对象)的情况。如结尾所讨论的。

两次调用解决方案

这是使用两次jq的两次通过解决方案。如果您有,则演示文稿假定类似bash的环境:

n=$(jq -n 'reduce (inputs|.labels|length) as $i (-1;
  if $i > . then $i else . end)' stream.json)
jq -nr --argjson n $n '
  def fill($n): . + [range(length;$n)|null];
  [range(0;$n)|"labels"],
  (inputs | .labels | fill($n))
  | @csv' stream.json

假设输入内容如上所述,则可以保证产生有效的CSV。希望您可以根据需要将以上内容调整为适合您的外壳程序-也许此链接会有所帮助 Assign output of a program to a variable using a MS batch file

使用input_filename和一次调用jq

不幸的是,jq没有“倒带”功能,但是 还有一种选择:在一次jq调用中两次读取文件。这比上面的两次调用解决方案更麻烦,但是避免了与后者相关的任何困难。

cat sample.json | jq -nr '

  def fill($n): . + [range(length;$n)|null];
  def max($x): if . < $x then $x else . end;

  foreach (inputs|.labels) as $in ( {n:0};
    if input_filename == "<stdin>" 
    then .n |= max($in|length)
    else .printed+=1
    end;
    if .printed == null then empty
    else .n as $n
    | (if .printed == 1 then [range(0;$n)|"labels"] else empty end),
      ($in | fill($n))
    end)
  | @csv'  -  sample.json

另一个单调用解决方案

以下解决方案使用特殊值(此处为null)来描述两个流:

(cat stream.json; echo null; cat stream.json) | jq -nr '
  def fill($n): . + [range(length; $n) | null];
  def max($x): if . < $x then $x else . end;

  (label $loop | foreach inputs as $in (0; 
     if $in == null then . else max($in|.labels|length) end;
     if $in == null then ., break $loop else empty end)) as $n
  | [range(0;$n)|"labels"],
    (inputs | .labels | fill($n))
  | @csv '

Epilog

通过使用--stream选项调用jq可以将顶级JSON数组太大而无法容纳到内存中的文件转换为数组项的流。如下:

jq -cn --stream 'fromstream(1|truncate_stream(inputs))'

答案 1 :(得分:1)

对于这么大的文件,您可能需要在两个单独的调用中执行此操作,一个调用获取计数,然后另一个调用实际输出csv。如果您想将整个文件读入内存,则可以一次完成,但我们绝对不希望这样做,我们希望将其以流形式传输。

在将命令结果存储到变量中时,事情变得有些丑陋,写入文件可能更简单。但是我宁愿不要使用临时文件。

REM assuming in a batch file
for /f "usebackq delims=" %%i in (`jq -n --stream "reduce (inputs | .[0][1] + 1) as $l (0; if $l > . then $l else . end)" input.json`) do set cols=%%i
jq -rn --stream --argjson cols "%cols%" "[range($cols)|\"labels\"],(fromstream(1|truncate_stream(inputs))|[.[],(range($cols-length)|null)])|@csv" input.json

> jq -n --stream "reduce (inputs | .[0][1] + 1) as $l (0; if $l > . then $l else . end)" input.json

对于第一次获取列数的调用,我们只是利用了以下事实:可以使用指向数组值的路径来指示数组的长度。我们只想获取所有项目的最大值。


> jq -rn --stream --argjson cols "%cols%" ^
"[range($cols)|\"labels\"],(fromstream(1|truncate_stream(inputs))|[.[],(range($cols-length)|null)])|@csv" input.json

然后输出其余的内容,我们只是采用labels数组(假设它是对象上的唯一属性),并用null填充它们,直到$cols个数。然后输出为csv。


如果标签与此处的示例位于不同的深度嵌套路径中,则需要根据适当的路径进行选择。

set labelspath=foo.bar.labels
jq -rn --stream --argjson cols "%cols%" --arg labelspath "%labelspath%" ^
"($labelspath|split(\".\")|[.,length]) as [$path,$depth] | [range($cols)|\"labels\"],(fromstream($depth|truncate_stream(inputs|select(.[0][:$depth] == $path)))|[.[],(range($cols-length)|null)])|@csv" input.json