写sed不写awk真是对强迫症的我的一种折磨啊。这次重温一下Linux/Unix下另一个也很老(还是比我老)的文本处理神器:awk(名字来源于三个创始人的姓的首字母)。Linux下的gawk是awk的GNU实现。要是不经常使用,很容易忘记。可以把本文当成一个例子库,有用的时候来查一下。
假设我们有一个不合法格式的csv文件如下:
1 2 3 4 5 6 7
| cat << EOF >staff.csv Country Name Age US Gavo 35 US Jane 21 US Bill 25 China Jimmy 42 EOF
|
查询
查询列
最基本的用法就是过滤出某列来:
1
| awk '{print $1}' staff.csv
|
其中的大括号表示一个动作,print
就是一个打印的动作。$1
代表第1列,$0
代表所有列。所以以下的命令也很好理解:
1 2 3 4 5 6
| awk '{print $3}' staff.csv awk '{print $0}' staff.csv awk '{print $1 $2}' staff.csv awk '{print $1" "$1}' staff.csv awk '{print NR FS NF}' staff.csv awk '{print $(NF-1)}' staff.csv
|
我们可以把这个不合法的csv文件变得合法:
1 2
| awk '{print $1","$2","$3 > "staff.csv"}' staff.csv cat staff.csv
|
注意这里awk的语法:写文件是在大括号里面,而不是外面。当然外面也是可以的,只要别和staff.csv
重名就好。
分隔符
现在再来一次awk '{print $1}' staff.csv
,就会发现awk无视逗号,把一整行都当成第一列了。可以用以下命令指定分隔符:
1 2 3
| awk -F"," '{print $1,$2}' staff.csv awk '{print $1,$2}' FS="," staff.csv awk '{print $1,$2}' FS="," OFS=":" staff.csv
|
上面最后一个命令中,虽然OFS指定了输出分隔符,但是需要在$1
和$2
中间加上这个分隔符才能生效。另外,有时候省略双引号会出错的,比如对于|
这个符号来说,有“或者”的意思,可能有歧义,所以还是加上双引号比较稳妥。
条件
awk支持丰富的条件语法以及正则表达式匹配:
1 2 3 4 5 6 7 8 9
| awk 'NR==1{print $3}' FS=, staff.csv awk 'NR!=1{print $3}' FS=, staff.csv awk '/Gavo/{print $3}' FS=, staff.csv awk '/Gavo|Jane/{print $0}' FS=, staff.csv awk '$3>40{print $0}' FS=, staff.csv awk '/Gavo/ || $3>40{print $0}' FS=, staff.csv awk '$1 ~ /US/{print $0}' FS=, staff.csv awk '$3 ~ /^2/{print $0}' FS=, staff.csv awk '/Gavo/{print $0}/Jane/{print $2}' FS=, staff.csv
|
还支持在动作里写更复杂的条件:
1 2 3 4
| awk '{if (NR==1) print $0;}{print $2}' FS=, staff.csv awk 'c=(NR==1){print $0} !c{print $2}' FS=, staff.csv awk '(NR==1){print $0;next}{print $2}' FS=, staff.csv awk '{r=(NR==1)?$0:$2; print r}' FS=, staff.csv
|
以上命令的后三条的效果是一样的。下面是大招:条件表达式的这一套全齐了:
1
| awk '{if (NR==1) {print $1;} else if (NR==2) {print $2} else {print $3}}' FS=, staff.csv
|
传参
为了命令简单起见,我们再把csv文件换成最早那个但是去掉表头:
1 2 3 4 5 6
| cat << EOF >staff.csv US Gavo 35 US Jane 21 US Bill 25 China Jimmy 42 EOF
|
入参
过了一年,要把所有人的Age都加上一岁:
1 2
| awk '{print $3}' staff.csv awk '{print $3+1}' staff.csv
|
如果这个一岁是个变量,那就这么做:
1 2 3
| age=1 awk -v value=$age '{print $3+value}' staff.csv awk '{print $3+value}' value=$age staff.csv
|
如果这个一岁是个环境变量,那就这么做:
1 2
| export AGE=1 awk '{print $3+ENVIRON["AGE"]}' staff.csv
|
出参
如果我们想拿到Jane和Bill的Age,怎么做呢?
1 2 3 4 5
| value=`awk '{if($2=="Jane")print "jane_age="$3;if($2=="Bill")print "bill_age="$3}' staff.csv` echo $value eval $value echo $jane_age echo $bill_age
|
这个方案的思路是在awk
里拼命令,然后出来执行。
统计
求和
求所有人的年龄总和:
1 2
| awk '{s+=$3}END{print s}' staff.csv awk '{s+=$3;print $2":"$3}END{print "SUM:"s}' staff.csv
|
求平均
下一个命令可以求平均值,也就是求和之后除以行数NR:
1
| awk '{a+=$3}END{print a/NR}' staff.csv
|
如果没有END
,awk会在每处理一行之后打印一次。
去重
查看所有的国家,去除重复项目:
1 2
| awk '{a[$1];}END{for (i in a)print i}' staff.csv awk '{print $1}' staff.csv | uniq
|
上面第一个命令比较复杂:a[$1]
是awk的数组(其实是字典),a["US"]=1
意味着在a的数组里,"US"
的值为1。在这里并没有用到它的值,而是利用了字典的键不能重复的原理。后面有一个for
循环,把字典的键都打印出来。如果要打印值,用a[i]
就好了。比如下面这个打印每个国家的总年龄:
1
| awk '{a[$1]+=$3;}END{for (i in a)print a[i]}' staff.csv
|
文件处理
分割文件
还是以上面那个文件为例:
1 2 3 4 5 6
| cat << EOF >staff.csv US Gavo 35 US Jane 21 US Bill 25 China Jimmy 42 EOF
|
将不同国家的记录写到不同文件中:
1 2 3 4
| awk '{print > $1".csv"}' staff.csv ls cat China.csv cat US.csv
|
这回看起来好简单啊。print
默认打印出整行,所以可以省略$0
。
每两行写一个文件:
1 2 3 4
| awk 'NR%2==1{f=++i".csv";}{print > f}' staff.csv ls cat 1.csv cat 2.csv
|
把多余文件删掉:
増删改列
先看下面这个命令:
1
| awk '{$1=++i FS $1}1' staff.csv
|
它在每行的前面增加了行号。关于$1=++i FS $1
,以第一行为例:++i
为1,FS
为分隔符,$1
为第一列,这三项结合起来赋值给前面的$1
,所以第一列就变成了1 US
。后面的1
代表True
,是整行打印的意思,也可以用{print $0}
来代替。所以:
1 2 3 4 5 6 7 8 9
| awk '{$(NF+1)=++i}1' staff.csv awk '{$(NF+1)=++i FS NF}1' staff.csv awk '{$NF=++i FS $NF}1' staff.csv awk '{$2=toupper($2)}1' staff.csv awk '{$2=substr($2,0,3)}1' staff.csv awk '{$2=""}1' staff.csv awk '{NF=2}1' staff.csv awk '{for(i=1;i<NF;i++)$i=$(i+1);NF=2}1' staff.csv awk '{$2=$2$3;NF=2}1' staff.csv
|
参考资料
The UNIX School 里的awk and sed tutorials含有大量的例子和解释,非常容易上手,本文就是以其为基础整理而成。
酷壳的AWK 简明教程很适合入门。
当然还有最全面的官方文档。