Linux文本處理三劍客之awk學習筆記08:數組

數組

在bash中我們已經見識過了數組。awk的數組和bash的數組的主要區別在於其支持的是關聯數組,而bash支持的是數值索引數組。

假設存在這樣一個數組。

arr=["zhangsan","lisi","wangwu"]

數值索引的下標是從0開始的數值。

arr[0] ==> "zhangsan"
arr[1] ==> "lisi"
arr[2] ==> "wangwu"

數組索引只能存儲一種信息,如果想存儲多種信息一般是向數組元素存儲多個信息並使用一個統一的分隔符。

arr=["zhangsan:18","lisi:28","wangwu:38"]

然後在處理數組元素時再根據分隔符做額外的處理。而關聯數組的下標則是字符串,這就意味着其天生就比數值索引數組多存儲一種信息。

arr["zhangsan"] ==> 18
arr["lisi"] ==> 28
arr["wangwu"] ==> 38

關聯數組的索引(下標)也可以是數值,只不過其內部會將其轉換成字符串。

像關聯索引這類保存key-value形式數據的數據結構,在其他編程語言當中也被叫做map、hash和dictionary等。

關聯數組的數組順序是不好確定的。

  • 即便字符串索引看起來是有順序的,但是在其內部會將該字符串轉換成其他編碼。
  • 因此也與用戶存入數組的順序無關。

awk的數組還支持多維數組(Multidimensional Array)和嵌套數組(Arrays of Arrays)。

數組的創建、訪問和賦值

awk中的數組的創建沒有專門的語句,當我們第一次訪問數組或者向數組元素賦值時,數組就自動創建了。

arr[idx]    # 訪問數組元素
arr[idx]=elements    # 向數組元素賦值

這裏的idx不要寫成index,awk有一個內置函數就叫index()。

此前我們提到數組的索引即使是數值也會自動轉換成字符串。因此會有一些需要注意的陷阱。

arr[1]和arr["1"]是等價的。awk會將數值1轉換成字符串"1"再作爲索引存儲。

# awk 'BEGIN{arr[1]=10;arr["1"]=20;print arr[1];print arr["1"]}'
20
20

在對下標進行數值到字符串的轉換時,會根據預定義變量CONVFMT(默認是%.6g)來進行轉換。

# awk 'BEGIN{arr[123.45678]="alongdidi";print arr[123.45678]}'
alongdidi    # 怎麼存就怎麼取,可以取到數據。
# awk 'BEGIN{arr[123.45678]="alongdidi";print arr["123.45678"]}'
    # 數值存,字符串取,即使保持“一樣”也取不到,因爲根據CONVFMT做了轉換。
# awk 'BEGIN{arr[123.45678]="alongdidi";print arr["123.457"]}'
alongdidi    # 使用字符串取,必須直到其轉換後的真實字符串爲多少。

如果我們訪問數組中不存在的元素(首次使用該索引),那麼會創建該索引並且其值爲空。這是我們所不希望看到的,關聯數組本身可用於存儲兩種順序,如果存儲空信息的話就是無效的數據,會造成內存空間的浪費。空元素多了也會影響數組的性能。

# awk 'BEGIN{arr[1];arr[2];print length(arr)}'
2

數組的長度

使用length()函數可用於獲取/返回數組的長度。它也可用於獲取數值和字符串的長度。

# awk 'BEGIN{arr["name"];arr["age"]=29;print length(arr)}'
2
# awk 'BEGIN{print length(100),length("alongdidi"),length(3.1415)}'
3 9 6

數組元素的刪除

delete arr[idx]    # 刪除數組中的具體某個元素
delete arr    # 刪除數組中的所有元素
# awk 'BEGIN{arr[1];arr[2];print length(arr);delete arr[1];print length(arr)}'
2
1
# awk 'BEGIN{arr[1];arr[2];print length(arr);delete arr;print length(arr)}'
2
0

數組的判斷

判斷一個變量名稱是否是數組可以使用兩種方式。

typeof(var)    # 如果是數組則返回字符串"array"。
isarray(var)    # 如果是數組則返回數值1
# awk 'BEGIN{arr[1];print typeof(arr)}'
array
# awk 'BEGIN{arr[1];print isarray(arr)}'
1

即便我們刪除了數組中的所有元素,那麼該變量的類型依然是一個數組,因此我認爲數組應該是無法刪除並且後續只能作爲數組,否則會報錯。

# awk 'BEGIN{arr[1];delete arr;print typeof(arr)}'
array
# awk 'BEGIN{arr[1];delete arr;print isarray(arr)}'
1
# awk 'BEGIN{arr[1];delete arr;arr="alongdidi";print typeof(arr)}'
awk: cmd. line:1: fatal: attempt to use array `arr' in a scalar context

數組元素的判斷

上文我們說過一個未賦值過的元素的值是一個空的,因此可能有人使用這種方法來判斷數組元素是否存在。

if(arr[idx]=="") {
    print "Element is not exist."
} else {
    print "Element is exist."
}

這麼做存在2個問題。

  • 數組元素可能本身就是空值,也就是說數組元素存在但其值未空。
  • 雖然數組元素不存在,但是經過判斷以後它就存在並佔用了數組的空間,只不過其值爲空。

我們可以使用這種方式來判斷數組元素是否存在。

if("idx" in arr) {
    print "Element is exist."
} else {
    print "Element is not exists."
}

這裏的idx是一個具體的索引名稱。"idx" in arr會返回1表示數組元素存在,返回0表示數組元素不存在。idx的雙引號很關鍵,加了是字符串用於數組元素判斷,不加是變量用於遍歷數組。

# awk 'BEGIN{arr["name"];print("age" in arr)}'
0
# awk 'BEGIN{arr["name"]="alongdidi";if("name" in arr){print "exist"}}'
exist
# awk 'BEGIN{arr["name"];if("name" in arr){print "exist"}}'
exist
# awk 'BEGIN{arr["name"];if("age" in arr){print "exist"}else{print "not exist"}}'
not exist

當然了,如果使用delete刪除元素,那麼數組元素自然就不存在了。和刪除數組不同,刪除數組以後其變量名依然是一個數組。

# awk 'BEGIN{arr["name"];print("name" in arr);delete arr["name"];print("name" in arr)}'
1
0

數組的遍歷

awk數組的遍歷和bash中數組的遍歷相似,都有“for i in arr ...”這類方式來遍歷數組。我們先不討論這種方式的遍歷。

我們先來看一個示例,假設關聯數組的索引是數值(最終還是會轉換成字符串就是了)。

# awk 'BEGIN{arr[1]=10;arr[2]=20;arr[3]=30;arr[4]=40;for(i=1;i<=length(arr);i++){print i"-->"arr[i]}}'
1-->10
2-->20
3-->30
4-->40

當數值是連續的時候,這麼做沒有問題。但是如果數值不連續呢?

# awk 'BEGIN{arr[1]=10;arr[2]=20;arr[3]=30;arr[8]=80;for(i=1;i<=length(arr);i++){print i"-->"arr[i]}}'
1-->10
2-->20
3-->30
4-->
5-->
6-->
7-->
8-->80

造成這種結果的原因是因爲,在for循環的前三次循環中length(arr)的結果爲4,但是在第4次的時候,我們嘗試輸出:

4-->arr[4]

雖然沒有arr[4]這個元素,但是這次對其的引用反而創建了該元素,只不過該元素的值爲空。創建了該元素以後,下一輪循環的length(arr)的結果就變成了5。以此類推,最終導致了這個結果。

好在arr[3]和arr[8]的跨度不是很大,如果跨度較大的話就會創建許多空元素(性能下降)。由於awk支持的是關聯數組,如果“最後”一個元素的索引是字符串的話,就會出現死循環的情況。

awk 'BEGIN{arr[1]=10;arr[2]=20;arr[3]=30;arr["name"]="alongdidi";for(i=1;i<=length(arr);i++){print i"-->"arr[i]}}'

我們可以在防止awk創建空元素,可以事先判斷元素的純在性。

# awk 'BEGIN{arr[1]=10;arr[2]=20;arr[3]=30;arr[8]=80;for(i=1;i<=length(arr);i++){if(i in arr){print i"-->"arr[i]}}}'
1-->10
2-->20
3-->30

但是這樣子的話,在上面那個例子中我們又少輸出了arr[8],這樣算遍歷失敗了。

不過awk有其專門用於遍歷數組的方式,這個方式和bash遍歷數組的方式類似。

for(idx in arr) {
    print idx"-->"arr[idx]
}

這裏的idx就是一個用來存儲關聯數組索引的變量名稱了,因此不要使用雙引號包裹。

# awk 'BEGIN{arr[1]=10;arr[2]=20;arr[3]=30;arr[8]=80;for(i in arr){print i"-->"arr[i]}}'
1-->10
2-->20
3-->30
8-->80
# awk 'BEGIN{arr["name"]="alongdidi";arr["country"]="china";arr["age"]=29;arr["gender"]="male";for(i in arr){print i"-->"arr[i]}}'
age-->29
country-->china
name-->alongdidi
gender-->male

注意:遍歷順序與我們認爲理解的順序不同,前面已經說過。

遍歷的順序

默認情況下數組的遍歷順序在認爲看來是無序的,無法預測的。但是我們可以使用預定義變量PROCINFO["sorted_in"]來指定遍歷的順序。

這個值可以是用戶自定義的函數,也可以是awk預定義的排序規則。根據自定義函數的規則定義數組遍歷順序等學習了自定義函數以後再補充。

默認值爲@unsorted,表示無序,字符@是固定字符。其餘的值的構成如下。

@x_y_z

x:指定數組是要基於索引(ind)還是值(val)來排序。

y:指定比較的方式。按字符串(str)比較,按數值(num)比較和按類型(type)比較。如果是按照類型,在升序的情況下,數值-->字符串-->數組。

z:指定升序(asc)或者降序(desc)。

@unsorted:無序。
@ind_str_asc:基於索引按字符串比較方式升序排序。
@ind_str_desc:基於索引按字符串比較方式降序排序。
@ind_num_asc:基於索引按數值比較方式升序排序。無法轉換成數值的一律視作數值0。
@ind_num_desc:基於索引按數值比較方式降序排序。無法轉換成數值的一律視作數值0。
@val_type_asc:基於元素按照數據類型比較方式升序排序。
@val_type_desc:基於元素按照數據類型比較方式降序排序。
@val_str_asc:基於元素按照字符串比較方式升序排序。
@val_str_desc:基於元素按照字符串比較方式降序排序。
@val_num_asc:基於元素按照數值比較方式升序排序。
@val_num_desc:基於元素按照數值比較方式降序排序。

示例如下。

# cat sortArray.awk 
BEGIN{
    PROCINFO["sorted_in"]="@ind_num_desc"
    arr[1]="one"
    arr[2]="two"
    arr[3]="three"
    arr["a"]="aa"
    arr["b"]="bb"
    arr[10]="ten"
    for(idx in arr){
        print idx"-->"arr[idx]
    }
}
# awk -f sortArray.awk
10-->ten
3-->three
2-->two
1-->one
b-->bb
a-->aa

多維數組

數值索引數組只能保存一份有效數據信息,即元素的值,而索引的值一般是無含義的非正整數。

關聯數組能保存兩份有效數據信息,即索引和元素的值。

當兩份有效信息不夠用的時候我們會考慮將多餘的信息存入索引或者元素值中。如果存入元素值中我們需要做額外的元素值分割操作。而存儲在索引值中的話,只需要通過多維數組就可以實現。

使用方式爲

arr[x,y]

數組索引分成了x和y實現了多保存一份信息的需求。雖然我們在書寫時使用逗號分隔了x和y,但是在awk內部會使用預定義變量SUBSEP來連接x和y。

假如我們將SUBSEP的值設置爲@符號,那麼我們可以直接使用arr["x@y"]的方式來引用數組元素。

# awk 'BEGIN{SUBSEP="@";arr["x","y"]="alongdidi";print arr["x","y"];print arr["x@y"]}'
alongdidi
alongdidi

預定義變量的默認值是“\034”,它是一個不可打印的字符。我們只需要知道awk多維數組的這個特性即可,在具體使用的時候,我們依然使用逗號來分隔即可,也沒必要去修改這個值。

多維數組的使用上主要是索引和一維數組有所區別,其他均是一樣的。

arr[x]
arr[x,y]
if(x in arr)
if(x,y in arr)

接下來我們看一個多維數組的使用示例,假設我們有一個數據如下:

# cat d.txt
1 2 3 4 5 6
2 3 4 5 6 1
3 4 5 6 1 2
4 5 6 1 2 3

我們期望把它順時針旋轉90°。

4 3 2 1
5 4 3 2
6 5 4 3
1 6 5 4
2 1 6 5
3 2 1 6

Talk is simple, show me the code!

# cat multiDimensionalArray.awk 
{
    for(i=1;i<=NF;i++){
        arr[NR,i]=$i
    }
}
END{
    for(j=1;j<=NF;j++){
        for(i=NR;i>=1;i--){
            printf "%d ",arr[i,j]
        }
        printf "\n"
    }
}
# awk -f multiDimensionalArray.awk d.txt 
4 3 2 1 
5 4 3 2 
6 5 4 3 
1 6 5 4 
2 1 6 5 
3 2 1 6

嵌套數組

嵌套數組就是數組中的數組(Arrays of Arrays),這塊作者在視頻中也沒有講解,暫時跳過,應該是比較複雜的內容,日常使用估計也比較少。上面的多維數組估計就已經很少用到了。

 

數組實戰

去除重複行

首先看示例文件x.log的內容。

# cat x.log 
abc    # 3個
def
ghi   # 2個
abc
ghi
xyz
    # 空行2個
mnopq
abc

思路一:在main中將$0保存至關聯數組索引中,然後最後在END中遍歷數組的索引。

# cat quchong.awk 
{
    arr[$0]
}
END{
    for(i in arr){
        print i
    }
}
# awk -f quchong.awk x.log 

def
mnopq
abc
ghi
xyz

缺點:無法保證輸出的順序和數據在原文件中出現的數據相同。

思路二:爲了保證數據的順序,當我們遇到$0時就判斷其是否是數組的索引。如果是則什麼也不做,否則我們就將其輸出並且加入數組的索引中。

# awk '{if(!($0 in arr)){print $0;arr[$0]}}' x.log 
abc
def
ghi
xyz

mnopq

統計行出現次數

如果引用一個新的數組元素等於創建該數組元素並賦空值,如果對這個值進行自增操作,那麼相當於對0進行自增操作,結果爲1。

# awk 'BEGIN{arr[1];print arr[1]}'

# awk 'BEGIN{arr[1]++;print arr[1]}'
1
# awk 'BEGIN{++arr[1];print arr[1]}'
1

基於這兩個特性我們就可以進行統計操作了。

# awk '{arr[$0]++}END{for(i in arr){print i" count is:"arr[i]}}' x.log
 count is:2    # 注意這個是空行。
def count is:1
mnopq count is:1
abc count is:3
ghi count is:2
xyz count is:1

統計單詞出現次數

可以統計行出現的次數,那麼就可以統計單詞出現的次數。只不過此前數組的索引保存的是行數據,更換成了單詞數據。

我們改寫一下x.log,新增幾個單詞。

abc
def
ghi def def def
abc
ghi abc
xyz
 abc
mnopq
abc
 mnopq
# awk '{for(i=1;i<=NF;i++){arr[$i]++}}END{for(idx in arr){print idx":"arr[idx]}}' x.log 
def:4
mnopq:2
abc:5
ghi:2
xyz:1

統計TCP連接狀態數量

netstat -tanp:用來顯示網絡連接的信息,如果信息不夠多的話可以自己重複幾個SSH連接。

# netstat -tanp
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 127.0.0.1:6012          0.0.0.0:*               LISTEN      9238/sshd: root@pts 
tcp        0      0 0.0.0.0:111             0.0.0.0:*               LISTEN      715/rpcbind         
tcp        0      0 192.168.122.1:53        0.0.0.0:*               LISTEN      1455/dnsmasq        
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      1143/sshd           
tcp        0      0 127.0.0.1:631           0.0.0.0:*               LISTEN      1136/cupsd          
tcp        0      0 127.0.0.1:25            0.0.0.0:*               LISTEN      1369/master         
tcp        0      0 127.0.0.1:6010          0.0.0.0:*               LISTEN      1797/sshd: root@pts 
tcp        0      0 127.0.0.1:6011          0.0.0.0:*               LISTEN      9192/sshd: root@pts 
tcp        0      0 192.168.152.100:22      192.168.152.1:53246     ESTABLISHED 9192/sshd: root@pts 
tcp        0     52 192.168.152.100:22      192.168.152.1:56142     ESTABLISHED 1797/sshd: root@pts 
tcp        0      0 192.168.152.100:22      192.168.152.1:53247     ESTABLISHED 9238/sshd: root@pts 
tcp6       0      0 ::1:6012                :::*                    LISTEN      9238/sshd: root@pts 
tcp6       0      0 :::111                  :::*                    LISTEN      715/rpcbind         
tcp6       0      0 :::22                   :::*                    LISTEN      1143/sshd           
tcp6       0      0 ::1:631                 :::*                    LISTEN      1136/cupsd          
tcp6       0      0 ::1:25                  :::*                    LISTEN      1369/master         
tcp6       0      0 ::1:6010                :::*                    LISTEN      1797/sshd: root@pts 
tcp6       0      0 ::1:6011                :::*                    LISTEN      9192/sshd: root@pts

一般我們要讓連接數高的排序靠前,所以涉及到遍歷數組的排序問題。

# netstat -tanp | awk '/^tcp/{arr[$6]++}END{PROCINFO["sorted_in"]="@val_num_desc";for(i in arr){print i":"arr[i]}}'
LISTEN:15
ESTABLISHED:3

根據字段取最大值

假設有這麼一個文件:

# cat version.txt 
file 10
dir 10
file 20
dir 20
file 10
dir 10
file 300
dir 999
file 30
dir 99

我們期望輸出:

file 300
dir 999

要確保file和dir後面的數字是最大的。

# awk 'arr[$1]<$2{arr[$1]=$2}END{for(i in arr){print i,arr[i]}}' version.txt 
dir 999
file 300

 

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