日志分析利器 - awk快速入门&场景分析

2,347 阅读6分钟

这是我参与新手入门的第2篇文章

写代码和写文章还是有共性的,可以把你的想法转化为代码/文章,好好享受思考的过程。

背景

上篇文章在讲Linux运维场景的时候,在日志分析部分有提到过awk,为了防止坑留的太久,这篇文章就来填上

在Linux上处理文本,使用awk还是很方便的,有的同学可能会觉得Python处理文本会更加方便,确实是这样,很多文本处理我也在用Python做,但是在处理做一些简单的文本处理的时候,借助管道还是很方便的

本文主要介绍一下awk的概念,并结合一些常见场景讲解一下awk的基本用法,我不能保证所有同学都会喜欢这种操作, 但是awk绝对会帮你在Linux处理文本的时候提高效率

简介

首先我们来先了解一下什么是awk, 以及awk常见的操作选项以及操作方法

什么是awk

虽然我们经常说awk命令,但实际上它是一种编程语言,主要用于Linux中按行处理文本和数据,相对于grep的查找,sed的编辑,awk更加注重于对于文本的切片处理,现在基本上Linux发行版本基本上都会内置awk程序

一句话理解:awk是一种按行切片处理文本数据的编程语言。

awk原理介绍

大体上介绍一下awk运行的一个过程(不感兴趣可跳过)

基本结构

awk脚本的基本结构是下面这样的

awk [options] 'BEGIN{ commands } pattern{ commands } END{ commands }' fileName

其中常用 options 包括:

-f progfile,--file=progfile:从文件执行
-F fs,--field-separator=fs:指定输入时的字段分割符,默认的是**空白字符(包含空格、TAB 字符)**
-v var=val,--assign=var=val:在执行代码之前的定义变量

工作原理

  1. 获取option内容并执行,比如指定特定的分割符
  2. 执行BEGIN{commands} 语句块中的语句。这个可选的,BEGIN语句块是在开始读取行之前被执行的,比如一些变量初始化什么的。
  3. 从文件读取每一行,然后执行pattern{commands}语句块,它是顺序按行扫描文件,每一行都执行这段逻辑,一直到文件全部处理完。
  4. 当读至输入流最后的时候,也就是所有行都被读取并且执行完后,再执行END{commands} 语句块。比如一些打印分析结果可以写在这里面

常见用法解析

上面可能说的比较复杂,实际在我们使用的时候,尤其是进行日志分析的场景中,大部分都是shell操作,常见的格式是这样的

awk {commands} filename

awk 指的是命令前缀
commands 指的是实际执行的代码
filename 是需要操作的文件名,可以是多个

内建变量

awk 有很多内置的内建变量,常见的内建变量如下

$0: 当前记录(存放着整行的内容)
1-n: 当前记录的第n个字段,字段是有上面说的分割符分割的
FS: 前面说过了,是分割符,默认是空格或者TAB
NF: 当前记录中的字段数,就是列数
NR: 已经读出的记录数,就是行号,多个文件的话,这个值会累加
FNR: 当前记录数,和NR不一样的是,这个值会是各个文件自己的行号
RS: 输入记录的分割符,默认是换行符
OFS:输出字段分隔符,默认也是空格
ORS:输出的记录分割符,默认为换行符
FILENAME: 当前输入的文件名

比如我们有a.txt, 内容如下

小明 汽车 男 1234
小红 火车 女 2234
小黑 飞机 男 3234
小白 自行车 男 4234
小绿 走路 男 4234

示例1

$ awk '{print $0}' a.txt

示例说明:
a.txt是我们处理的文本文件。前面单引号内部有一个大括号,里面就是每一行的处理动作print $0。也就是打印文件中的每一行。

示例2

awk -F ':' '{print $1, $(NF-1)}' a.txt
小明 男
小红 女 
小黑 男 
小白 男 
小绿 男 

示例说明:
NF是列数,$(NF-1)就是倒数第二列,这样就是打印第一列和倒数第二列

示例3

awk -F ':' '{print NR ": " $2}' a.txt
1: 汽车 
2: 火车  
3: 飞机  
4: 自行车  
5: 走路

示例说明:
NR表示当前处理的是第几行,在里面要打印原字符,就用双引号括起来

数组

在awk中,数组是比较特殊的,都是关联数组,下标都是字符串值, 和在其他语言中不太一样的是,即使你用的下标是一个数字,awk也会将下标隐式转换成字符串,和map有点像
所以你只要记住一句话,awk中数组就是map

All arrays in AWK are associative, i.e. indexed by string values

数组赋值

array[index]=value

数组遍历

可以用for..in..语法,其中item是数组元素对应的下标,也就是数组的key

for (item in array)

数组包含判断

可以在if分支里使用in操作符判断元素是否存在

if (item in array)

示例

还是以上面的文本a.txt为例

# awk '{
    a[$1]=$3;
}

END {
print "小白" in a
for (i in a)
   printf "%s: %s\n", i, a[i];
}' a.txt

# 输出
1 // 代表包含小白这个元素
小黑: 男
小红: 女
小白: 男
小绿: 男
小明: 男

语句

上面我们用到的基本的语句就是print,也是用的最多的语句
还有编程语言必备的条件语句以及循环语句 另外常用的还有printf、delete、break、continue、exit、next等,这些语句除了printf会调用带括号的参数,其他的都没有

# echo 1 | awk '{printf("%s - %s\n", "juejin", "I love U")}'
juejin - I love U

条件语句

和其他语言一样,awk 支持 if, if-else, if-else if 语法,基本用法:

# 单条指令
if (condition)
    command

# 多条指令
if (conditional)
{
    command1;
    command2;
    ...
}

同样的,我们给出个简单的例子:
筛选性别为女生的姓名

# awk '{if($3 == "女") print$1}' a.txt
小红

循环语句

awk中循环语句支持for 和 while(while和do...while...),用法和在C语言中类似,相信大家都已经比较熟悉了,这里只做简单的说明

for 循环

for(初始化;布尔条件表达式;表达式)
{
    commands...
}

while 循环

while 语法:
       
while(布尔表达式){
    commands…
}
       
       
       
do while 语法:
       
do {
    commands…
}while(条件)

鉴于这块大家比较熟悉,不一一举例了

函数

awk中的函数也是很丰富的,基本可以满足我们正常使用,这里主要介绍三类函数:字符串函数,数学函数以及自定义函数

字符串函数

awk 覆盖了常见的字符串操作的函数,这里我就简单的列举一下常用的,并结合一些简单例子说明

  • length([s]): 返回字符串的长度,如果没有指定参数s,则默认使用$0作为参数
# echo "juejin" | awk '{print length();}'
6
  • index(str, target): 返回字符串在str中出现的位置,注意这里位置是从1开始计算的,没找到返回0
# awk 'BEGIN {print index("juejin", "j")}'
1
# awk 'BEGIN {print index("juejin", "0")}'
0
  • match(s, ere):返回字符串s匹配ere的起始位置,如果不匹配则返回0。该函数会定义RSTART和RLENGTH两个内置变量。RSTART与返回值相同,RLENGTH记录匹配子串的长度,如果不匹配则为-1
awk 'BEGIN {
print match("juejinjin", /jinjin/);
printf "Matched at: %d, Matched substr length: %d\n", RSTART, RLENGTH;
}'
4
Matched at: 4, Matched substr length: 6
  • tolower(s):将字符串转换成小写字符。 例如
#  awk 'BEGIN {print tolower("JUEJIN");}'
juejin
  • toupper(s):将字符串转换成大写字符。 例如
# awk 'BEGIN {print toupper("juejin");}'
JUEJIN
  • substr(s, m, [n]):字符串子串,返回从位置m开始的,长度为n的子串,位置也是从1开始计算,如果未指定n或者n值大于剩余的字符个数,就计算到字符串末尾为止。 例如:
# awk 'BEGIN { print substr("juejin", 2, 4); }'
ueji

数字函数

这里列举几个常用到的吧

sin(x):正弦函数;
cos(x):余弦函数;
exp(x):以自然对数e为底指数函数;
log(x):以e 为底的对数值;
sqrt(x):平方根函数;
int(x):将数值转换成整数;
rand():返回01的一个随机数值,不包含1

自定义函数

除了上面说的系统的自定义函数,用户可以根据需要自定义函数

function function_name(argument1, argument2, ...)
{
    function body
}

argument1, argument2 是参数列表,用逗号分隔,参数是局部变量,无法在函数之外访问,但是在函数中定义的变量是全局变量,可以在函数之外访问

# echo line | awk '
function find_min(a, b){
  c = a+b
  if (a < b)
    return a;
  return b;
}

{
    printf("pre c=%d\n",c)
    print find_min(1,2);
    printf("after c=%d",c)
}'
pre c=0
1
after c=3

除了上面说的这些函数,还有个可能会用到的函数就是system,主要用于执行外部命令

# awk 'BEGIN {system("uname -m");}'
x86_64

使用场景

讲完了基本的用法,让我们来看一下在实际应用过程中的一些用法吧。结合场景大家可以理解的更快一些

统计

这里列举两个我自己平时用的较多的命令

统计一列数字的和

ls -l *.go *.conf *.sh | awk '{sum+=$5} END {print sum}'

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

ps aux | awk 'NR!=1{a[$1]+=$6;} END { for(i in a) print i ", " a[i]"KB";}'

从ifconfig命令的结果中筛选出除了lo网卡外的所有IPv4地址

ifconfig | awk '/inet / && !($2 ~ /^127/){print $2}'

日志分析

假设你有一个Nginx服务, 打印了一些access日志 access.log

日志格式:
'$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for"'

统计访问IP次数:

# awk '{a[$1]++}END{for(v in a)print v,a[v]}' access.log

统计访问IP次数并排序取前10:

# awk '{a[$1]++}END{for(v in a)print v,a[v]|"sort -k2 -nr |head -10"}' access.log

统计访问访问大于100次的IP:

# awk '{a[$1]++}END{for(v ina){if(a[v]>100)print v,a[v]}}' access.log

统计访问最多的10个页面:

# awk '{a[$7]++}END{for(vin a)print v,a[v]|"sort -k1 -nr|head -n10"}' access.log

统计每个IP访问状态码数量:

# awk '{a[$1" "$9]++}END{for(v ina)print v,a[v]}' access.log

统计访问IP是404状态次数:

# awk '{if($9~/404/)a[$1" "$9]++}END{for(i in a)print v,a[v]}' access.log

总结

本文主要介绍了awk的基本概念、基本用法以及两个常见的应用场景,希望大家看完这篇文章能在以后的日志分析中快速定位问题

时间关系,没列举太多应用场景,后续有时间或者我在工作中用到了特别有代表性的场景,我会更新在这篇文章


我是爱篮球、爱coding的程序员伍六

关注我,给你带来更多你想看的干货