最近用的比较多,学习总结一下。
一、awk介绍
awk是一个文本编辑器,按照指定的记录分隔符和字段分割符将文件分行分字段,然后一行一行的流式处理数据。
二、语句结构
该命令的一般格式为:awk 'BEGIN{动作} 条件类型1{动作1} 条件类型2{动作2} ... END{动作}' filename
。
BEGIN中的动作最先执行,一般用来对处理过程的参数如分隔符等进行设置,然后每条数据都依次会根据匹配到的条件类型执行相应的动作,当所有的数据都被处理完成,END中的动作最后执行。BEGIN和END部分都可以省略。
动作中可以是简单的单语句操作,也可以是复杂的多语句操作,像for循环、while循环等,总之,可以像python和java等高级语言那样自由的编写代码。
几个注意的点:
1)整个命令必须用单引号
''
包裹。如果命令体太长需要分行写,则前一个'
必须在第一行,后一个'
必须在最后一行,也就是在后一个'
的所在行必须结束整个命令。2)动作语句单语句可以不使用花括号
{}
包裹,复合语句需要使用花括号{}
包裹。3)条件语句中可以使用小括号
()
改变计算的优先级。4)如果命令中需要引号引用字面量,需要使用双引号
""
。5)每一行记录匹配一个条件类型后,其余的条件类型也会继续匹配,能匹配几个条件类型,就会执行几个对应的动作。
6)如果省略条件类型,也就是对应的动作一定执行,则动作语句必须使用花括号
{}
包裹,否则报语法错误。7)动作语句省略时,默认
{print $0}
。
1.条件控制语句
具体参考https://www.gnu.org/software/gawk/manual/gawk.html#Control-Statements-in-Actions,这里只列举部分常用示例。
1)if
语句格式:if (condition) then-body [else else-body]
。
如果else
关键字和then-body
语句处在同一行 且 then-body
不是复合语句,也就是没有使用花括号{}
包裹,那么else
和then-body
之间必须使用分号;
隔开,否则报语法错误。
sh
[wdy@node1 ~]$ cat test1
A B C
D E F
G H
# 这种情况必须带分号
[wdy@node1 ~]$ cat test1 | awk '{if(NF>=3) print ">=3";else print "<3"}'
>=3
>=3
<3
# then-body被{}包裹可以去分号
[wdy@node1 ~]$ cat test1 | awk '{if(NF>=3){print ">=3"}else print "<3"}'
>=3
>=3
<3
2)for
语句格式:for (initialization; condition; increment) body
。
sh
[wdy@node1 ~]$ cat test1 | awk '{for(i=1;i<=NF;i++) print $i}'
A
B
C
D
E
F
G
H
3)while
语句格式:while (condition) body
。
sh
[wdy@node1 ~]$ cat test1 | awk '{i=1;while(i<=NF) {print $i;i++}}'
A
B
C
D
E
F
G
H
4)break&continue&next&exit
这几个都是循环体中的关键字,主要对比下作用上的区别:
- break:直接结束当前整个循环体。
- continue:跳过本次循环循环体之后的语句,直接进入下一次循环。
- next:跳过当前行。这个关键字和continue容易混淆,看起来作用好像差不多,区别在于所结束的"循环对象"不同。awk中数据是一行行读入的,这本身就可以看成是一个循环;而一般我们自己定义的循环,是从列或其他维度循环,因此如果我们定义的循环维度不是从行的维度,那么next是直接结束当前行相关的任何操作跳到下一行。而continue在跳过循环体之后的语句后会继续执行与当前行相关的操作。所以我觉得理解这两个关键字的区别从循环的维度是行还是列可能更好理解一些。
- exit:如果有END语句,则直接执行END中的动作再结束awk命令,如果没有END语句,则直接结束命令。
sh
# break
[wdy@node1 ~]$ cat test1 | awk '{
> for(i=1;i<=NF;i++){
> if($i=="A") break;print $i}}'
D
E
F
G
H
# continue
[wdy@node1 ~]$ cat test1 | awk '{
> for(i=1;i<=NF;i++){
> if($i=="A") continue;print $i}}'
B
C
D
E
F
G
H
# next
[wdy@node1 ~]$ cat test1 | awk '{
> for(i=1;i<=NF;i++){
> if($i=="B") next;print $i}}'
A
D
E
F
G
H
# exit
[wdy@node1 ~]$ cat test1 | awk '{
> for(i=1;i<=NF;i++){
> if($i=="B") exit;print $i}}
> END{print "finish"}'
A
finish
2.比较运算符
文档:https://www.gnu.org/software/gawk/manual/gawk.html#Comparison-Operators
3.逻辑运算符
!
:not,取反
&&
:and
||
:or
三、常用内置变量
1.$0 & $n
0表示当前行这整个记录,n表示当前行的第n个字段。
sh
[wdy@node1 ~]$ cat test1
A B C
D E F
G H
[wdy@node1 ~]$ cat test1 | awk '{print $0}'
A B C
D E F
G H
[wdy@node1 ~]$ cat test1 | awk '{print $1}'
A
D
G
2.NR & FNR
Number of Records & File Number of Records,都用来对文件的当前行计数,从1开始。也就是说第一行的数据,NR=1,第二行的数据,NR=2,...,依次类推。区别在于,FNR只在每个文件内累计,跨文件时重新从1开始计数。NR在整个命令执行期间跨文件计数,相当于记录了整个流式处理过程中到当前为止累计处理的行数。
sh
[wdy@node1 ~]$ awk '{print NR,$0}' test1 test1
1 A B C
2 D E F
3 G H
4 A B C
5 D E F
6 G H
[wdy@node1 ~]$ awk '{print FNR,$0}' test1 test1
1 A B C
2 D E F
3 G H
1 A B C
2 D E F
3 G H
3.FS & OFS
Field Separator & Output Field Separator,默认值都是空格。这两个变量都是用来指定字段分隔符。FS指定读数据时的字段分隔符,OFS指定输出数据时的字段分隔符。
sh
# 读取输入流时空格分隔,输出流制表符\t分隔
[wdy@node1 ~]$ awk 'BEGIN{FS=" ";OFS="\t"}{print $1,$2}' test1
A B
D E
G H
这里有几个需要注意的点:
1)指定FS时,如果文件中的字段分隔符为\t
,而FS指定的是空格,那么么文件会被正常划分字段赋值给$n这样子的。如果文件中的分割符是空格,而指定的FS是\t
,那么每行数据之间不会被分割,也就是一行同时也是一个字段。可以这么来理解:\t
其实也是一种特殊的空白字符,一般包含4个空格。实际\t
FS空格,则awk会正常按照空格分隔并忽略多余的空格。实际空格FS\t
时,在文本中不能找到\t
所对应的空格长度,也就是找不到指定的分隔符,所以一整行数据就会被划分为一个字段了。
sh
[wdy@node1 ~]$ awk 'BEGIN{FS=" "}{print $1}' test1
A
D
G
[wdy@node1 ~]$ awk 'BEGIN{FS="\t"}{print $1}' test1
A B C
D E F
G H
2)print后面的变量之间只有以,
隔开时指定的OFS才会生效,以,
分隔时会将默认值空格替换为指定的OFS插在输出变量之间。
3)如果输出的变量是$0即整行数据,想对这整行重新使用指定的OFS分隔输出,直接print $0并不会改变输出记录的原有分隔符。因为就像上面说到的那样,以,
分隔时会将默认值空格替换为指定的OFS插在输出变量之间,$0本身只是一个变量而已,如果有这种需求,可以通过下面的这种技巧实现:
sh
# 可以看到输出时并没有将分割符重新转为\t
[wdy@node1 ~]$ awk 'BEGIN{FS=" ";OFS="\t"}{print $0}' test1
A B C
D E F
G H
# 使用下面这种技巧实现转换
[wdy@node1 ~]$ awk 'BEGIN{FS=" ";OFS="\t"} $1=$1{print $0}' test1
A B C
D E F
G H
可以这么理解下面这种重新赋值的技巧原理:如果不重新赋值,$0只是记录了一个原始数据行的变量而已,即使输出$1、$2等,awk只会根据FS对$0进行分割,输出时变量之间再按照指定的OFS分隔输出。但是这并不会影响原来$0的值。如果使用了$1=1这种重新赋值的技巧,awk会将所有的n变量重新计算一遍,所以此时的$0的分隔符就会被指定的OFS替换。
4.NF
Number of Fields,记录了每行数据的字段总数,从1开始计数,值受FS的影响。
sh
[wdy@node1 ~]$ awk 'BEGIN{FS=" "}{print NF,$0}' test1
3 A B C
3 D E F
2 G H
5.ARGIND
Argument Index,用来标识当前处理的文件序号,从1开始计数。
sh
[wdy@node1 ~]$ awk 'BEGIN{FS=""} {print ARGIND,$0}' test1 test1
1 A B C
1 D E F
1 G H
2 A B C
2 D E F
2 G H
四、真实业务场景下的一些demo
1.A&B两文件之间做diff,筛选出B中存在A中不存在的行
sh
[wdy@node1 ~]$ cat test1
A B C
D E F
G H
[wdy@node1 ~]$ cat test2
A B C
A1 B1 C1
A2 B2 C2
[wdy@node1 ~]$ awk 'BEGIN{FS=" "} NR==FNR{a[$0];next} !($0 in a)' test1 test2
A1 B1 C1
A2 B2 C2
上面这段命令,会先以第一个文件中每行记录的值作为数据a的索引建立一个数组a,然后在第二个文件中以每一行的值为索引判断是否在数组a当中,索引不在a中该行记录就会被输出。这里有个点需要注意,就是当动作语句省略时,默认是打印输出当前行。
2.修改文件中字段间的分隔符
sh
[wdy@node1 ~]$ cat test1
A B C
D E F
G H
[wdy@node1 ~]$ awk 'BEGIN{FS=" ";OFS="\t"} $1=$1{print $0}' test1
A B C
D E F
G H
五、相关面试题
1.词频统计
sh
[wdy@node1 ~]$ cat data
A B A
C D A
B C A
[wdy@node1 ~]$ cat data | awk '
> BEGIN{OFS="\t"}
> {for(i=1;i<=NF;i++) freq[$i]++}
> END{for(word in freq) print word,freq[word]}' | sort -rnk 2
A 4
C 2
B 2
D 1
主要思路是循环遍历每行中的各个字段,以字段值为索引建立数组freq,如果遇到相同的索引则对应索引位置处的值+1。最后打印这个数组结果就可以了。后面的sort命令是对结果进行下排序输出。
关于sort命令,参数代表的含义:
-r:reverse,逆序输出
-n:numeric-sort,根据字符串对应的数值大小来排序,而非字符串的字典顺序。这里是统计频次。
-k:key,指定用来排序的key,上面用2来指定根据频次排序,2代表的是第2列。
PS
实现awk词频统计过程中发现了个问题,当直接在虚拟机中vim新建data文件时,结果正常:
sh
[wdy@node1 ~]$ cat data | awk '
> BEGIN{OFS="\t"}
> {for(i=1;i<=NF;i++) freq[$i]++}
> END{for(word in freq) print word,freq[word]}' | sort -rnk 2
A 4
C 2
B 2
D 1
当在本地(win10)建好数据文件上传到虚拟机中时,结果错误:
sh
[wdy@node1 ~]$ cat data_win | awk '
> BEGIN{OFS="\t"}
> {for(i=1;i<=NF;i++) freq[$i]++}
> END{for(word in freq) print word,freq[word]}' | sort -rnk 2
A 3
C 2
B 2
D 1
A 1
也就是A本来是4,在后面被拆分成了3+1。
查看一下两文件中的全部内容有什么区别:
sh
[wdy@node1 ~]$ cat -A data
A B A$
C D A$
B C A$
[wdy@node1 ~]$ cat -A data_win
A B A^M$
C D A^M$
B C A^M$
Unix/Linux系统中,cat -A命令用于显示文件中的控制字符和非打印字符。在cat -A的输出中,$符号通常用来表示行尾,而M通常表示一个回车符(Carriage Return)。
问题就出现在这,win10上传的文件换行符和linux系统的换行符不一致,虽然不影响awk对行的划分,但是对于除了(1, 1)位置的A,data时A不变,而在data_win中就变成了A\r这个整体,验证一下想法是否正确:
sh
[wdy@node1 ~]$ cat data_win | awk '
> BEGIN{OFS="\t"}
> {for(i=1;i<=NF;i++) freq[$i]++}
> END{for(word in freq) print word,freq[word]}' | sort -rnk 2| cat -A
A^M^I3$
C^I2$
B^I2$
D^I1$
A^I1$
可以看到,之所以上面A分两次输出,就是因为其中有的A附带的有隐藏字符。这个问题可以通过dos2unix data_win
转换一下文本格式解决:
sh
[wdy@node1 ~]$ dos2unix data_win
[wdy@node1 ~]$ cat -A data_win
A B A$
C D A$
B C A$
[wdy@node1 ~]$ cat data_win | awk '
> BEGIN{OFS="\t"}
> {for(i=1;i<=NF;i++) freq[$i]++}
> END{for(word in freq) print word,freq[word]}' | sort -rnk 2
A 4
C 2
B 2
D 1
调试过程中还发现一个需要注意的地方,linux中vim编辑器创建的文本,哪怕最后一行没有手动换行,系统也会给最后一行添加换行符,windows中用文本编辑器创建不会。