【问题标题】:JSON to CSV: variable number of columns per rowJSON 到 CSV:每行的列数可变
【发布时间】:2019-06-12 19:34:06
【问题描述】:

我需要将 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 两遍,第一遍将收集“标签”数组的最大长度,第二遍将创建 CSV,因为此时结果列的数量是已知的。但是jq没有全局变量的概念,所以不知道哪里可以存放running total。

我希望能够通过 CLI 在 Windows 上执行此操作。 提前谢谢你。

【问题讨论】:

    标签: json windows csv command-line jq


    【解决方案1】:

    问题显示的是 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。希望您可以根据需要将上述内容调整到您的 shell 中——也许这个链接会有所帮助: 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 '
    

    结语

    顶级 JSON 数组的文件太大而无法放入内存,可以通过使用 --stream 选项调用 jq 将其转换为数组项的流,例如如下:

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

    【讨论】:

    • 哦,对了,我们可以在同一个文件中读取两次。 ...实际上,也许不是,文件被锁定了。
    • @peak 这太棒了,非常感谢您的帮助!浏览每个示例并理解算法需要一些时间。我的示例可能误导了您,但就我而言,标签的数量与当前的行号无关。在第一个示例中,您计算​​了标签数组的最大长度,它应该可以工作。但是在第二个和第三个示例中,在我看来,您认为标签的最大数量等于不应与我的数据一起使用的总行数。如果我错了,请纠正我。
    • 已修复。 (我希望)。发送。
    【解决方案2】:

    对于如此大的文件,您可能希望在两个单独的调用中执行此操作,一个用于获取计数,另一个用于实际输出 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
    

    【讨论】:

    • 非常感谢@jeff-mercado!花了一段时间才弄清楚流在 jq 中是如何工作的,但这是值得的。在我的特定用例中,我不使用流,因为我的输入数据有相当多的小 JSON 对象,而不是一个大 JSON。但无论如何,这是有益的!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-19
    • 2019-11-16
    • 1970-01-01
    • 2016-04-15
    • 2017-07-29
    • 2021-12-30
    相关资源
    最近更新 更多