我需要将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做到这一点。 预先谢谢你。
答案 0 :(得分:2)
该问题显示了JSON对象流,因此以下解决方案假定输入文件已经是如图所示的序列。这些解决方案也可以轻松调整,以涵盖输入文件包含大量对象(例如对象)的情况。如结尾所讨论的。
这是使用两次jq的两次通过解决方案。如果您有wsl,则演示文稿假定类似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 '
通过使用--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