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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章