如何在命令行中优雅地处理JSON

2019-12-24 ⏳2.1分钟(0.8千字)

当年刚出道的时候,整天使用 grep/sed/awk 处理各种文本日志。那时候的日志都是一行一条,内容使用特定分割符。使用各种 unix 命令配合管道真可谓得心应手。然而,到了 9102 年,好多数据都以 json 形式传输和存储。awk 这类工具在 json 面前就有点力不从心了,毕竟是几十年前开发的工具。那有没有针对 json 专门开发的工具呢?答案是肯定的,这就是本文要介绍的 jq 命令。

jq 支持各种 unix 系统,请自行安装。现在单讲几种使用场景。

先来一个段 json 数据,内容如下(并写入 a.json 文件):

{"code":0,"message":"0","ttl":1,
"data":{"results":[
{"item_id":1342,"type":1},
{"item_id":1785,"type":2},
{"item_id":1413,"type":3}]}}

首先要做的就是格式化,这是 jq 最简单的功能:

cat a.json | jq ''

输出效果如下(还有语法高亮):

使用 jq 格式化 json

如果 json 内容很长,则可以使用管道传给 less 命令:

cat a.json | jq '' | less

如果你自己试一下就会发现,less 展示的结果没有颜色,这怎么能忍?

终端下的颜色是由转义序列控制的。有兴趣的同学可以参考我的另一篇文章多彩的终端。像 jq 这类的工具会检测输出目的地是否为终端环境(tty),如果不是(比如管道或普通文件) 就不会输出颜色转义序列,否则这些转义序列会破坏文件内容。

但如果确定读写双方都支持转义序列,我们就可以强制开启颜色输出:

jq -C '' a.json|less -R

这里用到了 jq 的 -c参数和 less 的 -R 参数。


现在介绍 jq 的高级操作。还是以上面的 json 为例。

{
  "code": 0,
  "message": "0",
  "ttl": 1,
  "data": {
    "results": [
      {
        "item_id": 1342,
        "type": 1
      },
      {
        "item_id": 1785,
        "type": 2
      },
      {
        "item_id": 1413,
        "type": 3
      }
    ]
  }
}

显然,这是一个比较负杂的数据结构。我们可以使用 jq 提取部分字段

$ cat a.json | jq '.code, .ttl'
0
1

提取字段使用. 操作,提取多个字段可以使用, 连接。

如果我们想提取 results 内容,则可以这样:

$ cat a.json | jq '.data.results'
[
  {
    "item_id": 1342,
    "type": 1
  },
  {
    "item_id": 1785,
    "type": 2
  },
  {
    "item_id": 1413,
    "type": 3
  }
]

是不是清真多了?

传统的 unix 工具是以行为单位,而 json 数组的每个元素可能有复杂的数据结构(多行),我们能否以元素为单位处理 json 数据呢?当然可以。

提取数组的元素字段

$ cat a.json | jq '.data.results' | jq '.[].item_id'
1342
1785
1413

提取多个数组元素字段

$ cat a.json | jq '.data.results' | jq '.[]|.item_id,.type'
1342
1
1785
2
1413
3

结果是 item_id 和 type 交替出现,传统的 unix 工具依然不好处理。能将同一元素的不同字段放到一行吗?当然可以:

$ cat a.json | jq '.data.results' |jq '.[] |"item_id:\(.item_id) type:\(.type)"'
"item_id:1342 type:1"
"item_id:1785 type:2"
"item_id:1413 type:3"

这里用到了所谓的 string interpolation 语法,有点像 swift 语言。好了,现在可以配合其他 uinx 工具对数据进行处理了。

jq 还支持很多高级特性,有兴趣的同学可以参考官方手册。本文的重点是介绍 jq 最核心的 20% 功能,基本可以解决 80% 的问题。

@2023 再补充一个过滤功能。比如我们可以使用 select 语法只输出某个 type 的数据

$ cat a.json | jq '.data.results|.[]|select(.type==2)|.item_id,.type'
1785
2

先写这么多,有问题请留言讨论。