《Linux命令、编辑器与Shell编程》读书笔记4.2-格式化文本工具(awk)

来源:互联网 发布:马尔文粒径分析软件 编辑:程序博客网 时间:2024/06/04 22:38

格式化文本数据抽取工具awk

该工具主要是从具有一定格式的文本中抽取数据、然后以另一种方式展现。可以理解成数据库中的“视图”

【命令格式】

awk [-F分隔符] 'command' input-filename    使用-F指定被读取文件中的格式分隔符,默认是空格。

awk -f script-filename input-filename    使用-f调用已经写好的脚本文件,去处理被读取文件。

【处理原理】

以/etc/passwd文件为例,其内容格式如下:

at:x:25:25:Batch jobs daemon:/var/spool/atjobs:/bin/bash

awk -F: '{print "username:" $1}' /etc/passwd

当passwd文件被awk命令读入后,每行被分隔符(本例中是:)分成若干个域,从第一个域(可以理解成第一列)开始,被记为$1, $2...(如果要表示整条记录,则使用$0);

接着,awk按照{}里面的动作指令对每个域进行处理,直到处理完所有记录为止。

简单举例:使用awk输出包含文本头、尾的“视图”信息,这里用到了BEGIN和END(在编辑语句开始执行前后,输出指定的文本头尾信息)

假设存在以下文件:


awk 'BEGIN{print "Studen ID\tName\n---------------------"} {print $1"\t"$2}END{print "---------END--------"}' student2.2 | tee student2.2_awk

便得到了如下格式的新文件:


【正则表达、元字符、运算符和关系运算符】


举例:

1. 使用/28/匹配所有28开头的行:

awk '/28/{print $0}' student2.2

2. 先匹配所有28开头的行,然后打印这些行的1、2、3列:

awk '/28/{print $1"\t"$2"\t"$3}' student2.2

3. 输出第二列值为Liulu的行:

awk '$2=="Liulu"{print $0}' student2.2

4. 输出第二列值不为Liulu的行 :

awk '$2!="Liulu"{print $0}' student2.2

或者:

awk '$2 !~/[Ll]iulu/{print $0}' student2.2

5. 输出第一列以3、4结尾的行:

awk '$1 ~/^........[3,4]/{print $0}' student2.2

或者:

awk '$1 ~/^........[3-4]/{print $0}' student2.2

6. 输出第二列值为Xuli或Heli的行:

awk '$2 ~/(Xuli|Heli)/{print $0}' student2.2

或者:

awk '$2 ~/(Xuli|Heli)/' student2.2

*输出所有行的动作{print $0},可以省略,如上。

7. 加入算术运算符、并将计算结果生成新的一列:

假设存在如下list.txt文件:


计算每一行第四、五、六列的总和和平均值,并生成第七、八列;最后将新的文本内容保存到list_new.txt中:

awk '{print $1"\t"$2"\t"$3"\t"$4"\t"$5"\t"$3+$4+$5"\t"($3+$4+$5)/3}' list.txt | tee list_new.txt

8. 加入逻辑运算符实现更复杂的匹配操作:

针对上述新生成的list_new.txt文件,查找第二列值包含aa、第七列值大于150的行:

awk '($2 ~/^aa*/ && $6>150){print $0}' list_new.txt

*总之,使用awk时,一定要将执行编辑的语句放入单引号内,将多个模式和条件放在小括号中,函数和流控制语句放入大括号内,避免产生错误。

【在awk命令中使用内部变量】

awk的变量分两种,内部变量(Built-in Variables)和自定义变量。内部变量通常用于控制输出和保存awk当前工作状态等信息,在引用这些变量时通常不需使用$符号。

1. 关于awk全部的内部变量说明,可以使用man awk查看,在Built-in Variables部分有详细说明(本人使用awk版本为GNU Awk 3.1.8)。下面列举一些常用的

FILENAME:保存被读取文件的文件名

NF:保存当前正在处理记录的域个数(列数),即使只读取本行记录的某几个域,这个值也会把这行中所有符合分隔符条件的列数记录下来

NR:读取到的行数,当有多个输入文件、读取新文件时,不会被重置

FNR:保存读取当前文件的记录数(行数),当有多个输入文件、读取新文件时,awk会重置这个变量

OFS:设置输出记录的分割符,默认为空格

FS:设置输入文件中的字段是以何种分隔符分割

ORS:The output record separator, by default a newline.

RS:The input record separator, by default a newline.

OFMT:数字的输出格式,The output format for numbers, "%.6g", by default.

ENVIRON:An  array containing the values of the current environment.  The array is indexed by the environment variables, each element being the value of that variable (e.g., ENVIRON["HOME"] might be /home/arnold).  Changing this array  does  not  affect  the environment seen by programs which gawk spawns via redirection or the system() function.(读取环境变量)

举例:

(1) 手动设置输入文件的分隔符,并输出NF、FR、输入文件的文件名:

> awk 'BEGIN{FS=":";print "username\t\tShell"}{print NF,NR,$1"\t\t"$7}END{print FILENAME}' /etc/passwd

(2) 假设存在以下文件:

现在设置域(列)分隔符为#,记录(行)分隔符为回车:

> awk 'BEGIN{FS="#";RS="\\n\n"}{print $1,$2}' student3

如果不设置RS(行分隔符)为回车,则展示出来的结果会变成这样:

(3)指定输出数字的格式,比如要输出浮点型、小数点后保留2位的小数:

> awk 'BEGIN{OFMT="%.2f";print 3.14159}'

(4)引用环境变量:

awk 'END{print ENVIRON["LANG"]}' student3

或者:

awk 'BEGIN{print ENVIRON["LANG"]}'

*第一句由于使用了END表达式,所以最后必须引用一个文件名;如果不想引用文件名,把END去掉就好。

*上述两句可以合在一起写:

awk 'BEGIN{OFMT="%.2f";print 3.14159}END{print ENVIRON["LANG"]}' student3

【自定义变量】

(1)建立变量A并初始化为0,然后A与ls -l命令输出的第五列相加,得到所有文件占用的空间,最后在END表达式中将结果输出:

> ls -l | awk 'BEGIN{A=0}{A=A+$5}END{print "the total size of all files is : "A}'

变量也可以放到后面定义,比如,分别显示每个文件(夹)的大小:

> ls -l | awk '{print "the total size of "$9" is : "A}{A=0}{A=A+$5}'

*awk中引用变量时,通常不需要使用引用符号。但为了便于理解和阅读,应将变量名称大写并加上引用符号。


【awk命令的流程控制】

与C语言中的语法类似,包括if, while, do-while, for等。

1. if语句,允许多个if嵌套执行:

    if (条件表达式)

    {program1}

    else

    {program2}

2. 循环:

    while (条件表达式)

    {program}

或者:

    do

    {program}

    while (条件表达式)

或者:

    for (初始表达式;循环条件;步长)

    {program}

3. 用于控制循环的执行过程:

continue:立即跳转到循环初始位置开始新一次循环;这表示循环语句{program}内、它后面的语句在本次循环将不再执行。

break:立即停止循环

next:告诉awk立即读取文本的下一行,本行已经没有处理的必要

exit:如果出现在END语句中,则终止awk命令;如果不在END语句中,awk则直接跳到END处执行END表达式中的语句。

    <举例>

假设存在如下文本:


1. 输出第六列值大于150的行(仅输出符合条件行的第1,2,6列):

awk 'BEGIN{OFS="\t"}{if($6>=150)print $1,$2,$6}' list_new.txt

2. 输出第二列值以aa开头、且第六列值大于140的行(仅输出符合条件行的第1,2,6列):

awk 'BEGIN{OFS="\t"}{if($6>=140 && $2 ~/^aa/)print $1,$2,$6}' list_new.txt

3. 计算ls -l命令传递来的信息中,所有普通文件的大小和文件夹个数:

被调用的awk脚本文件如下:

> cat total.awk
BEGIN{
        #在BEGIN表达式中初始化变量A和count
        A=0;
        count=0;
}
{
        #如果域1的第一个字符为减号-,则将域5的值与变量A相加
        if($1 ~/^-/)
        A+=$5;
        #如果域1的第一个字符为d,则将变量count自加
        if($1 ~/^d/)
        count++;
}
        #在END表达式中输出计算结果和当前目录下文件夹总数
END{
        print "total:"A;
        #变量count-2,是因为ls -al命令输出中含有相对路径的当前目录和上级目录,如果使用ls -l则不需要-2
        print "directory counts:" count-2;
}

执行:

ls -al /etc | grep -f total.awk

*这里千万注意,BEGIN和END后面的{一定要紧跟在BEGIN后面,不能为了美观换行,否则调用时会报错

4. 使用ping命令发送4个ICMP数据包测试逐级之间的双向连通性,并用awk计算其中的最大时延、最小时延和平均时延

脚本ping.awk内容如下:
BEGIN{
        #使用重定义变量FS的方法设置域分隔符为空格,这里要根据ping命令实际的输出修改,不能照抄
        FS=" ";
        #初始化变量AVG,MAX,MIN
        AVG=0;
        MAX=0;
        MIN=0;
}
{
        #设置for语句循环次数
        for(I=1;I<9;I++)
        {
                #如果当前为第二条记录,则为变量MAX,MIN,IP_ADDR赋值
                if(NR==2)
                {
                        MAX=$10;
                        MIN=$10
                        IP_ADDR=$4;
                }
                #如果当前记录数大于1且小于6
                if(NR>1 && NR<6)
                {
                        #计算时延总和,这里要注意,由于shell类型不同,尽量不要用AVG=+$10这样的简写;老老实实用下面的表达式就行
                        AVG=AVG+$10;
                        #如果当前记录的时延小于变量MIN中记录的值,则更新MIN中的值
                        if($10<MIN)
                                MIN=$10;
                }
                #如果记录数大于6,则跳出循环并执行END中的表达式
                if(NR>=6)
                        exit;
                #否则拂去吓一跳记录,并重新开始循环
                next;
        }
}
END{
        #计算平均时延
        AVG=AVG/4;
        #输出IP地址,平均时延,最大时延和最小时延
        print "IP address:",IP_ADDR;
        print "Avg time:",AVG,"ms";
        print "Max time:",MAX,"ms";
        print "Min time:",MIN,"ms";
}

接下来先看下ping命令的输出结果:


因为冒号:和等号=影响了域(列)的分割,所以在调用上述脚本前,要先用sed命令把ping输出结果整理一下,最终命令为:

ping -c4 www.baidu.com | sed -e 's/=/ /g' -e 's/: / /g' | awk -f ping.awk


*FS变量支持设置多个域分隔符,如果将上述ping.awk脚本的第三行改写为:

        FS="[:= ]"; #注意方括号里还有一个空格

并将下面的$10替换为$11,就可以省去sed转换的过程直接被调用:

> ping -c4 www.baidu.com | awk -f ping.awk
IP address: 180.97.33.107
Avg time: 5.94 ms
Max time: 7.84 ms
Min time: 4.18 ms


【awk内置函数】


举例:

以下面的文本list_new.txt为例:


1. 搜索第一列值为4234的行,并把该行中第一个aa2替换为AA2:

> awk '$1 ~/4234/{print sub(/aa2/,"AA2",$0) "\n"$0}' list_new.txt
1
4234    AA2     aa2     38      68      131     43.6667

输出结果第一行的1,就是内部函数sub(/aa2/,"AA2",$0)执行的结果,执行了1次替换;之后的\n则表示换行,最后再把这行替换后的记录打印出来

*sub函数只针对每行的第一个符合匹配条件的字符串进行处理,一旦替换成功,则直接开始处理下一行

2. gsub:替换所有匹配到的字符(串),比如将上述文本中所有的aa替换成AA,并将替换次数N打印出来:

awk 'BEGIN{N=0}{N=N+gsub(/aa/,"AA",$0);print $0}END{print N}' list_new.txt

*由此可见,gsub函数的返回值为1。

3. 求出每行的字符数:

> awk '{print length($0)}' list_new.txt

只求出第一行的字符数:

awk '{print length($0);if(NR==1)exit}' list_new.txt

4.  查找某字符(串)第一次在本行中出现的位置:

> awk '{print index($0,"aa2")}' list_new.txt

*这里输出的结果数字,表示的是字符序号,不是列号;

5. 计算某字符(串)在给定字符串中首次出现的位置:

> awk 'BEGIN{print match("hello\!welcome to nanjing\!","nan")}'

给出结果为:nan在上述语句中出现的位置为18

6. split:将字符串按指定的分隔符拆开,然后放入指定的数组中并返回数组下标,比如:

> awk 'BEGIN{print split("FV7H8-FDH32-DSOJR-923IF-WEU32",ARR,"-");for(I in ARR)print ARR[I]}'

第一个5,表示给定的字符串被split使用-符号分割为5列,亦即split函数的返回值,后面的for语句则将数组ARR中的每个元素都打印出来。

*awk中的数组可以不用定义,也不必指定元素的个数;

7. 将匹配记录中的字符从大写转换为小写:

> awk '$1=="6790"{print tolower($0)}' list_new.txt

将文本中所有的大写字母转换成小写:

> awk '{print tolower($0)}' list_new.txt

8. 将匹配记录中的字符从小写转换为大写:

> awk '$1=="1234"{print toupper($0)}' list_new.txt

将文本中所有的小写字母转换成大写:

> awk '{print toupper ( $0 ) }' list_new.txt

9. 返回0至1之间的随机数:

> awk 'BEGIN{print rand()}'

10. 计算sin、cos等函数的值:

> awk 'BEGIN{pi=3.1415926;print sin(pi/4),"\n",cos(pi/4)}'

> awk 'BEGIN{print log(100)}'

#计算e的709次方:

> awk 'BEGIN{print exp(709)}'

> awk 'BEGIN{print sqrt(256)}'

【使用printf函数对输出字符串进行格式控制后再输出】

printf的使用格式:printf "修饰符 ",[参数1,参数2,...]

常用的修饰符:

1. 将数字100分别用用字符(也就是ascii码为100对应的字符)、八进制、十六进制数输出:

> awk 'BEGIN{printf "%c\n%o\n%x\n",100,100,100}'

2. 用不同的精度输出e的30次方的计算结果:

> awk 'BEGIN{printf "%e\n%f\n%g\n",exp(30),exp(30),exp(30)}'

3. 使用.号指定精度%m.n

m表示最小字符宽度,当实际字符显示,小于该值时,自动补空格,正数时数字右对齐,负数时,左对齐;(下面用#代替空格,方便查看)例如%3d   对应 1 的话,就是##1          %-3d   对应 1 的话,就是1##当实际的显示大于m,那就按实际输出,也就是m无意义了;例如%3d 对应1234的话,就是1234,没有空格;还有要注意,这里说的是字符的宽度,所以小数点.也要算进去。
4. 使用%s对齐输出,下例中的-15表示在对应字符串凑够15个字符宽度后、再打印下一个字符串(如果不够15,则在字符串后面补空格),%15s则表示如果不够15,则在字符串前面补空格:

> awk 'BEGIN{FS=":";printf "%-15s %-15s\n","username","Shell";printf "---------------------------\n"}{printf "%-15s %-15s\n",$1,$7}' /etc/passwd

5. 多列、多格式同时输出,还以上面的list.txt文件为例:


> awk '{printf "%-10s\t%-10s\t%d\t%d\t%d\t%d\t%d\n",$1,$2,$3,$4,$5,$3+$4+$5,($3+$4+$5)/3}' list.txt

这样输出的结果中、平均分一列就不会有小数了。


【awk用户自定义函数】

awk自定义函数的基本格式如下:

function name[(参数1,参数2,...)]

{

    语句块;

    [return 函数返回值];

}

1. 计算三个数总和与平均值的函数,并通过awk调用:

> cat sum_avg.awk#求和函数function Add(A,B,C){    return A+B+C;}#求平均值function Avg(A,B,C){    return (A+B+C)/3;}#打印源文件内容并附加结果列{    printf "%-10s\t%-10s\t%d\t%d\t%d\t%d\t%d\n",$1,$2,$3,$4,$5,Add($3,$4,$5),Avg($3,$4,$5);}#输出文件名END{printf "%s\n",FILENAME;}
#调用脚本处理list.txt:

> awk -f sum_avg.awk list.txt



2. 筛选每行中指定列的最值并返回:

> cat max.awk

function Max(Arr){        Ma=Arr[1];        for(I in Arr)        {                if(Ma<Arr[I])                Ma=Arr[I];        }        return Ma;}#使用for循环将不同列依次放入数组Ar中{        for(J=1;J<5;J++)        Ar[J]=$(J+2);        printf "%-10s\t%-10s\t%d\n",$1,$2,Max(Ar);}
> awk -f max.awk list.txt

就可以将list.txt文件中每行3、4、5列的最大值抽取出来。

*另一种算法:

> cat max2.awk

function Max(a,b,c){        a>b?a:a=b;        ma=a>c?a:a=c;        return ma;}{        printf "%-10s\t%-10s\t%d\n",$1,$2,Max($3,$4,$5)}
3. 使用awk监视磁盘状况并向用户发出警告:当某一个文件系统空余空间不足90%时,通过system函数运行mail命令向管理员告警

> cat df.awk

function mess(a,b,c){        #if usage is more than 0.1, alarm to root.        if(b>=0.1)        {                #use 'system' to excute Shell command                #First, use 'date' to find out current time, disk status, and put them in the temp file 'df.tmp'                #Then, send the content in 'df.tmp' to 346178152@qq.com and root user.                #Finally, delete the temp file 'df.tmp'.                system("date +'%F %r'>df.tmp;df -h>>df.tmp;cat df.tmp|mail -s 'Disk Warning' 346178152@qq.com,root;rm -rf df.tmp");                printf "Disk Warning!\nFile system: %s\nUsed:%3.0f%\nMounted on:",a,b*100,c;        }}{        #Judge if the current line begins with Disk name        if($1 ~/^\/dev\/sd/)        {                #calculate the usage rate                N=$3/$2;                if(N>=0.1)                        mess($1,N,$6);        }}
调用该脚本并执行:

> df | awk -f df.awk

使用root用户登录,可以看到如下提示:

使用type命令阅读当前最新的未读邮件:


0 0