JSON 是一种轻量级的数据交换格式。其采用完全独立于语言的文本格式,具有方便人阅读和编写,同时也易于机器的解析和生成。这些特性决定了 JSON 格式越来越广泛的应用于现代的各种系统中。作为系统管理员,在日常的工作中无论是编辑配置文件或者通过 http 请求查询信息,我们都不可避免的要处理 JSON 格式的数据。
jq 是一款命令行下处理 JSON 数据的工具。其可以接受标准输入,命令管道或者文件中的 JSON 数据,经过一系列的过滤器(filters)和表达式的转后形成我们需要的数据结构并将结果输出到标准输出中。jq 的这种特性使我们可以很容易地在 Shell 脚本中调用它。
jq 是开源软件。目前大部分的 Linux 系统和 Unix 系统的官方软件仓库中均有收录。用户可以通过系统自带的软件包管理器直接安装,也可以手动从源代码编译安装。jq 的源代码可以从其代码仓库中获得。编译 jq 的指令如下:
git clone https://github.com/stedolan/jq.git cd jq autoreconf -i ./configure --disable-maintainer-mode make sudo make install
windows 用户可以通过 Chocolatey NuGet 安装或者直接从官网下载可执行文件。
作为一个标准的命令行工具,jq 支持"-h"选项(或者长格式"--help")。通过该选项,我们可以看到 jq 的简略的使用帮助。在 linux 系统中,我们可以通过 man 命令来查看 jq 详细文档。
jq 可以处理 JSON 文件,也可以直接处理从命令行管道或者流中传入的数据。这方便我们在 shell 脚本中使用。如下面代码:
#xxx.JSON 中是我们要处理的 JSON 数据,我们可以直接将文件名传给 jq $ jq -r '.' xxx.JSON #或者由其他程序读出文件内容,并传给 jq $ cat xxx.JSON|jq -r '.'
需要说明的是 jq 只能接受 well form 的 JSON 字符串作为输入内容。也就是说输入内容必须严格遵循 JSON 格式的标准。所有的属性名必须是以双引号包括的字符串。对象的最后一个属性的末尾或者数组的最后一个元素的末尾不能有逗号。否则 jq 会抛出无法解析 JSON 的错误。
jq 通过命令行选项来控制对输入输出的处理。选项的具体内容请参见图一中的 jq 帮助。这里介绍几个比较常用和重要的选项。
从图 1 的 jq 帮助中我们可以看出,在调用 jq 处理 JSON 数据时有一个必须的部分"jq filters". 实际上,jq 内部构建了一个简易的,功能完备的语言系统。用户在使用 jq 时,需要使用 jq 支持的语法来构建表达式(filters)并将其传给 jq。 jq 根据语法规则解析表达式并应用在输入的 JSON 数据上从而得到需要的结果。
jq 表达式支持串行化操作。一个复杂的表达式可以有多个简单的,以"|"符号分割的,串行化执行的表达式组成。每个表达式以其前边表达式的结果为输入。例如:有 JSON 数据{"name":{"firstname":"Tom","lastname":"Clancy"}}。我们要查询 lastname 属性可以使用表达式'.name|.lastname'。为了方便处理 JSON 数据,jq 提供了以下几点特性支持:
基础表达式(Basic filters)是 jq 提供的基本过滤器,用来访问 JSON 对象中的属性。基础表达式也是实现更复杂查询功能的基础。基础表达式主要有以下几种:
jq 内部支持的数据类型有:数字,字符串,数组和对象(object)。并且在这些数据类型的基础上, jq 提供了一些基本的操作符来实现一些基本的运算和数据操作。列举如下:
jq 中有一种很特殊的运算规则:当运算符的一个或两个操作数是迭代器时,其运算以类似与笛卡尔乘积的方式进行,即把两个操作数中的每一个元素拿出来分别运算。例如:
#result is 5 6 7 8 jq -n '([1,2]|.[])+([4,6]|.[])'
jq 内部支持两种控制结构:判断语句和异常处理. 判断语句的完整结构为 if then-elif then-else-end. 当判断条件的结果为多个值时(迭代器),会对每个值执行一次判断。
异常处理语句的结构为 try <表达式 a> catch <表达式 b>. 当表达式 a 发生异常时,执行表达式 b,且输入为捕捉到的异常信息。如果不需要额外的处理,只是简单的抑制异常信息的输入,可以没有 catch 语句(如 try .a)。这时,整个表达式可以简写为'<表达式 a>?'(如:.a?)。
jq 内部还支持函数。在使用 jq 函数时,我们应该注意区分两个概念:输入和参数。输入可能是整个表达式的输入数据也可能是表达式别的部分的输出。而参数和函数一起构成新的 filter 来处理输入。和其他编程语言不同的是,在调用函数时,多个参数之间以分号分隔。jq 通过内置函数提供了数据处理时常用的操作,例如:过滤,映射,路径操作等。下面分别说明。
在数据处理过程中,我们经常需求将数据从一种形式转换成另外一种形式,或者改变数据的值。jq 提供了两个内置映射函数来实现这种转换:map 和 map_values。其中,map 处理的对象是数组,而 map_values 则处理对象属性的值。map 函数的参数为 filter 表达式。在该 filter 表达式中,'.'代表被映射的元素或值。
输入:[1,2,3,4] jq 表达式:jq -r 'map(.+1)' 输出:[2,3,4,5]
在 jq 中有两种类型的选择过滤操作。第一种是基于数据类型的过滤,如表达式'.[]|arrays'的结果只包含数组。可以用来过滤的类型过滤器有:arrays, objects, iterables, booleans, numbers, normals, finites, strings, nulls, values, scalars。
第二种是 select 函数。select 接受一个条件表达式作为参数。其输入可以是迭代器,或者和 map 函数配合使用来处理数组。当输入中的某个元素使 select 参数中的条件表达式结果为真时,则在结果中保留该元素,否则不保留该元素。
输入:[1,2,3,4] 表达式:jq -r 'map(select(.>2))' 输出:[3,4] 表达式:jq -r '.[]|select(.>2)' 输出:3 4
和 xpath 类似,在 jq 中的 path 也是指从根到某个叶子属性的访问路径。在 jq 中有两种表示路径的方式:数组表示法和属性表示法。属性表示法类似于我们在 filter 中访问某个属性值的方式,如'.a.b'。数组表示法是将路径中的每一部分表示为数组的一个元素。jq 提供了一个内置函数 path 用来实现路径从属性表示法到数组表示法的转换。
jq 还提供了函数用来读取路径的值(getpath), 设置路径的值(setpath)和删除路径(del)。不过遗憾的是,这三个函数对路径的处理并不一致。其中 getpath 和 setpath 只接受数组表示法的路径,而 del 函数只能正确处理属性表示法的路径。
jq 还提供了一个函数 paths 用来枚举可能存在的路径。在没有参数的情况下,paths 函数将输出 JSON 数据中所有可能的路径。paths 函数可以接受一个过滤器,来只输出满足条件的路径。
jq 中提供了一系列的函数用来判断某个元素或者属性是否存在于输入数据中。其中函数 has 和 in 用来判断 JSON 对象或数组是否包含特定的属性或索引。函数 contains 和 inside 用来判断参数是否完全包含在输入数据中。对于不同的数据类型,判断是否完全包含的规则不同。对于字符串,如果 A 是 B 的子字符串,则认为 A 完全包含于 B。对于对象类型,如果对象 A 的所有属性在对象 B 中都能找到且值相同,则认为 A 完全包含于 B。
除了前面讲述的基本操作符外,jq 提供内置函数用于完成数组的扁平化(flatten),反序(reverse),排序(sort, sort_by),比较(min,min_by,max,max_by)和查找(indices,index 和 rindex)。其中 indices 函数的输入数据可以是数组,也可以是字符串。和 index 函数不同的是,其结果是一个包含所有参数在输入数据中位置的数组,具体请参看下面的例子。
#结果是[1,2,3,4] jq -nr '[1,[2,3],4]|flatten' #结果是[3,2,1] jq -nr '[1,2,3]|reverse' jq -nr '[3,1,2]|sort' jq -nr '[{"a":1},{"a":2}]|sort_by(.a)' #下面两个表达式的结果都是[1,3] jq -nr '"abcb"|indices("b")' jq -nr '[1,3,2,3]|indices(3)'
jq 还提供了许多其他的内置函数,具体请参考 jq 的在线文档。
jq 内部支持两种变量的定义方式。第一种我们在前边 jq 的调用部分讲过,可以通过命令行参数(--arg)定义。这种方式用来从外部(如:shell)传入数据以供 filter 表达式使用。
第二种方式,在 jq 表达式内部,我们可以自己声明变量用来保存表达式的结果以供表达式其余部分使用。
jq 中定义变量的语句为: fiterexp as $variablename
#在下面的表达式中变量$arraylen 用来保存数组长度,整个表达式结果为 4 jq -nr '[1,2,3]|length as $arraylen|$arraylen+1' #可以同时定义多个变量 jq -nr '{"firstname":"tom","lastname":"clancy"}|. as {firstname:$fn, lastname:$ln}|"author is "+$fn+"*"+$ln'
jq 中同样存在变量作用域问题。在 jq 中,有两种方法分隔变量作用域。第一种是用括号包围部分表达式。括号内部的表达式与外部的表达式不在同一个作用域范围内。第二种方法是定义函数。默认情况下,声明的变量对其后的表达式可见。但是,如果变量在特定作用域内声明,则对作用域外部的表达式不可见,例如:
#会抛出 arraylen 没定义的异常 jq -nr '[1,2,3]|(length as $arraylen|$arraylen)|$arraylen+1' #正常执行,结果为 4. jq -nr '[1,2,3]|(length as $arraylen|$arraylen+1)' #函数作用域。该表达式会抛出异常,因为变量$fn 是在函数 fname 中定义,对最后一个子表达式##来说,$fn 是不可见的。 jq -nr '{"firstname":"tom","lastname":"clancy"}|def fname:. as {firstname:$fn, lastname:$ln}|$fn; fname|$fn'
我们知道 jq 有一种特殊的数据类型:迭代器。通常,有迭代器参与的运算,其结果也是一个迭代器。jq 提供了一些特殊的语法和内置函数用来缩减迭代器运算结果的个数。
reduce 关键字用来通过运算将迭代器的所有值合并为一个值。其调用形式为:reduce <itexp> as $var (INIT; UPDATE)。其中,表达式 itexp 产生的迭代器被赋值给变量 var, UPDATE 是关于变量 var 的表达式。INIT 是该表达式的初始输入。相对于 itexp 结果中的每个元素,UPDATE 表达式被调用一次,计算出结果用作下一次 UPDATE 调用的输入。
#结果是 6 jq -nr 'reduce ([1,2,3]|.[]) as $item (0; .+$item)' #上面的表达式等同于 jq -nr '0 | (3 as $item|.+$item)|(2 as $item | . + $item)|(1 as $item | . + $item)'
关键字 foreach 的作用和 reduce 类似。其调用形式为 foreach EXP as $var (INIT; UPDATE; EXTRACT)。和 reduce 关键字不同的是,foreach 关键字的每次迭代是先调用 UPDATE 再调用 EXTRACT,并以一个迭代器保留每一次的中间结果。该迭代器最后作为整个表达式的结果输出。
#下面的表达式,结果是 1 3 6 jq -nr 'foreach ([1,2,3]|.[]) as $item (0; .+$item;.)'
内置函数 limit(n;exp)用来取得表达式 exp 结果的前 n 个值。
内置函数 first, last 和 nth。这几个函数用来取迭代器中某一个特定的元素。这几个函数既可以以函数的形式调用,也可以作为子表达式调用。请看下面的示例:
#下面的表达式按照函数的形式调用 first,结果为 1 jq -nr 'first([1,2,3]|.[])' #下面的表达式以 filter 形式调用 first jq -nr '[1,2,3]|.[]|first' #nth 函数的使用,结果为 2 jq -nr 'nth(1;[1,2,3]|.[])'
作为一个类似于编程语言的表达式系统,jq 也提供了定义函数的能力。其语法规则为:def funcname(arguments) : funcbodyexp; 在定义函数时,需要注意下面几条规则。
在很多情况下,函数的参数都是被当作表达式引用的,类似于编程其他语言中的 callback 函数。
def map(f): [.[] | f]; #下面表达式的结果是 20,因为当作参数传入的表达式在函数 foo 中被引用两次 5|def foo(f): f|f;foo(.*2)
如果希望传入的参数只被当作一个简单的值来使用,则需要把参数的值定义为一个同名变量,并按照使用变量的方式引用。
#下面表达式结果为 10,传入的表达式'.*2'在函数 foo 中首先被求值。 5|def foo(f): f as $f|$f|$f;foo(.*2) #上面的表达式可以简写为如下形式,注意,引用参数时必须带$。 5|def foo($f): $f|$f;foo(.*2) #否则等于直接引用参数中的表达式。 #例如下面的表达式结果为 20 5|def foo($f): $f|f;foo(.*2)
函数内部可以定义子函数。利用这个特性我们可以实现递归函数。
#下面表达式的结果是 15 jq -nr '[1,2,3,4,5]|def total: def _t: .|first+(if length>1 then .[1:]|_t else 0 end); _t;total'
除了在表达式内部定义函数外,我们可以把自定义函数写在外部文件中形成单独的类库。jq 有一套完整的模块系统来支持自定义类库。
首先,可以通过命令行参数'-L'来指定 jq 搜索模块时需要搜索的路径。
其次,在模块内部,可以通过 import 指令和 include 指令来实现互相引用。在引用指令中,有几个特殊的路径前缀需要说明。
当通过 import 指令引用一个模块 foo/bar 时, jq 会在搜素路径中查找 foo/bar.jq 或者 foo/bar/bar.jq。
本文简要介绍了命令行 JSON 处理工具 jq 的使用。jq 提供了类是于编程语言的表达式系统。从笔者的经验来看,目前介绍的特性能够满足系统管理工作中的大部分需求。如果需要实现更复杂的功能,请参考 jq 的官方文档和 github 项目源代码库。