使用jq将JSON与数组转换为CSV

时间:2017-11-03 11:41:07

标签: json csv jq

我发现自己身处JSON世界,我正试图使用​​jq转换出来。我正在尝试将以下结构转换为CSV:

{
  "Action": "A1",
  "Group": [
    {
      "Id": "10",
      "Units": [
        "1"
      ]
    }
  ]
}
{
  "Action": "A2",
  "Group": [
    {
      "Id": "11",
      "Units": [
        "2"
      ]
    },
    {
      "Id": "20",
      "Units": []
    }
  ]
}
{
  "Action": "A1",
  "Group": [
    {
      "Id": "26",
      "Units": [
        "1",
        "3"
      ]
    }
  ]
}
{
  "Action": "A3",
  "Group": null
}

其中Ids介于10-99和1-5之间。预期的输出将是(引用或不引用,逗号分隔与否,为清晰起见,我使用了管道分隔符):

Action|Group|Unit1|Unit2|Unit3|Unit4|Unit5
A1|10|1|0|0|0|0
A2|11|0|1|0|0|0
A2|20|0|0|0|0|0
A1|26|1|0|1|0|0
A3|0|0|0|0|0|0

我已经玩了一段时间了(history | grep jq | wc -l说107)但是没有取得任何真正的进展将键与彼此组合,我基本上只是得到键列表({{ 1}} n00b)。

更新:

测试解决方案(抱歉,有点不对)我注意到数据也有jq s的记录,即:

"Group": null

(以上几行添加到主测试数据集中)导致错误:{ "Action": "A3", "Group": null } 。预期的产出将是:

jq: error (at file.json:61): Cannot iterate over null (null)

有一个简单的方法吗?

4 个答案:

答案 0 :(得分:2)

如果事先不知道单位列的集合,这是一般解决方案:

def normalize: [            # convert input to array of flattened objects e.g. 
      inputs                # [{"Action":"A1","Group":"10","Unit1":"1"}, ...]
    | .Action as $a
    | .Group[]
    |   {Action:$a, Group:.Id}
      + reduce .Units[] as $u ({};.["Unit\($u)"]="1")
  ];

def columns:                # compute column names
  [ .[] | keys[] ] | unique ;

def rows($names):           # generate row arrays
    .[] | [ .[$names[]] ] | map( .//"0" );

normalize | columns as $names | $names, rows($names) | join("|")

示例运行(假定filter.jq中的过滤器和data.json中的数据)

$ jq -Mnr -f filter.jq data.json
Action|Group|Unit1|Unit2|Unit3
A1|10|1|0|0
A2|11|0|1|0
A2|20|0|0|0
A1|26|1|0|1

Try it online!

在这个特定问题中,unique完成的排序与我们想要的列输出相匹配。如果不是这种情况columns会更复杂。

很多复杂性来自处理不了解最终的单位列集。如果单位组固定且相当小(例如1-5),则可以使用更简单的过滤器:

  ["\(1+range(5))"] as $units
| ["Action", "Group", "Unit\($units[])"]
, ( inputs 
  | .Action as $a 
  | .Group[] 
  | [$a, .Id, (.Units[$units[]|[.]] | if .!=[] then "1" else "0" end) ]
) | join("|")

示例运行

$ jq -Mnr '["\(1+range(5))"] as $units | ["Action", "Group", "Unit\($units[])"], (inputs | .Action as $a | .Group[] | [$a, .Id, (.Units[$units[]|[.]] | if .!=[] then "1" else "0" end) ] ) | join("|")' data.json
Action|Group|Unit1|Unit2|Unit3|Unit4|Unit5
A1|10|1|0|0|0|0
A2|11|0|1|0|0|0
A2|20|0|0|0|0|0
A1|26|1|0|1|0|0

Try it online at tio.runjqplay.org

要处理Group可能null的情况,最简单的方法是使用peak的变体建议。 E.g

  ["\(1+range(5))"] as $units
| ["Action", "Group", "Unit\($units[])"]
, ( inputs 
  | .Action as $a 
  | ( .Group // [{Id:"0", Units:[]}] )[]   # <-- supply default group if null
  | [$a, .Id, (.Units[$units[]|[.]] | if .!=[] then "1" else "0" end) ]
) | join("|")

Try it online at tio.runjqplay.org

答案 1 :(得分:2)

这适用于&#34; Unit&#34;的数量。列(n)是事先已知的。它只是@jq170717实现的变体。

如果max的给定值太小,n的使用是为了确保合理的行为。在这种情况下,输出中的列数会有所不同。

以下已经使用jq版本1.5和master进行了测试;请参阅下面的早期版本的jq所需的调整。

调用:jq -nr -f tocsv.jq data.json

tocsv.jq:

# n is the number of desired "Unit" columns
def tocsv(n):
  def h: ["Action", "Group", "Unit\(range(1;n+1))"];
  def i(n): reduce .[] as $i ([range(0;n)|"0"]; .[$i]="1");
  def p:
    inputs 
    | .Action as $a 
    | .Group[] 
    | [$a, .Id] + (.Units | map(tonumber-1) | i(n));
  h,p | join(",") ;

tocsv(5)

上述内容的编写方式是,只要您想要获得所有好处,就可以通过拨打join@csv来简单地将来电替换为@tsv。但是,在这种情况下,您可能希望在指标函数0中使用1"0"而不是"1"i

验证

$ jq -nr -f tocsv.jq data.json
Action,Group,Unit1,Unit2,Unit3
A1,10,1,0,0
A2,11,0,1,0
A2,20,0,0,0
A1,26,1,0,1

jq 1.3或jq 1.4

对于jq 1.3或1.4,将inputs更改为.[],并使用以下咒语:

 jq -r -s -f tocsv.jq data.json

更新

处理"Group":null案例的最简单方法可能是在| .Group[]之前添加以下行:

| .Group |= (. // [{Id:"0", Units:[]}])

这样你也可以轻松改变&#34;默认&#34;值&#34; Id&#34;。

答案 2 :(得分:2)

这是针对预先知道“单位”列数(n)未知的情况。它避免一次读取整个文件,并进行三个主要步骤:通过“概要”以紧凑的形式收集相关信息; n是计算的;并且形成了整行。

为简单起见,以下内容适用于jq 1.5或更高版本,并使用@csv。如果使用jq 1.4,则可能需要进行小的调整,具体取决于有关输出的详细要求。

调用

jq -nr -f tocsv.jq input.json

tocsv.jq:

# Input: a stream of JSON objects.
# Output: a stream of arrays.
def synopsis:
  inputs 
  | .Action as $a 
  | .Group[] 
  | [$a, .Id, (.Units|map(tonumber-1))];

# Input: an array of arrays
# Output: a stream of arrays suitable for @csv
def stream:
  def h(n): ["Action", "Group", "Unit\(range(1;n+1))"];
  def i(n): reduce .[] as $i ([range(0;n)|0]; .[$i]=1);
  (map(.[2] | max) | max + 1) as $n
  | h($n),
    (.[] | .[0:2] + (.[2] | i($n)))
  ;

[synopsis] | stream | @csv

输出

"Action","Group","Unit1","Unit2","Unit3"
"A1","10",1,0,0
"A2","11",0,1,0
"A2","20",0,0,0
"A1","26",1,0,1

更新

处理"Group":null案例的最简单方法可能是在| .Group[]之前添加以下行:

| .Group |= (. // [{Id:"0", Units:[]}])

这样您也可以轻松更改“Id”的“默认”值。

答案 3 :(得分:1)

对于预先知道“单位”列数(n)未知的情况,这是一个具有最小内存要求的解决方案。在第一遍中,计算n。

stream.jq

这是第二遍:

# Output: a stream of arrays.
def synopsis:
  inputs
  | .Action as $a
  | .Group |= (. // [{Id:0, Units:[]}])
  | .Group[] 
  | [$a, .Id, (.Units|map(tonumber-1))];

def h(n): ["Action", "Group", "Unit\(range(1;n+1))"];

# Output: an array suitable for @csv
def stream(n):
  def i: reduce .[] as $i ([range(0;n)|0]; .[$i]=1);
  .[0:2] + (.[2] | i) ;

h($width), (synopsis | stream($width)) | @csv

调用

jq -rn --argjson width $(jq -n '
  [inputs|(.Group//[{Units:[]}])[]|.Units|map(tonumber)|max]|max
  ' data.json) -f stream.jq data.json

输出

这是附加了“null”记录({“Action”:“A3”,“Group”:null})的输出:

"Action","Group","Unit1","Unit2","Unit3"
"A1","10",1,0,0
"A2","11",0,1,0
"A2","20",0,0,0
"A1","26",1,0,1
"A3",0,0,0,0