shell实现group by聚合操作统计
在sql中,我们可以方便的使用group by及相应的聚合函数如sum avg count来实现分组统计需求,那当我们面对一个文本,在shell中也可以实现相应的功能吗?
在shell中,我们主要用awk来实现类似的统计需求,如下我们用例子来解析说明。
数据准备
[root ~]#cat tdata.txt apple 20 5.5 pear 10 4.5 apple 10 6.5
我们以空格为分割符,创建一个水果类的售价及数量记录。第一列为水果名name,第二列为数量num,第三列为单价price。
数据初探,输出各列的数据
在进行awk操作之前,一般待处理的文本文件行数都比较多,列数也多,而当我们指定不同的分割符时,每个字段究竟输出在第几列有可能数起来也会有点麻烦,所以此时一般先拿第一行做下各字段对应值输出,方便后续的对指定列的处理
awk的语法格式为:pattern { action },对输入数据一行行处理。只有满足只定的pattern表达式,后面的action才会进行处理,如默认的BEGIN END,还有用户自定义的各种表达式判断条件。
当未指定分割符时,默认以空白符作为分割。NR NF是awk中的内置参数,NR代表当前文件处理到第几行,NF代表每行分割后的字段数。
[root ~]#awk 'NR==1{for(i=1;i<=NF;i++){print "$"i,"=", $i}}' tdata.txt $1 = apple $2 = 20 $3 = 5.5
这样我就就知道了$1 $2到$NF对应的输出值,对我们后续要对指定哪列进行分析提供位置参考,当然我们也可以指定另外的分割符看下,-F参数用于指定分割符。
[root ~]#awk -F'.' 'NR==1{for(i=1;i<=NF;i++){print "$"i,"=", $i}}' tdata.txt $1 = apple 20 5 $2 = 5
有了对数据的基本感知,我们就可以进一步来处理数据了。
统计水果数量之和
[root ~]#awk '{s+=$2}END{print s}' tdata.txt 40
awk中 END是在处理完所有行后才会进行的一个操作。在这里,当累加完所有行后,进行了求和输出。s未进行初始化赋值,初始默认为0
此处相当于sql: select sum(num) from tdata
统计apple数量之和
[root ~]#awk '$1=="apple"{s+=$2}END{print s}' tdata.txt 30
由上面讲解的pattern {action}处理模式,我们只需要在上一次中加入pattern表达式即可: $1=="apple"
此处相当于sql: select sum(num) from tdata where name='apple'
统计水果的种类
[root ~]#awk '{s[$1]}END{for(i in s){print i}}' tdata.txt apple pear
awk是支持数组的,它提供了更加强大的功能操作,如上为创建了一个叫s的数组,数组的索引值为第一列,因为我们这里只需要用到索引值,所以只写了s[$1],因为未赋值的变量都初始默认为0,所以这里相当于s[$1]=0,而在后面的for(i in s)的循环中,i为s的索引值,所以就遍历出了所有的水果种类
此处相当于sql: select distinc name from tdata
统计各种水果的总价格
[root ~]#awk '{s[$1]+=$2*$3}END{for(i in s){print i,s[i]}}' tdata.txt apple 175 pear 45
每种分类的总价格等于数量*价格。所以只需要把第二字段和第三字段相乘累加即可
此处想当于sql: select sum(num*price) from tdata group by name
统计各种水果的平均值
[root ~]#awk '{s[$1]+=$2*$3; t[$1]+=$2}END{for(i in s){print i,s[i]/t[i]}}' tdata.txt apple 5.83333 pear 4.5
由于需要计算均值,故除了上面提到的求总价的数组外,还需要一个数量来存储其总数量,以便做相除,这里我们再引入一个数组t来存储总数量。在最后输出时再做相除即可得出其平均值。
此处相当于sql: select name,avg(num*price)/sum(num) from tdata group by name
注意这里的输出小数点位数,输出并不美观,要控制awk的输出,需要用printf函数,如下我们可以保留两位小数点。
[root ~]#awk '{s[$1]+=$2*$3; t[$1]+=$2}END{for(i in s){printf("%s %0.2f\n", i, s[i]/t[i]) }}' tdata.txt apple 5.83 pear 4.50
awk中print与printf的一个区别是print输出默认会带换行,而printf得自己加上换行输出符。当print不符合你的输出格式要求时,就可以用printf。