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

 

posted @ 2021-02-06 11:10  阿龙弟弟  阅读(365)  评论(0编辑  收藏  举报
回到顶部