使用 jq 解析 json 数据

分析 Json 格式接口参数或返回值。

jq 基本用法

Github找到最新的版本,比如 1.6,下载:

1
wget -O jq https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64

下面是示例数据test.json,后续例子都是基于这个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
"data": {
"userInfo": {
"name": "Mike"
},
"range": [
{
"time": "20190823",
"type": "AM"
},
{
"time": "20190823",
"type": "PM"
},
{
"time": "20190824",
"type": "PM"
}
],
"one2many": [
{
"id": "1",
"list": [
{
"name": "abc",
"pwd": "123"
},
{
"name": "Mike",
"pwd": "456"
}
]
}
]
}
}

过滤器(Filter)

jq 的执行是基于“过滤器”的概念的,由过滤器来决定如何将数据聚合、转换或排除。

jq operates by way of filters: a series of text commands that you can string together, and which dictate how jq should transform the JSON you give it.

过滤器包括一些操作符(operator):

  • dot:.
    .保持输入不变,如果输入是一个复杂对象(被{}包围),则可以在.后面带上对应的 key 来获取其值。
    . leaves the input unmodified. Add the name of a key to it, however, and the filter will return the value of that key.
  • array operator:[]
    . 会直接返回一个大的 json 数组,如果要访问数组里的每一项,需要在对象后加上[]
    当然如果要访问数组中的某一项,可以通过向数组里传索引来获取,比如[1]
  • pipe:|
    The magic of jq is that you can connect, or pipe, several operators together to accomplish some very complex transformations of your data. What’s more, jq will repeat the filter for each JSON object provided by the previous step. Therefore, while we started with just one big JSON object, .artObjects[] created 10 smaller JSON objects. Any operator we put after the | will be repeated for each of these objects.
  • create new json:[]{}
    用于将结果合成为新的数组或对象。

一些函数过滤器:

  • select
    过滤掉一部分结果到下一步,一般和|组合使用。
  • group_by
    输入数组,将某个字段相同的对象整合到同一个子数组内,这么说起来有点抽象,下面是一个简单例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    [
    "a",
    "b",
    "a"
    ]
    $ ./jq -r 'group_by(.)' test1.json
    ->
    [
    [
    "a",
    "a"
    ],
    [
    "b"
    ]
    ]
  • csv
    用于将数组结果转换为 csv 格式输出。

下面是一个最简单的例子:

1
./jq -r '.data.range[] | {time: .time, type: .type}' test.json

但是需要注意的是,不能想当然地认为 jq 可以为我们处理复杂的关系,比如下面的命令结果是 4 个对象而不是 2 个:

1
./jq -r '.data.one2many[] | {id: .id, name: .list[].name, pwd: .list[].pwd}' test.json

如果只获取 one2many 数组里的一个属性则可以正常输出,下面的语句输出 2 个对象:

1
./jq -r '.data.one2many[] | {id: .id, name: .list[].name}' test.json

费了蛮大功夫在教程里找这种情况的解决方案,未发现jq对 one2many 关系的查询有很好的支持,只能通过编写 shell 脚本一层一层解析 json 了,我将示例放到下面的应用中了。

select

select 是一种过滤器函数,用于选出结果集合中的部分:

1
./jq -r '.data.one2many[].list[] | select(.name == "abc")' test.json

group_by

按某个字段分组,比如希望将同一天的数据放到一块,统计每一天有多少个 type:

1
./jq -r '.data.range | group_by(.time) | .[] | {time: .[0].time, type: [.[].type | tostring] | join(";")}' test.json

也可以用于去重并统计出现次数,下面的脚本用于统计每一天有多少个不同的 type:

1
2
3
./jq -r '.data.range | group_by(.time) | .[] | {time: .[0].time, type: [.[].type | tostring]} | {time: .time, count: .type | length} | [.time, .count] | @csv' test.json
下面这种写法也是可以的:
./jq -r '.data.range | group_by(.time) | .[] | {time: .[0].time, type: [.[].type | tostring]} | [.time, (.type | length)] | @csv' test.json

格式化

格式化输出:

1
jq -r '.' test.json

csv 格式输出:

1
jq -r '.data.range[] | [.time, .type] | @csv' test.json

传参

  • –arg,传递一个字符串参数,注意不能传数字,下面的脚本执行时会报错:
    1
    2
    $ ./jq -r --arg i 1 '.data.one2many.list[$i]' test.json
    jq: error (at test.json:30): Cannot index array with string "1"
  • –argjson,相对来说更自由一点,可以传数字或其他复杂的 json 结构:
    1
    $ ./jq -r --argjson i 1 '.data.one2many.list[$i]' test.json

应用

One-to-many

正如前面所述,jq对 one2many 结构没有特别好的支持,如果需要获取更多的属性,则只能按 json 格式层层遍历,写出类似下面这样的脚本:

1
2
3
4
5
6
7
8
9
i=0
while true; do
json=$(./jq --argjson i $i -r '.data.one2many[].list[$i]' test.json)
if [[ "$json" == "" || "$json" == "null" ]]; then
break
fi
i=$(($i+1))
echo $json
done

也不方便使用select之类的函数来过滤,因为多个属性组合的结果是笛卡尔积,下面表格中的 item2 和 item4 都是冗余的,但是因为不知道输出的规律,过滤起来比较麻烦(其实是按 json 中数组的顺序 dfs)。

字段 name pwd
item1 name1 pwd1
item2 name1 pwd2
item3 name2 pwd1
item4 name2 pwd2

通过线上服务器接口统计数据的一份快照

下面是实际工作中基于jq统计数据的一个脚本:

因为目标业务数据并没有入库,都是存到缓存里的,只能通过调对应的接口来获取数据当时的一份快照,虽不准确,但对评估需求有一定的参考意义。
具体统计的什么业务数据并不重要,只是在脚本编写上提供一个参考。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#!/usr/bin/env bash
set -o errexit
set -o nounset
# 下载医院详情数据(包含科室)
curl -s http://10.32.128.178:8080/product/wx/baseinfo?hosCode=H109471\&channel=wechat -o baseinfo.json
# 过滤出所有科室编码
./jq -r '.data.departDetails[].subDepartments[] | [.hosCode, .parentDepartCode, .departCode] | @csv' baseinfo.json > depts.txt
# 下载每个科室的号源
while read line
do
# 匹配"hosCode","firstDeptCode","secondDeptCode",像H123,,这样的都会被忽略
if [[ $line =~ ^\"(.+)\",\"(.+)\",\"(.+)\"$ ]];then
curl -s http://10.32.128.178:8080/door/product/info?hosCode=${BASH_REMATCH[1]}\&firstDeptCode=${BASH_REMATCH[2]}\&secondDeptCode=${BASH_REMATCH[3]} -o ${BASH_REMATCH[1]}-${BASH_REMATCH[2]}-${BASH_REMATCH[3]}.json
#else
#echo "Not match"
fi
done < depts.txt
# 遍历号源列表过滤出0元号源
echo > result.csv
while read line
do
if [[ $line =~ ^\"(.+)\",\"(.+)\",\"(.+)\"$ ]];then
# 1、一次性extract全部属性没有意义,因为filter时会计算所有属性的笛卡尔积
# 之后不管是用jq内置的unique_by还是awk、uniq都无法方便地得到正确结果(相当于找到矩阵的主对角线)
# 按hosCode-firstDeptCode-secondDeptCode-date-showTimeType-doctorId求unique,但uniq这些命令只能过滤出第一条
# ./jq --arg hosCode ${BASH_REMATCH[1]} --arg firstDeptCode ${BASH_REMATCH[2]} --arg secondDeptCode ${BASH_REMATCH[3]} -r '.data.productRedis[] | {hosCode: $hosCode, firstDeptCode: $firstDeptCode, secondDeptCode: $secondDeptCode, date: .date, showTimeType: .metaProductItems[].showTimeType, doctorId: .metaProductItems[].doctorId, doctorName: .metaProductItems[].doctorName, sellPrice: .metaProductItems[].sellPrice, count: .metaProductItems[].count} | [.hosCode, .firstDeptCode, .secondDeptCode, .date, .doctorId] | @csv' ${BASH_REMATCH[1]}-${BASH_REMATCH[2]}-${BASH_REMATCH[3]}.json >> products.txt
# 2、硬核双重循环
echo "开始查询"${BASH_REMATCH[1]}"-"${BASH_REMATCH[2]}"-"${BASH_REMATCH[3]}
i=0
while true
do
# 传递数值参数必须用--argjson,如果用--arg会被解析为字符串,就会报错
productJson=$(./jq --argjson i $i -r '.data.productRedis[$i]' ${BASH_REMATCH[1]}-${BASH_REMATCH[2]}-${BASH_REMATCH[3]}.json)
# 如果为空则退出
if [[ "$productJson" == "" || "$productJson" == "null" ]];then
break
fi
i=$(($i+1))
# 每个product一个date
date=$(echo $productJson | ./jq -r '.date')
echo "开始查询"$date
# 遍历所有items
j=0
while true
do
productItemJson=$(echo $productJson | ./jq --argjson j $j -r '.metaProductItems[$j]')
if [[ "$productItemJson" == "" || "$productItemJson" == "null" ]];then
break
fi
# echo $productItemJson
j=$(($j+1))
# 输出到result.csv文件
echo $productItemJson | ./jq --arg hosCode ${BASH_REMATCH[1]} --arg firstDeptCode ${BASH_REMATCH[2]} --arg secondDeptCode ${BASH_REMATCH[3]} --arg date $date \
-r 'select(.sellPrice == 0) | [$hosCode, $firstDeptCode, $secondDeptCode, $date, .showTimeType, .doctorId, .doctorName, .doctorTitle, .sellPrice, .count] | @csv' \
>> result.csv
done
done
#else
#echo "Not match"
fi
done < depts.txt

脚本运行完毕后会生成一大堆临时文件,随后需要清理一下:

1
2
3
4
5
6
7
8
9
# 清理
while read line
do
if [[ $line =~ ^\"(.+)\",\"(.+)\",\"(.+)\"$ ]];then
rm ${BASH_REMATCH[1]}-${BASH_REMATCH[2]}-${BASH_REMATCH[3]}.json
fi
done < depts.txt
rm depts.txt
rm baseinfo.json

参考

  1. Reshaping JSON with jq
  2. jq Manual (development version)
  3. stedolan/jq - Cookbook