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