Linux文本处理三剑客之awk学习笔记12:实战演练
此博文的例题来源于骏马金龙的awk课程以及awk示例的整合。一些在以往的awk学习笔记中有涉及的示例,这里就不再重复了。
处理代码注释
# cat comment.txt /*AAAAAAAAAA*/ # 整行都被注释所占满。 1111 222 /*aaaaaaaaa*/ 32323 12341234 12134 /*bbbbbbbbbb*/ 132412 # 注释的左右两边有内容,需保留。 14534122 /* # 跨行注释。 cccccccccc */ xxxxxx /*ddddddddddd # 跨行注释且注释的左边有内容,需保留。 cccccccccc eeeeeee */ yyyyyyyy # 跨行注释且注释的右边有内容,需保留。 5642341
需要充分理解哪些是应该删除的,哪些是应该保留的。
# cat comment.awk index($0,"/*"){ if(index($0,"*/")){ # 同行包含“*/”字符串。 # 12134 /*bbbbbbbbbb*/ 132412 print gensub("^(.*)/\\*.*\\*/(.*)$","\\1\\2","g") }else{ # 同行不包含“*/”字符串。 print gensub("^(.*)/\\*.*$","\\1","g") while(getline){ if(index($0,"*/")){ print gensub("^.*\\*/(.*)$","\\1","g") next # 这里不能使用break。请理解它们的区别。 } } } } !index($0,"/*"){ print } # awk -f comment.awk comment.txt 1111 222 32323 12341234 12134 132412 14534122 xxxxxx yyyyyyyy 5642341
这个代码还有一些可以优化的点,例如去除空白行与空行。
前后段落判断
有这样的一个文件。
# cat order.txt 2019-09-12 07:16:27 [-][ 'data' => [ 'http://192.168.100.20:2800/api/payment/i-order', ], ] 2019-09-12 07:16:27 [-][ 'data' => [ false, ], ] 2019-09-21 07:16:27 [-][ 'data' => [ 'http://192.168.100.20:2800/api/payment/i-order', ], ] 2019-09-21 07:16:27 [-][ 'data' => [ 'http://192.168.100.20:2800/api/payment/i-user', ], ] 2019-09-17 18:34:37 [-][ 'data' => [ false, ], ]
由多段构成,每一段的格式类似如下:
YYYY-MM-DD HH:mm:SS [-][ 'data' => [ 'URL', ], ]
需求:找出段信息包含“false”并且它的前一段包含“i-order”,然后将符合条件的这两段信息打印出来。
思路:
- 文本信息具有规律性,修改RS使得每段信息成为一条记录。
- 需要定义一个变量来保存前一段信息。
- 当前段信息和前一段信息需要同时满足条件。
# cat order.awk BEGIN{ ORS=RS="]\n" } { if($0~/false/&&prev~/i-order/){ # 只有第一条记录的$0会和prev相同。如果第一条记录同时包含了“false”和“i-order”,那么就要另作考虑了。 print prev print $0 } prev=$0 } # awk -f order.awk order.txt 2019-09-12 07:16:27 [-][ 'data' => [ 'http://192.168.100.20:2800/api/payment/i-order', ], ] 2019-09-12 07:16:27 [-][ 'data' => [ false, ], ]
行列转换
示例一
这道题我个人认为是比较经典的一道题目,尤其是进阶版的考察了awk的许多方面。
首先我们来看基础版,也就是作者的原版。
# cat RowColumnConvert.txt ID name gender age email 1 Bob male 28 qq.com 2 Alice female 20 163.com 3 Tony male 18 gmail.com 4 Kevin female 30 xyz.com
期望将行转换成列。
ID 1 2 3 4 name Bob Alice Tony Kevin gender male female male female age 28 20 18 30 email qq.com 163.com gmail.com xyz.com
原作者给出的答案。
# cat RowColumnConvert.awk { for(i=1;i<=NF;i++){ if(typeof(arr[i])=="unassigned"){ arr[i]=$i }else{ arr[i]=arr[i]"\t"$i } } } END{ for(i=1;i<=NF;i++){ print arr[i] } }
这种使用字符串连接再在其中加入一个制表符来构建的方式,如果某些记录的长度过长或者过短,就会导致排版的不统一。
在该示例中则是原第5行第4列“gmail.com”长度过长导致的。
这个代码要求每一行同字段之间的长度不可以太长。
因此我们来看一下进阶版,要求行列转换以后要对齐。
- 首先需要先将原始数据保存起来,然后再输出。原始数据由第N行第N列以及其对应的具体值来表述,例如“第3行第3列是female”,那么需要存储的信息就有3个,就可以使用二维数组。
- 使用变量i表示原始数据的行,变量j表示原始数据的列。在脑中要有这样的思路,不然很容易出错。
- 原文件行数和列数一致,容易造成误导,最好修改一下,使它们不一致。
- 对齐的思路是我们去计算应该填充多少空格字符。
# cat RowColumnConvert2.awk { for(j=1;j<=NF;j++){ arr[NR,j]=$j len[j]=length($j) maxLength[NR]=len[j]>maxLength[NR]?len[j]:maxLength[NR] } } func cat(count ,str,x){ # 这里的“局部变量”的定义很重要,尤其是如果这里使用了同名变量i或者j的情况下! for(x=1;x<=count;x++){ str=str" " } return str } END{ for(j=1;j<=NF;j++){ for(i=1;i<=NR;i++){ if(typeof(brr[j])=="unassigned"){ brr[j]=arr[i,j]""cat(maxLength[i]-length(arr[i,j]))" " }else{ brr[j]=brr[j]""arr[i,j]""cat(maxLength[i]-length(arr[i,j]))" " } } print brr[j] } } # awk -f RowColumnConvert2.awk RowColumnConvert.txt ID 1 2 3 4 5 name Bob Alice Tony Kevin Tom gender male female male female male age 28 20 18 30 25 email qq.com 163.com gmail.com xyz.com alibaba.com
示例二
name age alice 21 ryan 30
期望转换成:
name alice ryan age 21 30
# cat RowColumnConvert3.awk { for(i=1;i<=NF;i++){ if(typeof(arr[i])=="unassigned"){ arr[i]=$i }else{ arr[i]=arr[i]" "$i } } } END{ for(i=1;i<=NF;i++){ print arr[i] } } # awk -f RowColumnConvert3.awk test.txt name alice ryan age 21 30
示例三
# cat test.txt 74683 1001 74683 1002 74683 1011 74684 1000 74684 1001 74684 1002 74685 1001 74685 1011 74686 1000 100085 1000 100085 1001
期望输出:
74683 1001 1002 1011 74684 1000 1001 1002 74685 1001 1011 74686 1000 100085 1000 1001
# cat RowColumnConvert4.awk { if(!$1 in arr){ arr[$1]=$2 }else{ arr[$1]=arr[$1]" "$2 } } END{ for(i in arr){ print i,arr[i] } } # awk -f RowColumnConvert4.awk test.txt 74683 1001 1002 1011 74684 1000 1001 1002 74685 1001 1011 74686 1000 100085 1000 1001
格式化空白字符
主要涉及awk对于$N进行修改时会基于OFS来重建$0。在【字段与记录的重建】中我们已经提到过。
# cat chaos.txt aaa bb cccc dd ee ff gg hhhhh i jjjj # awk 'BEGIN{OFS="\t"}{$1=$1;print}' chaos.txt aaa bb cccc dd ee ff gg hhhhh i jjjj
在Linux中是对齐的,不晓得是不是博客园【插入代码】显示的问题。
筛选IP地址
目标是从ifconfig的输出结果中筛选出IPv4地址。这题我们以前就做过,具体的解题思路详见读取文件中的【数据筛选示例】,这里直接给答案。
ifconfig | awk '/inet /&&!/127.0.0.1/{print $2}' ifconfig | awk 'BEGIN{RS=""}!/^lo/{print $6}' ifconfig | awk 'BEGIN{RS="";FS="\n"}!/^lo/{FS=" ";$0=$2;print $2;FS="\n"}'
读取配置文件中的某段
这里我们以yum源的配置文件为例。我们过滤掉注释和空行。
# grep -vE "^#|^$" /etc/yum.repos.d/CentOS-Base.repo ... ... [extras] name=CentOS-$releasever - Extras mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=extras&infra=$infra gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 ... ...
期望仅取出某一段数据,例如[extras]段。
思路一:
- 配置文件具备规律性,将中括号作为记录分隔符。
- 基于上面那点再修修补补即可取到想要的信息。
# grep -vE "^#|^$" /etc/yum.repos.d/CentOS-Base.repo | awk 'BEGIN{RS="[";ORS=""}/^extras/{print "["$0}' [extras] name=CentOS-$releasever - Extras mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=extras&infra=$infra gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
思路二:
- 先找extras那行,找到以后输出。
- 随后循环getline并打印,直到遇到下一个配置段“[.+]”。
# cat extract.awk index($0,"[extras]"){ print while((getline)>0){ if($0~/\[.+\]/){ break } print } } # grep -vE "^#|^$" /etc/yum.repos.d/CentOS-Base.repo | awk -f extract.awk [extras] name=CentOS-$releasever - Extras mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=extras&infra=$infra gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
根据$0中的部分信息进行去重
首先来看示例文件。
# cat partDuplicate.txt 2019-01-13_12:00_index?uid=123 2019-01-13_13:00_index?uid=123 2019-01-13_14:00_index?uid=333 2019-01-13_15:00_index?uid=9710 2019-01-14_12:00_index?uid=123 2019-01-14_13:00_index?uid=123 2019-01-15_14:00_index?uid=333 2019-01-16_15:00_index?uid=9710
如果问号后面的“uid=xxx”相同,我们就认为是重复的数据,并且将其去除。
输出的时候,我们要保证原本的数据出现的顺序,因此就不应存入数组并进行无序遍历了。
思路在数组的实战中我们就有接触过了。
思路一:
以问号作为FS,将$2作为数组索引,每次awk内部循环对arr[$2]进行自增,第一次出现的数据arr[$2]的值就为1,仅针对第一次出现的数据进行输出即可。
# awk 'BEGIN{FS="?"}{arr[$2]++;if(arr[$2]==1){print}}' partDuplicate.txt 2019-01-13_12:00_index?uid=123 2019-01-13_14:00_index?uid=333 2019-01-13_15:00_index?uid=9710
思路二:
我们可以将“!arr[$2]++”拿来做pattern,第一次出现数据时返回值为1,往后的返回值均是0。
action部分只需要输出,并且以下三者等价:
PAT{print $0} PAT{print} PAT
关于pattern和action的省略情况,详见这里。因此我们就只需要pattern即可。
# awk 'BEGIN{FS="?"}!arr[$2]++' partDuplicate.txt 2019-01-13_12:00_index?uid=123 2019-01-13_14:00_index?uid=333 2019-01-13_15:00_index?uid=9710
次数统计
示例文件:
# cat test.txt portmapper portmapper portmapper portmapper portmapper portmapper status status mountd mountd mountd mountd mountd mountd nfs nfs nfs_acl nfs nfs nfs_acl nlockmgr nlockmgr nlockmgr nlockmgr nlockmgr
# awk '{arr[$0]++}END{for(i in arr){print i"-->"arr[i]}}' test.txt nfs-->4 status-->2 nlockmgr-->5 portmapper-->6 nfs_acl-->2 mountd-->6
统计TCP连接状态数量
详见数组的实战部分。
根据http状态码统计日志中各IP的出现次数
需求:统计web日志中,http状态码非200的客户端IP的出现次数,按照降序的方式统计出前10行。
日志文件放百度网盘了,提取码是jtlg。
111.202.100.141 - - [2019-11-07T03:11:02+08:00] "GET /robots.txt HTTP/1.1" 301 169 "-" "Sogou web spider/4.0(+http://www.sogou.com/docs/help/webmasters.htm#07)" "-"
# awk '$8!=200{arr[$1]++} END{PROCINFO["sorted_in"]="@val_num_desc";for(i in arr){if(cnt++==10){break}print arr[i]"-->"i}}' access.log 896-->60.21.253.82 75-->216.83.59.82 21-->211.95.50.7 21-->61.241.50.63 20-->59.36.132.240 18-->182.254.52.17 16-->50.7.235.2 15-->101.89.19.140 15-->94.102.50.96 13-->198.108.67.80
统计独立IP
# cat independence.txt a.com.cn|202.109.134.23|2015-11-20 20:34:43|guest b.com.cn|202.109.134.23|2015-11-20 20:34:48|guest c.com.cn|202.109.134.24|2015-11-20 20:34:48|guest a.com.cn|202.109.134.23|2015-11-20 20:34:43|guest a.com.cn|202.109.134.24|2015-11-20 20:34:43|guest b.com.cn|202.109.134.25|2015-11-20 20:34:48|guest
从该文件中统计每个域名及其对应的独立IP数。
例如,a.com.cn的行有3条,但是独立IP只有2个。因此需要记录的信息就是:
a.com.cn 2
将所有的域名及其独立IP的数量统计后输出到“域名.txt”格式的文件中。
# awk 'BEGIN{FS="|"} !arr[$1,$2]++{brr[$1]++} END{for(i in brr){print i,brr[i]>i".txt"}}' independence.txt # cat a.com.cn.txt a.com.cn 2 # cat b.com.cn.txt b.com.cn 2 # cat c.com.cn.txt c.com.cn 1
两个文件的处理
存在两个文件file1.txt和file2.txt:
# cat file1.txt 50.481 64.634 40.573 1.00 0.00 51.877 65.004 40.226 1.00 0.00 52.258 64.681 39.113 1.00 0.00 52.418 65.846 40.925 1.00 0.00 49.515 65.641 40.554 1.00 0.00 49.802 66.666 40.358 1.00 0.00 48.176 65.344 40.766 1.00 0.00 47.428 66.127 40.732 1.00 0.00 51.087 62.165 40.940 1.00 0.00 52.289 62.334 40.897 1.00 0.00 # cat file2.txt 48.420 62.001 41.252 1.00 0.00 45.555 61.598 41.361 1.00 0.00 45.815 61.402 40.325 1.00 0.00 44.873 60.641 42.111 1.00 0.00 44.617 59.688 41.648 1.00 0.00 44.500 60.911 43.433 1.00 0.00 43.691 59.887 44.228 1.00 0.00 43.980 58.629 43.859 1.00 0.00 42.372 60.069 44.032 1.00 0.00 43.914 59.977 45.551 1.00 0.00
需求:替换file2.txt的第5列的值为file2.txt的第1列减去file1.txt的第1列的值。
方法一
# cat twoFile1.awk { num1=$1 if((getline < "file2.txt")>0){ $5=$1-num1 print $0 } } # awk -f twoFile1.awk file1.txt 48.420 62.001 41.252 1.00 -2.061 45.555 61.598 41.361 1.00 -6.322 45.815 61.402 40.325 1.00 -6.443 44.873 60.641 42.111 1.00 -7.545 44.617 59.688 41.648 1.00 -4.898 44.500 60.911 43.433 1.00 -5.302 43.691 59.887 44.228 1.00 -4.485 43.980 58.629 43.859 1.00 -3.448 42.372 60.069 44.032 1.00 -8.715 43.914 59.977 45.551 1.00 -8.375
方法二
我们期望将file1.txt和file2.txt都直接作为命令的参数。形如:
awk '...rule...' file1.txt file2.txt
# cat twoFile2.awk NR==FNR{ # 如果NR和FNR相等,那么就表示awk在处理的文件是第一个文件 arr[FNR]=$1 } NR!=FNR{ $5=$1-arr[FNR] print $0 } # awk -f twoFile2.awk file1.txt file2.txt 48.420 62.001 41.252 1.00 -2.061 45.555 61.598 41.361 1.00 -6.322 45.815 61.402 40.325 1.00 -6.443 44.873 60.641 42.111 1.00 -7.545 44.617 59.688 41.648 1.00 -4.898 44.500 60.911 43.433 1.00 -5.302 43.691 59.887 44.228 1.00 -4.485 43.980 58.629 43.859 1.00 -3.448 42.372 60.069 44.032 1.00 -8.715 43.914 59.977 45.551 1.00 -8.375