很多语言中都有 reduce
操作。比如 JavaScript中的 Array.prototype.reduce
; 再比如 Java 中的 java.util.stream.Stream#reduce()
。
reduce中文为减少、归纳之意,在程序语言中,通常用于把一组数据归并为单个数据(或将多个数据转换为另一种数据)。这也是为什么上述两种语言把 reduce
操作挂载到 Array
和 Stream
中的原因,它们都是对一组数据的封装。
以 JavaScript 为例,如果需要对数组内数字求和,我们通常会借助reduce
方法:
const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((p, v) => p + v, 0);
// 打印 10
console.log(sum);
其实jq
中的reduce
有类似的道理,让我们细细对其分解吧。
reduce
语法其语法如下:
reduce _inputs_ as $line (INIT; REDUCER)
其中:
INIT
: 提供一个初始值,通常是{}
或[]
。如果是对数字求和,也可以传一个数字0
,像上面JavaScript代码中那样;REDUCER
: 是一个转换函数。在它这里, .
代表上次归并后的值,$line
是当前要处理的值,函数返回的是一个新的归并结果(供下次归并时使用,或者全部归并完成后作为最后的返回值)。对该说明仍然感到晦涩?那我们进入实战环节。
模仿上面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
对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
这确实解决了我们的问题,却引出了另一个问题。
当rows.json
数据量比较小时,这样做是可行的;但当数据量很大时,jq
需要将文件内容全部加载进内存后才能操作,这样很明显是内存不友好的。
庆幸的是,reduce
也允许我们按行进行解析处理:
# 输出 103
jq -n 'reduce inputs as $line (0; .+$line.a)' rows.json
注意 -n
参数的使用,不加此参数,会忽略第一行;这里使用了内置的 inputs
函数,其会将剩余的输入逐一输出,而这正是我们要的。
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