一个叫木头,一个叫马尾

详解jq中强大的reduce操作

很多语言中都有 reduce 操作。比如 JavaScript中的 Array.prototype.reduce; 再比如 Java 中的 java.util.stream.Stream#reduce()

reduce中文为减少、归纳之意,在程序语言中,通常用于把一组数据归并为单个数据(或将多个数据转换为另一种数据)。这也是为什么上述两种语言把 reduce 操作挂载到 ArrayStream中的原因,它们都是对一组数据的封装。

以 JavaScript 为例,如果需要对数组内数字求和,我们通常会借助reduce方法:

const numbers = [1234];
const sum = numbers.reduce((p, v) => p + v, 0);
// 打印 10
console.log(sum);

其实jq中的reduce有类似的道理,让我们细细对其分解吧。


reduce 语法

其语法如下:

reduce _inputs_ as $line (INIT; REDUCER)

其中:

对该说明仍然感到晦涩?那我们进入实战环节。

实站

1. 对数字数组求和

模仿上面JS代码的例子,使用jq可以这么写:

echo '[1, 2, 3, 4]' |
jq 'reduce .[] as $item (0; .+$item)'

解释:

我们让 jq 处理 [1, 2, 3, 4] 数组,每次循环时,把当前元素(即.[])赋值给 $item 变量;初始值设为 0,每次用当前的累加值(.,第一次循环时和初始值相等)和元素的值($item)相加,作为下一次的累加值。

整个过程伪代码如下:

init = 0;
sum = init;
loop: 
  $item = next();
  sum += $item;
return sum;

终端执行结果如下:

jq对数组进行求合
jq对数组进行求合

2. 对象合并

之前写过一篇用jq对JSON对象进行合并的文章: jq的两个特殊用法示例[1],其实reduce也能合并对象。

jq+ 操作符进行了重载,如果是两个对象相加,则返回一个合并了双方属性的新对象:

jq -n '{"a":1}+{"b":2}'

其返回的是:

{
  "a"1,
  "b"2
}

由此,我们易知下面指令也可以得到相同的结果:

echo '[{"a":1},{"b":2}]' |
jq 'reduce .[] as $item ({}; .+$item)'

注意,在这里初始值由 0 变成了 {},因为我们不是对数字求和,而是要合并对象。

延伸与进阶

更简单的汇总方法

jq中,-s/--slurp选项可以把多个独立的对象当成一个完整的数组来处理:

echo 1 2 3 | jq -s

上述命令会返回:

[
  1,
  2,
  3
]

因此,在需要对一系列数字进行求和时,可以像下面这样操作,甚至不用手动组装数组了:

# 返回6
echo 1 2 3 |
  jq -s 'reduce .[] as $e (0; .+$e)'

对于这类汇总操作,jq甚至内置了一个 add 函数用于简化流程:

echo 1 2 3 | jq -s 'add'

更高效地处理大数据集

有的数据导出工具会把表中的每一条记录以JSON格式作为一个单独的行导出,类似下面这样:

导出的 rows.json 文件:

{"a":1}
{"a":2}
{"a":100}

观察可知,这并不是有效的JSON文件(每一行的作为独立的JSON是有效的,文件的整体内容不是有效的JSON格式),如果我们需要用 jq 对所有的a的值求和,如何操作呢?

首先,我们并不能这样操作:

jq '.a | add' rows.json

因为这要求 .a 返回的是一个数组,实际上其返回的是一列数字:

解决办法是借助上面介绍的-s选项,并利用map方法提取出所有数值:

最终的求和表达式如下:

jq -s 'map(.a) | add' rows.json
jq对按行记录的json数据求和
jq对按行记录的json数据求和

这确实解决了我们的问题,却引出了另一个问题。

rows.json数据量比较小时,这样做是可行的;但当数据量很大时,jq需要将文件内容全部加载进内存后才能操作,这样很明显是内存不友好的。

庆幸的是,reduce也允许我们按行进行解析处理:

# 输出 103
jq -n 'reduce inputs as $line (0; .+$line.a)' rows.json

注意 -n 参数的使用,不加此参数,会忽略第一行;这里使用了内置的 inputs 函数,其会将剩余的输入逐一输出,而这正是我们要的。

参考


[1]

jq的两个特殊用法示例: https://beloved.family/wx/jq的两个特殊用法示例

[2]

jq官方手册: https://stedolan.github.io/jq/manual/

[3]

jq reduce: https://blog.differentpla.net/blog/2019/01/11/jq-reduce/

[4]

Memory usage of jq's --slurp option: https://stackoverflow.com/questions/34778425/memory-usage-of-jqs-slurp-option

[5]

Why does inputs skip the first line of the input file?: https://stackoverflow.com/questions/55995980/why-does-inputs-skip-the-first-line-of-the-input-file