• 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="]
    "
    }
    {
        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]"	"$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="	"}{$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="
    "}!/^lo/{FS=" ";$0=$2;print $2;FS="
    "}'

    读取配置文件中的某段

    这里我们以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
  • 相关阅读:
    MIT Linear Algebra#4 Determinants
    MIT Linear Algebra#3 Orthogonality
    MIT Linear Algebra#2 Vector Spaces and Subspaces
    MIT Linear Algebra#1 Solving Linear Equations
    MIT Linear Algebra#0 Introduction to Vectors
    Image Filter and Recover
    Computational Geometry
    TOP-K Problems
    pat 1151 LCA in a Binary Tree
    上传文件到git仓库中
  • 原文地址:https://www.cnblogs.com/alongdidi/p/awkTraining.html
Copyright © 2020-2023  润新知