【Linux】awk命令学习

最近用的比较多,学习总结一下。
文档地址:https://www.gnu.org/software/gawk/manual/gawk.html

一、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不是复合语句,也就是没有使用花括号{}包裹,那么elsethen-body之间必须使用分号;隔开,否则报语法错误。

[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

[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

[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语句,则直接结束命令。
# 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个字段。

[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在整个命令执行期间跨文件计数,相当于记录了整个流式处理过程中到当前为止累计处理的行数。

[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指定输出数据时的字段分隔符。

# 读取输入流时空格分隔,输出流制表符\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个空格。实际\tFS空格,则awk会正常按照空格分隔并忽略多余的空格。实际空格FS\t时,在文本中不能找到\t所对应的空格长度,也就是找不到指定的分隔符,所以一整行数据就会被划分为一个字段了。

[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本身只是一个变量而已,如果有这种需求,可以通过下面的这种技巧实现:

# 可以看到输出时并没有将分割符重新转为\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的影响。

[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开始计数。

[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

四、常用内置函数

1.substr

字符串截取。substr(string, start, length),从start位置截取length长度,省略length时截取到末尾。

map@gzdt-map-poi-yingxiang-offline04 yuyihua_notexisted$ echo 'F123' | awk -F"\t" '{print substr($1, 1)}'
F123
map@gzdt-map-poi-yingxiang-offline04 yuyihua_notexisted$ echo 'F123' | awk -F"\t" '{print substr($1, 2)}'
123
map@gzdt-map-poi-yingxiang-offline04 yuyihua_notexisted$ echo 'F123' | awk -F"\t" '{print substr($1, 2, 2)}'
12

需要注意截取后的子串,即使为数值型串,和数字比较大小时也是将另一边的数字按照字符串对待,按照ascii码比较(查看第七部分)。 可以通过给子串+0转为数值型。

map@gzdt-map-poi-yingxiang-offline04 yuyihua_notexisted$ echo 'F123' | awk -F"\t" 'substr($1, 2)>1'
F123
map@gzdt-map-poi-yingxiang-offline04 yuyihua_notexisted$ echo 'F123' | awk -F"\t" 'substr($1, 2)>2'
map@gzdt-map-poi-yingxiang-offline04 yuyihua_notexisted$ echo 'F123' | awk -F"\t" 'substr($1, 2)+0>122'
F123
map@gzdt-map-poi-yingxiang-offline04 yuyihua_notexisted$ echo 'F123' | awk -F"\t" 'substr($1, 2)+0>123'

五、真实业务场景下的一些demo

1.A&B两文件之间做diff,筛选出B中存在A中不存在的行

[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.修改文件中字段间的分隔符

[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

3.查看指定字段名在文件中处于第几列

map@gzdt-map-poi-yingxiang-offline04 test$ head -n 1 exproute_match_659098_20240505.csv | awk 'BEGIN{FS="\t"} {for(i=1;i<=NF;i++) if($i=="end_poi_distribution") print i,$i}'
26 end_poi_distribution

4.根据文件某列筛选指定字符出现次数大于2的记录

map@gzdt-map-poi-yingxiang-offline04 test$ cat exproute_match_659098_20240505.csv | awk 'BEGIN{FS="\t";OFS="\t"} {if(gsub(/num_track/,"&",$26)>2) print $0}' > res.file

上面命令将根据文件中第26列的值进行筛选,将该列中字符num_track出现次数>2的所有记录写入到结果文件res.file中。

gsub(regexp, replacement [, target])

  • regexp:被替换字符的正则形式。
  • replacement:新替换的字符,&代表原字符。
  • target:要进行替换的目标值或字段,省略时对$0整体替换。
  • 返回值:发生替换的次数。

g表示global全局替换的意思,target中所有符合regexp的字符都将被替换成replacement,具体使用见文档:https://www.gnu.org/software/gawk/manual/gawk.html#index-gsub_0028_0029-function-1

map@gzdt-map-poi-yingxiang-offline04 test$ cat test_file
abc12	67dhj12	hiwh119
ksnc6767msl	dsk8w9	djiw511

map@gzdt-map-poi-yingxiang-offline04 test$ cat test_file | awk 'BEGIN{FS="\t"} gsub(/[a-z]+/,"123",$3)'
abc12 67dhj12 123119
ksnc6767msl dsk8w9 123511

map@gzdt-map-poi-yingxiang-offline04 test$ cat test_file | awk 'BEGIN{FS="\t"} gsub(/[a-z]+/,"&",$3)'
abc12 67dhj12 hiwh119
ksnc6767msl dsk8w9 djiw511

map@gzdt-map-poi-yingxiang-offline04 test$ cat test_file | awk 'BEGIN{FS="\t"} gsub(/[a-z]+/,"123")'
12312	6712312	123119
1236767123	12381239	123511

5.找出某列中字符最大长度及对应的行号

map@gzdt-map-poi-yingxiang-offline04 test$ awk 'BEGIN{FS="\t"} length($5)>a {a=length($5);b=NR} END{print a,b}' 0712_step2_v2
12347 504101

length函数统计的是字符长度而不是字节长度(https://www.gnu.org/software/gawk/manual/gawk.html#String_002dManipulation-Functions)。

map@gzdt-map-poi-yingxiang-offline04 test$ echo abc123+=测试 | awk '{print length($0)}'
10

6.找出文件中字符最大长度及对应的行号

map@gzdt-map-poi-yingxiang-offline04 test$ awk 'BEGIN{FS="\t";a=0} {for(i=1;i<=NF;i++) {if(length($i)>a) {a=length($i);b=NR}}} END{print a,b}' res1
3883 90667

7.统计某个字符在一列中的出现总次数

map@gzdt-map-poi-yingxiang-offline04 test$ awk 'BEGIN{FS="\t";count=0} $5=="\\N"{count++} END{print count}' 0712_step2_v2_pv_area
100335

8.文件A按照文件B中的顺序排序

抽数场景中经常使用到,给指定的excel中的数据补充额外信息,基于excel中的id列在分布式环境下提取的结果往往不能保证返回结果的顺序,批量粘贴到excel中不匹配,可以通过下面命令对返回结果基于原id列的顺序重新排序。

map@gzdt-map-poi-yingxiang-offline04 extract_data$ cat result_file
5	郑州市
4	信阳市
1	北京市
2	武汉市
3	深圳市
map@gzdt-map-poi-yingxiang-offline04 extract_data$ cat id_file
2
3
4
5
1
map@gzdt-map-poi-yingxiang-offline04 extract_data$ awk 'BEGIN{FS="\t"} NR==FNR{a[$1]=$0;next} {print a[$1]}' result_file id_file
2	武汉市
3	深圳市
4	信阳市
5	郑州市
1	北京市
map@gzdt-map-poi-yingxiang-offline04 extract_data$

9.预设变量

将地址中包含的特殊字符@#去掉,返回原始数据、处理数据、处理时间戳。

map@gzdt-map-poi-yingxiang-offline04 extract_data$ awk -v date="$(date +%s)" 'BEGIN{FS=OFS="\t"} {raw=$4; gsub(/@|#/,"",$4); print raw,$4,date}' 艾特数据处理 | head -n3
老街东门坊同策戴斯@#2017	老街东门坊同策戴斯2017	1724308274
市南区兴安支路青岛山公园@#1号	市南区兴安支路青岛山公园1号	1724308274
岱岳区大官园社区@#21号楼1单元11楼1102	岱岳区大官园社区21号楼1单元11楼1102	1724308274

10.正则匹配

awk中正则语法:字段 ~ /正则/。下面查看地下楼层最大的楼层号和统计地下楼层楼层号大于5的数据总条数。

map@gzdt-map-poi-yingxiang-offline04 extract_data$ cut -f1,16 0823_floors_join_vertify | awk 'BEGIN{FS="\t"} $2~/^B/' | sort -rk2 | head -n3
9988931716088927305	B9
9984544294990236847	B9
9869525056092462438	B9
map@gzdt-map-poi-yingxiang-offline04 extract_data$ cut -f1,16 0823_floors_join_vertify | awk 'BEGIN{FS="\t"} $2~/^B[6-9]/' | wc -l
987

上面楼层号最大为9所以统计楼层号大于5的楼层时正则直接^B[6-9]就可以,下面测试包含多位数字的楼层号统计,了解多条件匹配时的相关语法。

map@gzdt-map-poi-yingxiang-offline04 extract_data$ echo B19 | awk 'BEGIN{FS="\t"} $1~/^B[6-9]|^B[1-9][0-9]+/' | wc -l
1
map@gzdt-map-poi-yingxiang-offline04 extract_data$ echo B19 | awk 'BEGIN{FS="\t"} $1~/^B[6-9]/ || $1~/^B[1-9][0-9]+/' | wc -l
1
map@gzdt-map-poi-yingxiang-offline04 extract_data$ echo B19 | awk 'BEGIN{FS="\t"} !($1~/^B[6-9]/ || $1~/^B[1-9][0-9]+/)' | wc -l
0

多条件逻辑关系匹配时放在正则里还是awk里都可以,取反时还是通过!

六、相关面试题

1.词频统计

[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文件时,结果正常:

[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)建好数据文件上传到虚拟机中时,结果错误:

[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。

查看一下两文件中的全部内容有什么区别:

[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这个整体,验证一下想法是否正确:

[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转换一下文本格式解决:

[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中用文本编辑器创建不会。

七、!tips

1.数值字符与非数值字符的比较逻辑

工作中在处理一批数据时,发现$7>200的结果中有\N的数据,过去一直默认数值字符和非数值字符在比较时,非数字字符会被视为0对待处理的,一直认为错误…

实际awk在进行逻辑比较时:
1)两边都为数值型字符时,就都会按照数值大小比较。
2)任意一边为非数值型字符时,就都会被转为普通字符串按照ASCII码表顺序逐位比较(ASCII码表)。

可以通过对比较字段+0的方式将非数值字符全部转为0。特别需要注意,awk中对于以数字开头,后面包含非数字的字符,也会当做数值型对待,把前面所有能提取出来的数值型字符转为数字,eg:123abc <=> 123。

map@gzdt-map-poi-yingxiang-offline04 ext0712$ echo '019' | awk '$1>=19'
019
map@gzdt-map-poi-yingxiang-offline04 ext0712$ echo '19.1' | awk '$1>=19'
19.1
map@gzdt-map-poi-yingxiang-offline04 ext0712$ echo '\' | awk '$1>=19'
\
map@gzdt-map-poi-yingxiang-offline04 ext0712$ echo '/' | awk '$1>=19'
map@gzdt-map-poi-yingxiang-offline04 ext0712$ echo '123abc' | awk '{print $1+0}'
123
map@gzdt-map-poi-yingxiang-offline04 ext0712$ echo 'abc123' | awk '{print $1+0}'
0
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值