YOLO813

AWK学习笔记 - 日志分析

    最近因为服务器的日志数量过于庞大,经常需要定制分析,之前安装的goaccess已经有点不能满足需求了,便想着学习一下awk语法专门用于分析日志,其实使用python、php等语言也可以做,例如公司的老大就使用php做了一个分析工具,但是这类脚本语言终于还是有点上层建筑的味道,迁移起来不方便,至于具体的性能我也没比较过,但是想来鼎鼎大名的awk应该不差。

    测试数据emp.data如下:

name salary hour
Beth 4.00 0
Dan 3.75 0
kathy 4.00 10
Mark 5.00 20
Mary 5.50 22
Susie 4.25 18


awk的基本用法为模式+动作,可以二选一,语法会对每一行进行逐个处理,第一列为$1,以此类推,整行数据为$0,awk中只有两种数据类型:数值 和 字符构成的字符串,awk 程序一次从输入文件中读取一行内容并把它分割成一个个字段,通常默认情况下, 一个字段是一个不包含任何空格或制表符的连续字符序列.

例如想取出工作时间大于0的所有员工的姓名及薪水总和:

awk '$3 >0 {print $1, $2*$3}' emp.data

其中模式为$3>0,动作为{print$1,$2*$3},花括号的作用在于将模式和动作分开,使用单引号包括是防止shell解释程序中的$符号(注:如果你在shell中定义了var=ls,那么直接输入$var或者是"$var"会被shell解释成ls)。在 print语句中被逗号分割的表达式, 在默认情况下他们将会用一个空格分割来输出,每一行 print生成的内容都会以一个换行符作为结束.

    或者单纯的动作

awk '{print $0}' emp.data

    如果脚本过长,也可以建立一个文件,让awk来执行文件中的代码

echo '$3 >0 {print $1}' > awk_test
awk -f awk_test emp.data

使用-f参数来执行文件中的代码也是可行的。

    内建变量(Built-in Variables) 

    NF,字段数量,The number of fields in the current input record,因为只有3个字段,所以取得$3,如果某一行字段被判定不一样,那么它的NF也就不一样,如下方的NF就是为3

awk '{print $NF}' emp.data

NR,存储当前已经读取了多少行的计数(The total number of input records seen so far.)

awk '{print NR,$0}' emp.dataa

    如果想要改变输出的格式也是可以的

awk '{print "total pay for",$1,"is",$2*$3}' emp.data

    如果想要格式化输出的样式,可以使用printf,printf的格式为(format,value1,value2......),format定义格式,例如下方定义第一列为字符串,第二列为以数字的方式打印= $2*$3,并保留小数点后面两位,由于printf中没有换行,因此,每一行后面添加了换行符

awk '{printf("total pay for %s is $%.2f\n",$1,$2*$3)}' emp.data

又例如,定义8个字符宽度左对齐,6个字符宽度分别输出

awk '{printf("%-8s %6.2f\n",$1,$2*$3)}' emp.data

    排序输出

awk '{printf("%6.2f  %s\n", $2 * $3, $0)}' emp.data |sort -nr

    计算选择模式,选择总薪资大于50的员工,并格式化

awk '$2*$3 > 50 {printf("%.2f for %s\n"), $2*$3, $1}' emp.data

相等模式

awk '$1 == "Dan"' emp.data

正则表达式

awk '/^M/' emp.data

逻辑操作符

需要注意逻辑操作符对于两个条件都满足的行只会打印一遍,与存在两个模式是完全不同的,例如下方的Mary两个条件都满足,所以打印了两次

echo '$2>=5\n$3>20' > awk_test
awk -f awk_test emp.data

数据验证。数据的验证本质上是在否定,即打印可疑的行,这方面awk是很优秀的,

vim awk_test

NF != 3     { print $0, "number of fields is not equal to 3" }
$2 < 3.35   { print $0, "rate is below minimum wage" }
$2 > 10     { print $0, "rate exceeds $10 per hour" }
$3 < 0      { print $0, "negative hours worked" }
$3 > 60     { print $0, "too many hours worked" }

awk -f awk_test emp.data


特殊模式。“特殊模式 BEGIN 用于匹配第一个输入文件的第一行之前的位置, END 则用于匹配处理过的最后一个文件的最后一行之后的位置。”

BEGIN { print "Name    RATE    HOURS"}
      { print $0}

上方的语句是将代码放在文件中执行的,如果想写在shell窗口中,需以分号分割动作语句,如:

awk 'BEGIN {print "Name    RATE    HOURS"};{print $0}' emp.data

通过上面可以发现一个动作就是一个以新行或者分号分隔的语句序列。

如果想要自定义变量,在awk中无须声明,例如,想要统计多少员工工作时间大于18小时

awk '$3>18{emp=emp+1}END{print emp, "employers worked more than 18 hours"}' emp.data

用作数字的awk变量的默认初始值为0,所以我们不需要初始化 emp。

    如果想要统计员工总数,我们可以使用NR来计算,在文件中写入

END{print NR,"employees"}

为什么前面需要加入一个END呢?上面我写过“END用于匹配处理过的最后一个文件的最后一行之后的位置”,如果不加入END,就会在每一行都输出一次

{print NR,"employees"}

    计算平均薪资,第一个动作计算出所有薪资:

{pay=pay+$2*$3}END{print NR,"employees total pay is",pay,"average pay is",pay/NR}

    找出最高时薪的员工(如果存在多个,程序只会列出一个)

$2>maxrate{maxrate=$2;maxemp=$1}END{print"highest hourly rate:",maxrate,"for",maxemp}

模式定义每列的时薪要大于maxrate,随后,在第一个动作中,将每一行的时薪赋予给maxrate,由于模式中定义了规则,所以maxrate变量始终是最大的,最后END动作中打印出结果

    拼接字符串

{names=names$1"+"}END{print names}

    打印最后一行

{last=$0}END{print last}
或者
END{print $0}

    内置函数:

    计算字符串长度

{print $1,length($1)}

一个比较综合的示例,计算行,单词,字符的数量,在第一个动作中定义两个变量单词数量及字符串长度

{danci=danci+NF
chars=chars+length($0)
}END{print NR, danci, chars}


控制语句:

ifelse判断是否有大于4元的时薪,有则打印出来

$2>4{n=n+1; pay=pay+$2*$3; names=names$1"-"}
END {if(n>0)
        print n, pay,pay/n, names
     else
        print "no person"
}

while设置条件和执行体,一旦条件为真则一直执行

{i=18
        while (i <=$3){
        print $1,$3
        i=i+1
        }
}

for 循环

{for (i=18; i<$3;i=i+1)
        print i,"-",$1
}

awk中可以把相关值存入数组,例如,想要倒序打印文件

{line[NR]=$0} #必须先记住每个输入行
END{i=NR
        while(i>0){
        print(line[i])
        i=i-1
        }
}

“awk检查你的程序以确认不存在语法错误后,一次读取一行输入,并对每一行按序处理模式。对于每个匹配到当前输入行的模式,执行其关联的动作。不存在模式,则匹配每个输入行,因此没有模式的每个动作对于每个输入行都要执行。一个仅包含模式的模式-动作语句将打印匹配该模式的每个输入行。” 

程序的格式 

模式-动作语句以及动作中的语句通常以换行分隔,如果它们以分号分隔,则多个语句可以出现在一行中。分号可以放在任意语句的尾部。 

动作的开大括号必须与其对应的模式处于同一行;动作的其余部分,包括闭大括号,则可以出现接下来的行中。

1. BEGIN { 语句 }

在读取任何输入前执行一次 语句

2. END { 语句 }

读取所有输入之后执行一次 语句

3. 表达式 { 语句 }

对于 表达式 为真(即,非零或非空)的行,执行 语句

4. /正则表达式/ { 语句 }

如果输入行包含字符串与 正则表达式 相匹配,则执行语句

5. 组合模式 { 语句 }

一个 组合模式 通过与(&&),或(||),非(|),以及括弧来组合多个表达式;对于组合模式为真的每个输入行,执行语句

6. 模式1,模式2 { 语句 }

范围模式(range pattern)匹配从与 模式1 相匹配的行到与 模式2 相匹配的行(包含该行)之间的所有行,对于这些输入行,执行语句 。

--AWK程序设计语言


最后请注意: “BEGIN和END不与其他模式组合。范围模式不可以是任何其他模式的一部分。BEGIN和END是仅有的必须搭配动作的模式。”

    实战:

我的日志如下:

51.222.253.4 - - [02/Dec/2022:22:14:52 +0800] 
"GET /nba/0713 HTTP/1.1" 200 5106 "-" 
"Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)" 
"www.domain.com:443" 
"unix:///srv/NbaData.sock:0.459" "0.459"


想找出访问时间最长的日志

先来捋一捋思绪,如果使用默认的awk分隔符不太合适,因为,在UA中存在很多的空格,会导致列的长度不好取,由于我的时间字段在日志最后一个,所以可以考虑使用NF来解决,反正取最后一个值就完了

awk '{print $NF}' emp.data

但如果同时想将访问的IP、路径打印出来可能就有容易出错,所以我选择使用内置变量FS设定分隔符为引号

awk 'BEGIN { FS = "\"" };{print $12}' emp.data

awk 'BEGIN { FS = "\"" };{print $12,$1}' emp.data

awk 'BEGIN { FS = "\"" };{split($1,arr," ")};{print $12,arr[1]}' emp.data

split函数可以将字符串分解成数组,然后可以从定义的第二个参数中获取每个值

排序如下:

awk 'BEGIN { FS = "\"" };{split($1,arr," ")};{print $12,arr[1]}' access.log | sort -nr |uniq | head -10

非常清晰的看到这个64开头的ip访问时长异常,达到了惊人的47秒,肯定有问题。

如果想要取出访问时长的最长的路径也很简单,把参数修改一下即可

awk 'BEGIN { FS = "\"" };{split($2,arr," ")};{print $12,arr[2]}' access.log | sort -nr |uniq | head -10

综合ip和路径

awk 'BEGIN { FS = "\"" };{split($1,arrip," ")};{split($2,arrpath," ")};{print $12,arrpath[2],arrip[1]}' access.log | sort -nr |uniq | head -10

不得不说,AWK真的强!

《AWK程序设计语言》
# 内置函数
https://www.runoob.com/w3cnote/awk-built-in-functions.html