awk - 数据分析和展示

NAME

gawk - pattern scanning and processing language
模式扫描和处理语言

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

格式

gawk [options] 'program' FILE ...
program: PATTERN{ACTION STATEMENTS}

语句之间用分号分隔

常用选项

  • -F'' :指明输入时用到的字段分隔符,从文件中读取数据时,用什么做分隔符,和内建变量FS一个意思
  • -v var=value :自定义变量
  • -f /path/to/awk_script :将awk调用文件里写好的awk语法进行执行。

表达式

表达式是最简单的语句,大多数其他语句都是由不同类型的表达式组合而成。初等表达式与其他表达式通过运算符组合在一起,形成一个新的表达式。初等表达式是最原始的构造块:它们包括常量、变量、数组引用、函数调用、以及各种内建变量,例如字段的名字。

常量

awk中只有两种常量:字符串和数值,将一个字符序列用一对双引号包围起琰就创建一个字符串常量。所有的数都用浮点格式存储。

变量

用户定义的,内建的,或字段。用户定义的变量名字由数字,字母与下划线构成,但是名字不能以数字开始。所有的内建变量的名字都是大写字母。
每一个变量都有一个值,这个值可以是字符串或数值,或两者都是。因为变量的类型不需要事先声明,所以awk需要根据上下文环境推断出变量的类型。当需要时,awk可以把字符串转化为数值,或反之。

内建变量

变量 意义 默认值
ARGC 命令行参数的个数 -
ARGV 命令行参数数组 -
FILENAME 当前输入文件名 -
FNR 当前输入行的个数 -
FS 控制着输入行的字段分割符 “ ”
NF 记录每行的字段个数 -
NR 到目前为止读到的行的数量 -
OFS 输出字段分割符 “ ”
ORS 输出时的换行符 "\n"
RLENGTH 被函数match匹配的字符串的长度 -
RS 输入时的换行符 "\n"
RSTART 被函数match匹配的字符串的开始
SUBSEP 下标分割符 "\034"

**NF** (number of field) 有时候,必须总是通过\$1,\$2这样的形式引用字段,但是任何表达式都可以出现在\$后面,用来指明一个字段的编号:表达式被求值,求出的值被当作字段的编号。awk计算当前输入行的字段数量,并将它存储在一个内奸的变量中,这个变量叫作`NF`,因此 `{print NF,$1,$NF}` 将会打印:每一行的字段数量,第一个字段,以及最后一个字段 ``` [root@node1 ~]# echo "dm ft 12" | awk '{print NF,$1,$NF}' 3 dm 12 [root@node1 ~]# echo "dm ft 12" | awk '{print NF,$1,$NF-1}' 3 dm 11 [root@node1 ~]# echo "dm ft 12" | awk '{print NF,$1,$(NF-1)}' 3 dm ft ```
**NR** (number of record) `NR`这个变量计算到目前为止,读取到的行的数量。 ``` [root@node1 ~]# echo -e "dm ft\nft dm" | awk '{print NR,$0}' 1 dm ft 2 ft dm ```
**FNR**(file record number) `FNR`表示从当前输入的行数,总共读取的行数。分别在与`NR`的地方是`FNR`对各文件分别计数 ``` [root@node2007 ~]# awk -F':' '{print FNR}' /etc/passwd /etc/shadow 1 2 ... 20 1 2 ...20 [root@node2007 ~]# awk -F':' '{print NR}' /etc/passwd /etc/shadow 1 2 ... 40

[root@node1 ~]# awk 'FNR == 1' /etc/passwd
root❌0:0:root:/root:/bin/bash


<br />
**FILENAME**
`FILENAME`表示当前输入文件名

[root@node2007 tmp]# awk 'FILENAME == "/tmp/a.log" {print }' /tmp/*.log
hello
[root@node2007 tmp]# awk '{print FILENAME}' /tmp/a.log /tmp/b.log
/tmp/a.log
/tmp/b.log


<br />
**FS**(input field seperator)
输入字段分割符,默认为空白字符。

[root@node2007 ~]# echo "root:x" | awk -v FS=':' '{print $1}'
root
[root@node2007 ~]# echo "root:x" | awk -F':' '{print $1}'
root

<br />
**OFS**(output field seperator)
输出字段分隔符,默认为空白字符。

[root@node2007 ~]# echo "root:x" | awk -v FS=':' -v OFS="|" '{print $1,$2}'
root|x

<br />
**RS**(input record seperator)
输入时的换行符。默认`\n`

[root@node2007 ~]# echo "Hello World" | awk -v RS=' ' '{print}'
Hello
World
[root@node2007 ~]# echo "Hello World" | awk -v RS='o' '{print}'
Hell
W
rld

<br />
**ORS**(output record seperator)
输出时的换行符。默认`\n`

[root@node2007 ~]# echo "Hello World" | awk -v ORS='\t' '{print}'
Hello World [root@node2007 ~]# echo "Hello World" | awk -v ORS='#' '{print}'
Hello World#[root@node2007 ~]#

<br />
**ARGC**	
命令行参数的个数。awk命令本身是第一个参数,也就是数组的零下标,之后的常用选项不算做参数,最后提供的参数也是参数。


**ARGV**
数组,保存的是命令行所给定的各参数


####内建函数
内建函数分为`算术函数`和`字符串函数`个人使用算术函数不多,这里只讲`rand`算术函数,剩余都是字符串函数。

**算术函数:**
**rand()**
返回0和1之间的一个随机数

[root@node2007 ~]# echo | awk '{print rand()*10}' #返回0-10之间的数字
2.37788

<br />
**字符串函数:**

函数|描述
-|-
index(s,t)|返回字符串t在s中第一次出现的位置,如果t没有出现的话,返回0
length(s)|返回s包含的字符个数
split(s,a)|用`FS`将s分割到数组a中,返回字段的个数
split(s,a,fs)|用`fs`分割s到数组a中,返回字段的个数
sub(r,s)|将\$0的最左最长的,能被r匹配的子字符串替换为s,返回替换发生的次数
sub(r,s,t)|t就是选定区域,然后执行`sbu(r,s)`
substr(s,p)|返回s中从位置p开始的后缀
substr(s,p,n)|返回s中从位置p开始的,长度为n的子字符串
gsub(r,s)|将\$0中所有出现的r替换为s,返回替换发生的次数
gsub(r,s,t)|将字符串t中所有出现r替换为s,返回替换发生的次数

<br />

**index**

[root@node2007 ~]# echo -e "hello\nworld"| awk '{print index($1,"o")}'
5
2
[root@node2007 ~]# echo -e "hello\nworld"| awk '{print index($1,"a")}'
0
0

<br />
**length**

[root@node2007 ~]# echo -e "hello\nworldd"| awk '{print length($1)}'
5
6

<br />
**split**

[root@node2007 ~]# echo -e "/etc/nginx/nginx.conf"| awk '{split($0,a,"/");for(i=1;i<=length(a);i++){if (a[i] == ""){countine}else{print a[i]}}}' #语法格式后面会详细介绍
etc
nginx
nginx.conf

<br />
**sub,gsub**
`sub`和`gsub`相当于`sed`命令替换命令后带`g`参数的效果。  
匹配指定域/记录中最大、最靠左边的子字符串的正则表达式,并用替换字符串替换这些字符串。如果没有指定目标字符串就默认使用整个记录。替换只发生在第一次匹配的时候。

[root@node2007 ~]# echo -e "hello"| awk '{print $0,sub("l","L"),$0}'
hello 1 heLlo

[root@node2007 ~]# echo -e "hello world"| awk '{print $0,sub(/l+/,"L",$2),$0}'
hello world 1 hello worLd

[root@node2007 ~]# echo -e "hello world"| awk '{gsub(/l+/,"L");print}'
heLo worLd

[root@node2007 ~]# echo -e "hello world"| awk '{gsub(/l+/,"L",$2);print}'
hello worLd


<br />
**substr**

[root@node2007 ~]# echo -e "/etc/nginx/nginx.conf"| awk -v FS='/' '{print substr($NF,1)}' #给定一个位置,然后将后缀输出
nginx.conf
[root@node2007 ~]# echo -e "/etc/nginx/nginx.conf"| awk -v FS='/' '{print substr($NF,1,5)}' #限定输出长度
nginx

<br />

####字段变量
当前输入行的字段从\$1,\$2,一直到\$NF;\$0表示整行。字段变量与其他变量相比没什么不同,也可以用在算术或字符串运算中,也可以被赋值。

[root@node2007 ~]# echo "hello" | awk '{$1 = "world";print}'
world
[root@node2007 ~]# echo "10" | awk '{$1 = $1 / 2;print $1}'
5

<br />



###PATTERN(模式)
####BEGIN&END
`BEGIN`与`END`这两个模式不匹配任何输入行,实际情况是,当awk从输入读取数据之前,`BEGIN`的语句开始执行;当所有输入数据被读取完毕,`END`语句开始执行。于是,`BEGIN`和`END`分别提供了一种控制初始化与扫尾的方式。
`BEGIN`的一个常用用途是更改输入行被分割为字段的默认方式。使用内键变量`FS`和常用选项`-F`

[root@node1 tmp]# cat countries
USSR 8649 275 Asia
Canada 3852 25 North America
China 3705 1032 Asia
USA 3615 237 North America

[root@node1 tmp]# awk 'BEGIN{FS="\t"
printf("%10s%6s%5s %s\n\n",
"country","area","pop","continent")
}
{ printf("%10s %6d %5d %s\n",$1,$2,$3,$4)
area = area + $2
pop = pop + $3
}
END{printf("\n%10s %6d %5d\n","TOTAL",area,pop)}' countries
country area pop continent

  USSR   8649   275    Asia
Canada   3852    25    North America
 China   3705  1032    Asia
   USA   3615   237    North America

 TOTAL  19821  1569
<br />
####relational expression
`relational expression{action}`表示每碰到一个使`relational expression`为真的输入行,`{action}`就执行。为真:指的是其值非零或非空。这里的`relational expression`其实就是使用操作符来做判断,并根据判断结果来确定是否要执行`{action}`。

[root@node2007 tmp]# awk -F':' '$3 == 0{print}' /etc/passwd
root❌0:0:root:/root:/bin/bash


* `relational expression`是表达式:
	- 比较操作符:`>,<,>=,<=,!=,==,~(匹配),!~(不匹配)`
	- 算术操作符:`+,-,*,/,^(指数运算),%`
	- 赋值操作符:`=,+=,-=,/=,%=,^=,++,--`
	- 模式匹配符:`||,&&,!(使用时最好将所需取反用小括号括起来)`

<br />
####/regular expression/
`/regular expression/{action}`仅处理能够被此处模式匹配到的行。此处的`regular`即可使用`regex`(正则表达式)来做匹配

[root@node2007 tmp]# awk -F':' '$1 ~ /oot>/{print}' /etc/passwd #只要$1包含oot结尾的单词即为真
root❌0:0:root:/root:/bin/bash

[root@node2007 tmp]# awk -F':' '$1 ~ /o*t>/{print $1}' /etc/passwd #这里的的o*匹配的是零个o或者任意个o,与glob语法中的*请区分下
root
halt

<br />

####line ranges
`/part1/,/part2/`匹配一个或多个输入行,这些输入行从匹配part1的行开始,到匹配part2的行结束,包括这两行;part1可以与part2匹配到同一行。`part`匹配也可使用正则表达式。
**注:不支持直接给出数字,但可以使用内键变量`FNR`来代替直接给出数字。**

[root@node2007 tmp]# echo -e "1\n2\n1\n3\n2\n3" | awk '/1/,/3/{print}' #这里可以看出只匹配第一个
1
2
1
3
[root@node2007 tmp]# echo -e "1\n2\n1\n3\n2\n3" | awk 'FNR == 3 {print}'
1
[root@node2007 tmp]# echo -e "1\n2\n1\n3\n2\n3" | awk 'FNR <= 3 {print}'
1
2
1

<br />
####模式总结:

模式|例子|匹配
-|-|-
BEGIN|BEGIN|输入被读取之前
END|END|所有输入被读取完之后
expression|$3 < 100|第3个字段小于100的行
string-matching| $2 ~ /Asia/|第2字段含有Asia的行
compound|\$3 < 100 && \$2 ~ /Asia/|第3个字段小于100并且第2字段含有Asia的行
range|NR==10,/^root\>/|第10行到行首第一个单词是root的之间的行。


**额外使用技巧:**
正则表达式可以不用包围在两个斜杠中,可以将正则表达式赋值给一个变量,然后使用该变量匹配数据。

BEGIN { digits = "[1]+$" }
$2 ~ digits



<br />

###流程控制语句
> awk提供有用于决策`if-else`语句,以及循环语句,它们只能用在动作(action)里。所有的这些都来源于C语言,如果你熟悉C语言,我相信下面的语法对你来说小菜一碟。
> awk 提供花括号用于语句组合,`if-else`用于决策,`while`,`for`,`do`语句用于循环。一条单独的语句总是可以被替换为一个被花括号包围起来的语句列表,列表中的语句用换行符或分号分开,换行符可以出现在任何左花括号之后,也可心出现在任何右花括号之前。


流程控制语句:

if (expression) statements
if (expression) statements1 else statements2
while (expression) statements
for (variable in array) statements
do statements while (expression) #执行statements,如果为expression为真就重复

状态控制

break #退出循环
continue #退出当前循环
next #开始输入主循环的下一次迭代,BEGIN后算主循环
exit #执行END动作;如果已经在END动作内,那就退出程序,将expression作为程序退出状态返回

<br />
####if
语法格式:

PATTERN {
if (condition)
{
action
....
}
else
action
}

if-else可以缩写成如下格式:
selector?if-true-expression:if-false-expression

#if-else-if
PATTERN {
if (condition1)
{
action1
}
else if (condition2)
{
action2
}
...
else
action
}


示例:

[root@node2007 ~]# echo "1 2 3" | awk '{if ($1 == 1){print "\$1 equal 1"}}'
$1 equal 1
[root@node2007 ~]# echo "1 2 3" | awk '{if ($1 != 1){print "\$1 equal 1"}else{print "other is 2 3"}}'
other is 2 3

[root@node2007 ~]# echo "1 2 3" | awk '{if ($1 != 1){print "\$1 equal 1"}else if ($2 == 2){print "\$2 equal 2"}}'
$2 equal 2


<br />
####for
C语言的for语句,这里不做解释。

PATTERN {
for (i=1;i<=10;++i){
action
...
}
}

无限循环

PATTERN {
for (;😉{
action
...
}
}


示例:

[root@node2007 ~]# echo -e "/etc/nginx/nginx.conf"| awk '{split($0,a,"/");for(i=1;i<=length(a);i++){if (a[i] == ""){countine}else{print a[i]}}}' #语法格式后面会详细介绍
etc
nginx
nginx.conf

<br />

####while
要额外注意判断变量,一 不小心就可能无限循环。想要无限循环的话,`condition`可写成`true`

PATTERN {
action
while (condition) {
action
...
}
}


示例:

[root@node2007 ~]# echo -e "/etc/nginx/nginx.conf"| awk '{split($0,a,"/");i = 1;while(i<=length(a)){if (a[i] == ""){i += 1;countine}else{print a[i];i += 1}}}'
etc
nginx
nginx.conf
[root@node2007 ~]#

<br />
####do
和`while`循环类似,地位和shell中的util循环一样。都是至少执行一次命令。

PATTERN {
do{
action
...
} while (condition) {
action
....
}
}


示例:

[root@node2007 ~]# echo -e "/etc/nginx/nginx.conf"| awk '{split($0,a,"/");i=1;do{if (a[i] == ""){i += 1;countine}else{print a[i];i += 1}}while(i<=length(a))}'
etc
nginx
nginx.conf


`break`,`continue`,`exit`用法这里不一一介绍,有一点shell脚本经验的人,都应该会用。


<br />
###数组
数组可以说是`awk`命令中最重要的功能实现,因为只有用上他,报表的分析和数据的展示才能做到丰富。

`awk`提供了一维数组,用于存放字符串与数值。数组与数组元素都不需要事先声明,也不需要说明数组中有多少个元素,就像变量一样,当被提及时,数组元素就会被创建,数组元素的默认初始值为0或空字符串""


{ x[NR] = $0 }
END { for (i = NR;i > 0;i--) print x[i]}

第一个动作仅仅是将每个输入行存放到数组元素x中,使用行号做为下标;真正的工作在END语句中完成。效果是倒着输出


`awk`数组最大的特点 就是,数组元素的下标是字符串,也是由于这个原因,awk数组称为**关联数组**(associative arrays)

root@node2007 ~]# echo -e "hello\nworld" | awk '{x[NR] = $0;print x["1"],x["2"]}' #第一次循环时只获取1下标的值,所以2下标为空。第二次循环1,2下标都有值。
hello
hello world
[root@node2007 ~]# echo -e "hello\nworld" | awk '{x[FILENAME] = $0;print x["-"]}' #如果不使用双引号"-"括起来,则会报错
hello
world

[root@node2007 ~]# ss -an |awk '{count[$2]++} END{for (i in count) print(i,count[i])}'
LISTEN 34
ESTAB 82
State 1
UNCONN 47
[root@node2007 ~]# ss -an |awk '{count[$2]++} END{print count[LISTEN]}'

[root@node2007 ~]# ss -an |awk '{count[$2]++} END{print count["LISTEN"]}'
34
[root@node2007 ~]# echo -e "" | awk '{LISTEN="hello";count["hello"]=100;print count[LISTEN]}'
100

应该注意的是,数组下标是字符串常量“LISTEN”,如果我们将`count["LISTEN"]`写成`count[LISTEN]`,后者使用变量LISTEN的值作为下标,又因为变量是未初始化过的,所以显示为空,只有将LISTEN变量的值做为`count`数组下标且初始化过后,才会有相应的值。  

上面的程序使用`for`语句的另一种形式来遍历数组下标:

for (variable in array)
statement

这个循环将variable轮流地设置为数组的每一个下标,并执行statements。如果statements往数组里加入数的元素,那么结果是不可预知的。

为了判断某个特定的下标是否出现在数组中,可以这样写:

if ("subscript" in array)




**多维数组**(Multidimensional Arrays),awk不直接支持多维数组,但是它利用一维数组来近似模拟多维数组。从未用过,也就不细说了。
格式如下:

for (i = 1; i <= 10; i++)
for (j = 1; j <= 10; j++)
arr[i, j] =




**delete**语句,一个数组元素可以通过:
`delete array[subscript]`
可以使用循环删除所有下标:

for(i in array)
{
delete array[i]
}

<br />
###print,printf格式化输出
print 与 printf 语句可以用来产生输出. print 用于产生简单的输出; printf 用于产生格式化的输出. 来自 print 与 printf 的输出可以被重定向到文件, 管道与终端. 这两个语句可以混合使用,输出按照它们产生的顺序出现
1. print
将 $0 打印到标准输出
2. print expression, expression, …
打印各个 expression, expression 之间由 OFS 分开, 由 ORS 终止
3.  print expression,expression,… > filename
输出至文件 filename
4. print expression,expression,… >> filename
累加输出到文件 filename, 不覆盖之前的内容
5. print expression,expression,… | command
输出作为命令 command 标准输入
6. printf(format,expression,expression,…)
7. printf(format,expression,expression,…) > filename
8. printf(format,expression,expression,…) >> filename
9. printf(format,expression,expression,…) | commandprintf 
类似于 print, 但是第 1 个参数规定了输出的格式
10. close(filename), close( command)
断开 print 与 filename (或 command) 之间的连接
11. system(command)
执行 command; 函数的返回值是 command 的退出


这里主要介绍下`printf`的使用格式,它与C语言中的`printf`函数很像,但是awk的printf不支持格式说明`*.`。
`printf(format,expression1,expression2, ..., expression`
参数`format`总是必须的,它是一个变量,其字符串值含有字面文本与格式说明符,字面文本按照文本的字面值输出,格式说明符规定了参数列表中的表达式将被如何格式化地输出。每一个格式说明符都以`%`开始,以转换字符结束,可能含有下面三种修饰符:
* \- :表达式在它的域内左对齐
* width :为了达到规定的宽度,必要时填充空格;前导的0表示用零填充
* .prec :字符串最大宽度,或十进制数的小数部分的位数
* \+ :显示数值正负号

格式说明符`%`后跟以下常见字符:
* %d,%i :十进制整数
* %c :字符的ASCII码
* %e,%E :科学计数法数值显示
* %f :浮点数
* %s :字符串
* %u :无符号整数
* %% :%本身


[root@node2007 ~]# echo "10 1.234 -100 string" |awk '{printf("%.3d %.4f %-d %.3s",$1,$2,$3,$4)}'
010 1.2340 -100 str
[root@node2007 ~]# awk -F":" 'BEGIN{print"UserName UID"}{printf "%-15s %d\n",$1,$3}' /etc/passwd
UserName UID
root 0
bin 1
daemon 2
...


<br />





###常用示例

获取网络服务状态信息,并记录下各种状态各有多少次

[moorecat@proxy ~]$ ss -an | sed '1d' | awk '{count[$1]++} END{for (i in count) print(i,count[i])}'
LAST-ACK 4
SYN-RECV 20
ESTAB 380
FIN-WAIT-1 26
FIN-WAIT-2 3
TIME-WAIT 66
CLOSE-WAIT 7
LISTEN 18



查看每个ip地址连进来多少次

[root@node2007 ~]# ss -tn | sed '1d' |awk '{split($5,ip,"😊;count[ip[1]]++}END{for(i in count) {print i,count[i]}}' #ip就是切割后的位置符号,1就是以:分割后第一个字符串
183.163.23.183 2
182.200.180.145 1
59.44.12.15 3
....



测试uid大于20的用户和小于20的用户

[root@node2007 ~]# gawk -F: '{$3>20?type="COMM USER":type="SYSTEM USER";printf "%15s:%-s\n",type,$1}' /etc/passwd #这里$3>20?type=xxx:tyep=xxx相当于一个if-else语句
SYSTEM USER:root
SYSTEM USER:bin
...
COMM USER:nobody
COMM USER:systemd-network
...



匹配非UUID开头,非空,非#开头三个条件的行

[root@node2007 ~]# gawk '!/^UUID/ && !/^#/ && !/^$/{print}' /etc/fstab
/dev/mapper/centos-root / xfs defaults 0 0
/dev/mapper/centos-swap swap swap defaults 0 0



查看当前磁盘中超过80%的磁盘

[root@node2007 ~]# df -h | awk -F% '{print $1}' | awk '/^/dev//{if($NF>80)print $1}'
/dev/sda1




总结:
`awk`的基本功能`cut`也可以实现,最主要的不同是`awk`可以通过的C语言编程对数据进行分析和展示。最后展示的效果是直观的。`awk`可以说就是一个裁剪版的C语言。


最后本篇文章只记录了一些日常用得着的资料,如果需要更加详细的了解`awk`的使用方法,请阅读<\<AWK程序设计语言>\>

  1. 0-9 ↩︎

posted @ 2019-02-03 16:13  dance_man  阅读(539)  评论(0编辑  收藏  举报