三剑客grep,sed,awk
log分析是非常常见的异常追查手段,但是由于生产环境下log体量极为庞大,正确的选择分析工具是快速排查异常的先决条件。
那么如何从海量的log信息中基于特定的格式自动抽取我们需要的信息呢?这里我们就会用到Linux世界里赫赫有名的三剑客(grep,sed,awk)
1.概述
grep:是Linux系统中的一个文本搜索工具,使用正则表达式搜索文本,功能最为简单,也最快,适用于单纯的查找和匹配
。
sed:是一种非交互式编辑器,能够执行vi相同的编辑任务,一次处理一行内容,适用于编辑匹配文本
。
awk:是一个强大的文本分析工具,也可以把他理解为脚本语言解释器,有很多内建功能,适用于处理格式化的文本,对文本进行复杂的格式化处理
。
综上所述:
应用难易程度:grep <= sed < awk
功能:grep < sed <awk
下面由易到难逐步介绍这几个命令:
2.grep
输入:文件或一个标准输入stdin或管道
输出:打印至屏幕,或重定向至文件,或输出至管道
命令格式: grep [options] 'pattern{actions}' filename
grep单独使用时grep会一直等待,直到该程序被中断。如果遇到了这样的情况,可以按“Ctrl+c”终止。避免这种情况的方式是将需要查询的文件通过管道扔给grep程序,例cat /etc/passwd | grep root
2.1常用的option ()内是笔者记忆的窍门
-o 这个选项时最常用,只显示匹配内容,grep默认显示满足匹配条件的那一行的所有结果 (only-matching)
-i 比较字符时忽略大小写(ignore)
-w 模式作为单词来查找,相当于正则中"<word>" (word)
-x 只显示被匹配到的内容是整行的结果,相当于正则中"^$",一般是用于利用正则表达式匹配整行(regexp)
-v 取反,输出我们定义模式相反的内容 (invert)
-c 统计匹配到的行数,统计匹配比例时可以,比如先计算出匹配行数,然后用wc -l 计算出总行数,这样就可以求出匹配比例(count)
-m 可以匹配的最大个数 ,比如特例抽取时,可以通过限定匹配的最大个数,简化结果(max)
-n 在输出结果时显示行号 (number)
2.2常用的正则表达式
^行首
$行尾
\<词首
\>词尾
. 除换行符以外的所有字符
*匹配零个或者多个前导字符
[]匹配任意一个包含字符
[^]匹配不属于包含字符中的任意一个字符
x{m}重复字符x,m次,如grep 'a{5}'表示匹配包含5个a的行
x{m,}重复字符x至少m次,
x{m,n}重复字符x至少m次,但不能多于n次如grep 'a{5,10}表示匹配包含5~10个a的行
3.sed
输入:文件或者一个标准输入或管道
输出:打印至屏幕,或重定向至文件,或输出至管道
工作原理:sed会逐行
从数据源读入数据,放置在一个临时缓冲区(术语叫模式空间)中,然后对数据进行处理后输出,默认理论上是不修改数据源的内容,但是如果加入-i操作,就会将处理后的数据输出至数据源,所以需要-i操作时一定要慎重。
此外,如果是连续命令,sed每个命令都是前一个命令的输出,所以命令顺序是很重要的
,例如:sed 's/abc/def/g;s/def/jqk/g' file,这个命令相当于sed 's/abc/jqk/g' file,因为file文件中的abc 先被替换成def,然后def再被替换成jqk。
命令格式:sed [options] 'pattern{actions}' filename
3.1常用的option ()内是笔者记忆的窍门
-r sed 使用扩展正则。
-i 直接修改文档读取内容,不再屏幕上输出。
-n 使用安静模式,只打印被sed处理的行。⚠️此选项只有当行整体内容被改变时才会打印处理的行,比如a
,i
,c
命令;如果后面内容处理命令是s
部分替换命令,需要配合p
命令才可以打印出处理的行,例如 sed -n 's/root/newroot/gp' /etc/passwd
3.2常用处理命令
处理命令中不仅仅包含编辑命令
,还有地址匹配命令
。地址匹配命令是处理命令中非常重要的一个部分,它的作用是告诉sed处理命令适用于那些行,缺省的地址匹配命令是全局匹配,即对所有内容执行编辑操作。
地址匹配命令:行号(例如:^,1,2,3,4....$ )或正则表达式 缺省值为全局匹配,即编辑命令应用到所有行 指定了一个值,表示编辑命令只匹配该行,这个值可以由行号给出,也可以由正则表达式匹配后给出 指定一个地址对,将编辑命令应用至这个地址对包含范围中的所有行,地址对可以全部由行号组成,也可以全部由正则表达式组成,还可以由行号和正则表达式混合组成,例如:1.(2,5)表示2到5行;2.(/正则1/,/正则2/) 正则1为开始条件,正则2为结束条件,先找满足开始条件的行,然后再找满足结束条件的行,如果找到了满足结束条件的行后,再向下继续寻找满足开始条件的行。3.(/正则1/,3)或者(2,/正则2/) 反向地址匹配,在地址后边添加一个感叹号(!),则将编辑命令全部应用到没有匹配到的文本行上
a:在满足条件的行的下一行添加(add)
i: 在满足条件的行前插入文本⚠️(与选项中的-i要区分开)(insert)
d:把满足条件的行全部删除(delete)
c:用新文本替换匹配范围内的所有行⚠️(是把所有行全部替换新文本,而不是逐行替换)
(copy)
s:用新文本替换匹配上的字符串(部分替换)(seperate)
g: 对每行进行执行全局匹配(global)
⚠️s一般和g一起使用,如果一行出现多个正则匹配项,单独使用s命令,只会替换每一行第一个匹配项,如果在命令末端添加g,则可以替换每一行全部的匹配项
y: 转换命令,字符按照一对一的方式从左到右进行转换
不能对正则表达式使用此命令
,例如:y/abc/ABC/,会将a转换成A,b转换成B,c转换成C。(Transliterate转译,可以利用译的拼音yi的首字母)p: 打印命令,用于显示模式缓存区的内容。(print)
q: 结束命令,处理到匹配行后sed程序不再执行(quit)
示例
#匹配root行 与匹配bin行之间所有内容,在每一行的后面追加一行test字符串
sed '/root/,/bin/a test' /etc/passwd
#第一行和第一个匹配bin行之间的所有内容,在每一行的前面插入一行test字符串
sed '1,/bin/i test' /etc/passwd
#删除第一行至第五行的所有内容
sed '1,5d ' /etc/passwd
#将匹配root行到第5行的所有内容替换成 test
sed '/root/,5c test' /etc/passwd
#将所有内容中 root替换成newroot
sed 's/root/newroot/g' /etc/passwd
#只打印执行了上边处理命令的行
#⚠️如果sed采用安静模式时sed -n,后序的处理命令是替换命令s时,需要配合p命令使用才可以打印出处理过的行,否则将无任何输出
sed -n 's/root/newroot/gp' /etc/passwd
3.3多行处理命令(进阶)
上面我们提到了,sed命令是将每一行的内容读到一个叫模式空间
的缓冲区里,然后执行命令。并且如果sed执行N/D/P 三个命令,还会形成多行处理的模式空间,但是多行处理时某些场景可能需要保存模式空间中被处理的内容,这里就引入了另外一个缓存空间--保持空间。
保持空间用于保存模式空间的内容,两个空间中的内容可以相互复制
命令 | 说明 | 记忆 |
---|---|---|
h/H | 将模式空间的内容复制或者追加到保持空间 | hold |
g/G | 将保持空间的内容复制或者追加到模式空间 | get |
x | 交换两个空间的内容 | exchange |
小写是覆盖目的空间的内容,大写是追加到目的空间,追加的内容前加\n区别于原有内容
经典例子:
如何利用sed替换换行符?
sed 'H;$!d;${x;s/^\n//;s/\n/,/g}' /etc/passwd
解析:H将每一行追加到保持空间,$!d对不是最末一行的都执行删除操作,主要是利用d命令打断常规的命令执行流程,让sed继续读入新的一行,直接到最后一行都放在保持空间,x将保护空间与模式空间模式对调,s/^\n//去掉空行,s/\n/,/g将换行符替换成逗号
3.4流程控制(进阶)
sed提供了分支命令b(branch)和测试命令test两个命令来控制流程,可以实现类似与C++中goto语法的想过,跳转到指定标签位置继续执行命令。标签是以符号 : 为开头
命令 | 说明 | 区别 |
---|---|---|
b label | 跳转到分支label处,如果label不存在则索引到命令尾部 | 在匹配行进行无条件跳转 |
t label | 在最后读取完最后一行输入或者上一次执行t命令之后,成功执行了一次s///语句,分支到label,如果label不存在则索引到命令尾部 | 需要成功执行一次s替换语句才可跳转,即有条件跳转 |
用法如下:
- 基本用法
':lable;command1;command2;/pattern/b lable;command3'
执行到pattern/b lable时,如果匹配,则跳转到头,继续执行command1,如果不匹配,则往下执行command3
- 仿if流程控制
'command1;/pattern/b if;commad2;b;:if;command3'
执行/pattern/b if,如果匹配则执行commad3,如果不匹配则执行command2,然后跳到最后(分支命令如果后边不接标签,则默认位置为最后)
- 只利用模式空间将换行符替换成","方法有很多中,命令也有很多种
#利用test标签
sed ':label;$!N;s/\n/,/g;t label' /etc/passwd
#利用branch标签
sed ':label;$!N;s/\n/,/g;$!b label' /etc/passwd
#利用y转换命令,由于只转换一个字符
sed ':label;$!N;y/\n/,/;$!b label' /etc/passwd
4.awk
4.1awk文本处理流程
首先,我们需要了解awk命令处理文本的流程,awk从输入中读入数据时,工具RS会讲输入分割为许多记录(records),awk的每一次循环都会读取一个record进行处理。
awk默认的记录分隔符(RS)
为换行符,因此缺省记录分隔符,awk一次处理一行。
在处理每一行时,awk会将根据字符分隔符(FS)
分割读入的记录文本,分割成列。
awk默认的字段分隔符是空格,所以,awk默认会根据空格或者连续几个空格划分每一个记录。
abc def hidk
|1 | 2 | 3 |
| 0 |
$0代表整条记录,$1表示第一列,$2表示第二列
命令格式:awk [options] 'pattern {actions}' filename
4.2常用option
-F:字段分隔符,⚠️大写(field-separate)
-v:设置变量(var)可以设置内置变量,也可以设置自定义变量
awk -F":" '{print $1}' /etc/passwd
awk -v FS=":" '{print $1}' /etc/passwd
上段代码是自定义字段分隔符的两种模式,一个是利用的-F直接设置字段分隔符,一个利用的是-v设置awk内置变量,字段分隔符的内置变量名是FS。
下面列举一下awk常用的内置变量:
内置变量名 | 说明 |
---|---|
RS | 输入记录分隔符的值,缺省值为\n |
ORS | 输出记录分隔符的值,缺省值为\n |
FS | 输入字段分隔符的值,缺省值为空格 |
OFS | 输出字段分隔符的值,缺省值为空格 |
FNR | 每个文件已经处理的记录数,多文本处理时,切换文件时,此值都会重置 |
NR | 已经处理的累计记录书,多文本处理时,切换文件,此值依然不会重置 |
NF | 当前行的字段的个数 |
FILENAME | 当前处理的文件名 |
ARGC | 表示命令行中除了选项和选项参数以外的所有参数的个数,awk命令也算一个参数,表示命令名位置是ARGV[0] |
ARGV | 数组,记录命令行中的参数,数组下标从0开始 |
-
多字段分隔符的应用
根据上图很明显可以看到:
FS='[/:]' 表示的是[]中的如果一段记录中有字符是/或:都将分隔
FS='[/:]+' 表示的是如果一段记录中存在/或:或二者的任意组合,都可进行分割。
4.3常用命令
4.3.1BEGIN和END
命令是pattern+actions的合集,pattern可以是正则表达式,也可以是布尔表达式,还可以是两个特殊的模式:BEGIN和END
- BEGIN模式,允许用户在初始化文本之前执行BEGIN代码块,多用于初始化内置变量,初始化以后程序用到的全局变量等
- END模式,允许用户在处理完所有行之后执行END代码块,多用于执行最终计算或者打印提示信息等
最长用到的BEGIN代码块,以初始化字段分隔符为例:
未使用BEGIN模式
使用了BEGIN模式
区别在于第一行,因为如果没有使用BEGIN时,awk处理第一条记录时使用的是默认字段分隔符,FS是在执行第一条命令时设置的,这个时候第一条记录已经完成分隔,所以从第二行才生效。
4.3.2pattern
除了BEGIN和END两个特殊匹配模式,还有正则表达式/REG/actions,和布尔表达式/bool /actions。
awk '/root/{print $0}' /etc/passwd#正则表达式
awk 'NR==3{print $0}' /etc/passwd #布尔表达式
4.3.3命令中的actions
awk是类似与shell的脚本编程语言,既然是编程语言,就必须支持最基本的变量定义以及必要的流程控制。
4.3.3.1独立运行的脚本
- awk 有一个叫做-f的选项
注意和-F加以区分
,这个选项可以接awk源码文件,在Linux及UNIX中凡是#!后边跟的统统是加载器(解释器)的路径,所以我们可以通过这一特性创建属于我们自己的awk文件,利用which确定自己的awk路径,例如/bin/awk
#!/bin/awk -f
BEGIN {print "i'm awk file"}
命令为test.awk,然后通过chmod 命令添加可执行权限,然后运行./test.awk
注意:awk文件中是多个'pattern{actions}'组成,不能在文件中指定处理参数options,也不能指定处理文件,处理文件在文件命令外部执行,例如 ./test.awk /etc/passwd
- 通过shell脚本调用
#!/bin/bash
awk -F ":" '{
#actions
}
' /etc/passwd
实际上就是将awk命令分行编写不需要加";"进行命令分割,有利于阅读
4.3.3.2变量
- 数组
awk中的数组都是关联型数组,类似与C++中的map结构,所以无论是什么索引最终都会被转换成字符串索引进行存储。
#!/bin/awk -f
BEGIN {
list[0]="abc"
list[1]="def"
list[2]="xyz"
print list[0]
print list[1]
print list[2]
}
4.3.3.3流程控制
如果你有C语言基础,阅读awk命令毫无障碍
- if语句
#!/bin/awk -f
BEGIN {
list[0]="abc"
list[1]="def"
list[2]="xyz"
if( "def" == list[0] ){
print "index 0 "list[0]
}
else if("def" == list[1]){
print "index 1"list[1]
}
else{
print"index nothing"
}
}
- 循环语句while
awk支持的循环语句是"do... while"类型,即中间代码至少需要执行一次,如果不满足条件一次都不自信,可以在循环外嵌套条件语句作为第一次进入的条件。
#!/bin/awk -f
BEGIN {
list[0]="abc"
list[1]="def"
list[2]="xyz"
count=0
do{
print list[count]
count++
}while(count<3)
}
- 循环语句for
awk中支持两种for循环
1.for(initial ;comparison;increment){}
2.for(index in array){}
第二种仅用于array
#!/bin/awk -f
BEGIN {
list[0]="abc"
list[1]="def"
list[2]="xyz"
for(i=0;i<3;i++){
print list[i]
}
for( i in list){
print list[i]
}
}
- break和continue
与C语言中的break和continue用法完全一致这里就不再赘述 - next命令
告诉awk跳过后边所有的代码段,直接处理下一记录,命令有助于提高代码的运行速度。比如代码段运行break后不再对当前行进行处理时,可以用next代替,直接进行下一记录的编辑。
4.3.3.4函数(进阶)
拥有函数语法是awk被称为语言的重要原因之一
1.自定义函数
格式为:function name(arg1,arg2,...){statements}
参数列表由逗号分割,参数默认为局部变量,无法在函数之外访问,但是⚠️⚠️在函数内部定义的变量是全局变量,是可以在函数之外访问
#!/bin/awk -f
function fun(a){
b=a
print "a in function = "a
}
BEGIN {
print "b before func = "b
fun("test")
print "a = "a
print "b = "b
}
2.库函数
1.数学函数
函数名 | 说明 |
---|---|
srand(n) | 设置随机种子,默认为当前时间为种子 |
rand() | 返回[0,1)中的一个随机值,注意不包含1 |
int(n) | 强制转化成整数,当不是数字时,返回数值为零 |
sqrt(n) | 绝对值函数 |
sin(n) | 正弦 |
cos(n) | 余弦 |
exp(n) | e的n次方的指数 |
log(n) | e的n次对数 |
2.字符串函数
函数名 | 说明 |
---|---|
sub(pattern,repl[,in]) | 将in(输入)中的pattern内容替换成repl,只替换第一个匹配的substring |
gsub(pattern,repl[,int]) | 和sub功能类似,但是是全局替换 |
length([,in]) | 字符串长度,如果没有指定,默认使用$0 |
index(s,t) | 字符串t在s第一次出现的位置,位置从1开始计算,如果没找到返回0 |
match(s,pattern[,array]) | 找到s中pattern匹配的起始位置,并返回,位置从1开始计算,不匹配返回0,函数定义了两个全局的内置变量RSTART和RLENGTH,RSTART与返回值相同,RLENGTH记录匹配子串的长度,如果没有找到,此值为-1 1.没有array的情况下,通过pattern匹配,在s中寻找最左边,最长substring,返回的就是substring的index;2.存在array的情况下,将pattern中用()匹配的内容按照顺序放入array中,array[1]代表第一个()中的内容 |
split(s,array[,fs]) | 字符串根据fs切分s字符串然后存入array中,fs的缺省值为内置变量FS的值 |
strsub(s,m[,n]) | 返回从位置m开始,长度为n的字串,位置从1开始 |
tolower(s) | 全部变成小写 |
toupper(s) | 全部变成大写 |
spritf(format,arg,...) | 格式化输出 |
3.时间函数
命令名称 | 说明 |
---|---|
mktime(datespec) | 参数按照(YYYY" "MM" "DD" "HH" "MM" "SS)注意空格分割,空格需要双引号包围
|
strftime([format [, timestamp[, utc-flag]]]) | 将时间戳格式化后自定义输出,utc缺省值为本地时间,格式化模式字符与ANSI C的strftime一致 |
systime() | 返回当前的时间戳 |