linux中玩转awk技巧分享

awk其名称得自于它的创始人 Alfred Aho 、Peter Weinberger 和 Brian Kernighan 姓氏的首个字母。实际上 AWK 的确拥有自己的语言: AWK 程序设计语言 , 三位创建者已将它正式定义为“样式扫描和处理语言”。它允许您创建简短的程序,这些程序读取输入文件、为数据排序、处理数据、对输入执行计算以及生成报表,还有无数其他的功能。

awk简明教程

简介

awk是一个强大的文本分析工具,相对于grep的查找,sed的编辑(擅长取行),awk(擅长取列)在其对数据分析并生成报告时,显得尤为强大。简单来说awk就是把文件逐行的读入,以空格为默认分隔符将每行切片,切开的部分再进行各种分析处理。

awk有3个不同版本: awk、nawk和gawk,未作特别说明,一般指gawk,gawk 是 AWK 的 GNU 版本。

awk简单来讲:取文件中的某列数据

通常,awk是以文件的一行为处理单位的。awk每接收文件的一行,然后执行相应的命令,来处理文本。

使用方法

1
2
3
4

awk '{pattern + action}' {filenames}
# 第二种
awk [可选的命令行选项] 'BEGIN{命令 } pattern{ 命令 } END{ 命令 }' 文件名

关于第二种方式,解释如下:

1.BEGIN 开头部分,可选的。用来设置一些参数,输出一些表头,定义一些变量等。上面的命令仅打印了一行信息而已。

2.END 结尾部分,可选的。用来计算一些汇总逻辑,或者输出这些内容。上面的命令,使用简单的for循环,输出了数组rt中的内容。

3.Pattern 匹配部分,依然可选。用来匹配一些需要处理的行。上面的命令,只匹配tcp开头的行,其他的不进入处理。

4.Action 模块。主要逻辑体,按行处理,统计打印,都可以。

初步使用

1.首先保存一个示例文档

1
2
3
4
5
6
7
8
$ cat netstat 
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 2036/sshd
tcp6 0 0 :::7500 :::* LISTEN 17767/./frps
tcp6 0 0 :::80 :::* LISTEN 17767/./frps
tcp6 0 0 :::7000 :::* LISTEN 17767/./frps
tcp6 0 0 :::443 :::* LISTEN 17767/./frps

2.输出第一列和第四列

  • 其中单引号中被大括号括着的就是awk语句只能被单引号包含
  • $1..$n表示第几列$0表示整行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ awk '{print $1,$4}' netstat 
Active (only
Proto Local
tcp 0.0.0.0:22
tcp6 :::7500
tcp6 :::80
tcp6 :::7000
tcp6 :::443

$ cat /etc/passwd |awk -F ':' '{print $1}'
root
daemon
bin
sys

过滤记录

1.awk可以对其中的某些字段进行判断,通过与或非连接。awk 中的比较运算符用于比较字符串和或者数值,包括以下类型:

  • > – 大于
  • < – 小于
  • >= – 大于等于
  • <= – 小于等于
  • == – 等于
  • != – 不等于
  • some_value ~ / pattern/ – 如果 some_value 匹配模式 pattern,则返回 true
  • some_value !~ / pattern/ – 如果 some_value 不匹配模式 pattern,则返回 true

如以下常用示例:

1
2
3
4
5
6
7
8
9
10
11
12
$ awk '$1=="tcp" && $6=="LISTEN" ' netstat  
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 2036/sshd
# 需要表头的话,可以引入内建变量NR
$ awk '$1=="tcp" && $6=="LISTEN" || NR==2 ' netstat
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 2036/sshd
# NR也可以用来打印行号
$ awk '$1=="tcp6" { print NR,$0 }' netstat
4 tcp6 0 0 :::7500 :::* LISTEN 17767/./frps
5 tcp6 0 0 :::80 :::* LISTEN 17767/./frps
6 tcp6 0 0 :::7000 :::* LISTEN 17767/./frps
7 tcp6 0 0 :::443 :::* LISTEN 17767/./frps

内建变量

程序已经定义好的变量,可以直接来使用,以下常见的变量。

变量释义
$0当前记录(这个变量中存放着整个行的内容)
$1~$n当前记录的第n个字段,字段间由FS分隔
FS输入字段分隔符 默认是空格或Tab
NF当前记录中的字段个数,就是有多少列
NR已经读出的记录数,就是行号,从1开始,如果有多个文件话,不断累加。
FNR当前记录数,与NR不同的是,这个值会是各个文件自己的行号
RS输入的记录分隔符, 默认为换行符
OFS输出字段分隔符, 默认也是空格
ORS输出的记录分隔符,默认为换行符
FILENAME当前输入文件的名字

❈ ❈ ❈ 一般的开发语言,数组下标是以0开始的,但awk的列$是以1开始的,而0指的是原始字符串。

1.输出表头、行号及特定列

1
2
3
4
5
6
7
8
# 使用内建变量NR、FNR输出行号以及表头
$ awk '$3==0 && $6=="LISTEN" || NR==2 {print NR,FNR,$1,$3,$6}' netstat
2 2 Proto Send-Q Foreign
3 3 tcp 0 LISTEN
4 4 tcp6 0 LISTEN
5 5 tcp6 0 LISTEN
6 6 tcp6 0 LISTEN
7 7 tcp6 0 LISTEN

2.使用分隔符来输出特定的行或者列,下面👇两个命令等价:

1
2
3
4
5
6
7
8
9
10
11
12
$ awk 'BEGIN{FS=":"} {print $1, $7}' /etc/passwd | head -n 4
root /bin/bash
bin /sbin/nologin
daemon /sbin/nologin
adm /sbin/nologin

# -F指定域分隔符为':'
$ cat /etc/passwd |awk -F ':' '{print $1"t"$7}' | head -n 4
root /bin/bash
bin /sbin/nologin
daemon /sbin/nologin
adm /sbin/nologin

❈ ❈ ❈ 如果要是用多个分隔符,可以使用下面方式来指定:

1
awk -F '[;:]'

3.自定义以t作为分隔符输出的例子:

1
2
3
4
5
$ awk -F: '{print $1,$7}' OFS="t" /etc/passwd | head -n 4
root /bin/bash
bin /sbin/nologin
daemon /sbin/nologin
adm /sbin/nologin

字符串匹配

1.像grep一样匹配相关字符

1
2
3
4
5
6
7
8
9
# 只匹配80的字符
$ awk '/80/' netstat
tcp6 0 0 :::80 :::* LISTEN 17767/./frps
# 匹配80、或者7000、或者tcp6的相关字符
$ awk '/80|7000|tcp6/' netstat
tcp6 0 0 :::7500 :::* LISTEN 17767/./frps
tcp6 0 0 :::80 :::* LISTEN 17767/./frps
tcp6 0 0 :::7000 :::* LISTEN 17767/./frps
tcp6 0 0 :::443 :::* LISTEN 17767/./frps

2.精确的匹配某个字段

1
2
3
4
5
6
7
# $4匹配第四列的数据
$ awk '$4 ~/80|7000/ {print $0}' netstat
tcp6 0 0 :::80 :::* LISTEN 17767/./frps
tcp6 0 0 :::7000 :::* LISTEN 17767/./frps
# 搜索包含System关键字的行并输出
$ cat -n /etc/passwd | gawk -F: '/System/{print $1" "$5}'
17 dbus System message bus

❈ ❈ ❈ 其实 ~表示模式开始。/ /中是模式。这就是一个正则表达式的匹配。

对此模式取反的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 字符串匹配取反
$ awk '!/80/' netstat
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 2036/sshd
tcp6 0 0 :::7500 :::* LISTEN 17767/./frps
tcp6 0 0 :::7000 :::* LISTEN 17767/./frps
tcp6 0 0 :::443 :::* LISTEN 17767/./frps
# 精确匹配取反
$ awk '$4 !~/80|7000/ {print $0}' netstat
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 2036/sshd
tcp6 0 0 :::7500 :::* LISTEN 17767/./frps
tcp6 0 0 :::443 :::* LISTEN 17767/./frps

拆分文件

1.指定某列为分类符,使用重定向就可以导出到不同的文件中,例如下列语句通过第1列进行分割:

1
2
3
4
5
6
7
# 按照第1列拆分文件,第一列有tcp、tcp6两种类型
# 所以按照类型命名,当前目录下会多两个文件
$ awk 'NR!=2{print > $1}' netstat
$ cat tcp
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 2036/sshd
$ ls
netstat tcp tcp6

2.也可以把指定的列追加>>到文件,追加不会覆盖源文件,如下:

1
2
3
4
$ awk 'NR!=1{print $4,$7 >> $1}' netstat
$ cat tcp
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 2036/sshd
0.0.0.0:22 2036/sshd

3.也可以使用复杂的表达式(例如if-else-if语句,awk是个脚本解释器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ awk 'NR!=1{if($4 ~ /80|443/) print > "1.txt";         
else if($4 ~/7000/) print > "2.txt";
else print > "3.txt" }' netstat
$ ls ?.txt
1.txt 2.txt 3.txt
$ cat 1.txt
tcp6 0 0 :::80 :::* LISTEN 17767/./frps
tcp6 0 0 :::443 :::* LISTEN 17767/./frps
$ cat 2.txt
tcp6 0 0 :::7000 :::* LISTEN 17767/./frps
# 其中NR!=1,不取第一行数据
# 这是第一行数据Active Internet connections (only servers)
# else print > "3.txt" 匹配的是非前两列匹配到的其他内容,注意if和else if要对相同列数据进行操作,不然可能输出内容有问题
$ cat 3.txt
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 2036/sshd
tcp6 0 0 :::7500 :::* LISTEN 17767/./frps

统计

1.统计某个文件夹下的文件占用的字节数

1
2
$ ls -l |awk 'BEGIN {size=0;} {size=size+$5;} END{print "[end]size is ", size}'
[end]size is 8657198

如果以M为单位显示:

1
2
$ ls -l |awk 'BEGIN {size=0;} {size=size+$5;} END{print "[end]size is ", size/1024/1024,"M"}' 
[end]size is 8.25889 M

注意,统计不包括文件夹的子目录。

2.统计以.txt为后缀的文件总大小:

1
2
$ ll *.txt | awk '{sum+=$5} END {print sum}'
769.9

3.统计文件夹下的文件数目:

1
2
$ ls -lha | gawk 'BEGIN {count=0} {count++} END { print "File Count:"count}' 
File Count:41

4.统计外网连接数,根据ip分组

1
2
3
4
5
$ netstat -ant | awk '/^tcp/{print $5}' | awk -F: '!/^:/{print $1}' | sort | uniq -c
# sort 将重复的行集中在一起
# uniq 对文本行进行去重 -c 统计重复次数
# 可实现对文本行按重复次数进行排序 默认是升序 -r 降序
$ sort 1.txt | uniq -c | sort -rn

5.统计第6列连接状态总共出现了几次

1
2
3
$ awk 'NR!=1{a[$6]++;} END {for (i in a) print i ", " a[i];}' netstat 
LISTEN, 5
Foreign, 1

6.统计每个用户的进程占用了多少内存

1
2
3
4
5
 ps aux | awk 'NR!=1{a[$1]+=$6;} END {for(i in a) print i "," a[i]"KB"; }'
dbus,1492KB
polkitd,10972KB
ntp,2104KB
root,138580KB

awk脚本

在上面我们可以看到一个END关键字。END的意思是“处理完所有的行的标识”,即然说到了END就有必要介绍一下BEGIN,这两个关键字意味着执行前执行后的意思,语法如下:

  • BEGIN{ 这里面放的是执行前的语句 }
  • END { 这里面放的是处理完所有的行后要执行的语句 }
  • { 这里面放的是处理每一行时要执行的语句 }

so,举个栗子🌰

1
2
3
4
5
6
$ cat score 
Marry 2143 78 84 77
Jack 2321 66 78 45
Tom 2122 48 77 71
Mike 2537 87 97 95
Bob 2415 40 57 62

awk脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ cat cal.awk 
#!/bin/awk -f
#运行前
BEGIN {
math = 0
english = 0
computer = 0

printf "NAME NO. MATH ENGLISH COMPUTER TOTALn"
printf "---------------------------------------------n"
}
#运行中
{
math+=$3
english+=$4
computer+=$5
printf "%-6s %-6s %4d %8d %8d %8dn", $1, $2, $3,$4,$5, $3+$4+$5
}
#运行后
END {
printf "---------------------------------------------n"
printf " TOTAL:%10d %8d %8d n", math, english, computer
printf "AVERAGE:%10.2f %8.2f %8.2fn", math/NR, english/NR, computer/NR
}

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
# -f调用脚本
$ awk -f cal.awk score
NAME NO. MATH ENGLISH COMPUTER TOTAL
---------------------------------------------
Marry 2143 78 84 77 239
Jack 2321 66 78 45 189
Tom 2122 48 77 71 196
Mike 2537 87 97 95 279
Bob 2415 40 57 62 159
---------------------------------------------
TOTAL: 319 393 350
AVERAGE: 63.80 78.60 70.00

环境变量

awk脚本与环境变量的交互,使用-v参数(定义变量)和ENVIRON,使用ENVIRON的环境变量需要export。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ x=5
$ y=10
$ echo $x
5
$ export y
$ echo $x $y
5 10
$ awk -v val=$x '{print $1,$2,$3,$4+val,$5+ENVIRON["y"]}' OFS="t" score
Marry 2143 78 89 87
Jack 2321 66 83 55
Tom 2122 48 82 81
Mike 2537 87 102 105
Bob 2415 40 62 72

有趣示例

1.从file文件中找出长度大于20的行

1
2
3
4
5
6
$ awk 'length>=21' score 
Marry 2143 78 84 77
Jack 2321 66 78 45
Tom 2122 48 77 71
Mike 2537 87 97 95
Bob 2415 40 57 62

2.按连接数查看客户端ip,根据ip分组计数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ netstat -ntu | awk '{print $5}' | cut -d: -f1 | sort | uniq -c
1 100.100.30.25
2 112.213.179.149
1 173.255.234.138
1 47.52.207.198
1 47.93.222.200
1 60.247.88.82
1 77.247.110.219
1 80.1.15.172
7 80.82.70.187
1 89.248.174.198
1 Address
1 servers)
# cut用法
# -d :自定义分隔符,默认为制表符。
# -f :与-d一起使用,指定显示哪个区域。

3.打印九九乘法表

1
2
3
4
5
6
7
8
9
10
$ seq 9 | sed 'H;g' | awk -v RS='' '{for(i=1;i<=NF;i++)printf("%dx%d=%d%s", i, NR, i*NR, i==NR?"n":"t")}'
1x1=1
1x2=2 2x2=4
1x3=3 2x3=6 3x3=9
1x4=4 2x4=8 3x4=12 4x4=16
1x5=5 2x5=10 3x5=15 4x5=20 5x5=25
1x6=6 2x6=12 3x6=18 4x6=24 5x6=30 6x6=36
1x7=7 2x7=14 3x7=21 4x7=28 5x7=35 6x7=42 7x7=49
1x8=8 2x8=16 3x8=24 4x8=32 5x8=40 6x8=48 7x8=56 8x8=64
1x9=9 2x9=18 3x9=27 4x9=36 5x9=45 6x9=54 7x9=63 8x9=72 9x9=81

参考链接:
AWK 简明教程