讀取文件
讀取“每行”數據
博客的開頭我們說過,默認情況下awk讀取文件的每行數據並將其存入$0變量當中。其實,awk在讀取數據之前會根據其內部的預定義變量RS的值來分隔每條記錄(record)。RS的默認值是“\n”,即換行符,因此也就會有我們剛才所說的默認情況。
所以,awk在讀取文件時,會根據其自定義變量RS(Record Separator,記錄分隔符)的值將文件分為多條記錄來循環讀取,每讀取一條記錄就將其賦值給$0變量,賦值完畢后再執行main代碼塊。
如果文件是一個空文件,那么就讀取不到記錄也就不會執行main代碼塊。
[root@c7-server ~]# touch x.log
[root@c7-server ~]# awk '{print "hello world"}' x.log [root@c7-server ~]#
可以在BEGIN代碼塊中設置RS的值來改變awk分隔記錄的方式。
[root@c7-server ~]# awk 'BEGIN{RS="com"}{print "---";print $0;print "---"}' a.txt
--- ID name gender age email phone 1 Bob male 28 abc@qq. --- --- 18023394012 2 Alice female 24 def@gmail. --- ... ...
被分隔的每條記錄中不會包含RS的值本身,在上述示例中即每條記錄不會包含com字符串。
細心的朋友會留意到
~]# awk '{print $0}' a.txt
當我們不修改RS,上述指令在輸出的時候會使得每條記錄之間自動換行,看起來就好像輸出數據包含了換行符(RS的默認值)。這個特點留到我們講解另一個預定義變量ORS的時候再做解釋。
那么為什么一般將RS設置在BEGIN代碼塊當中呢?
首先,一般來說一個文件的RS在我們使用awk處理文件之前就可以確定了,而且一般不會改動。其次,基於我們截止目前為止介紹的awk特性,如果我們在main代碼塊中設置RS的話,awk讀取第一條記錄的時候依然會使用換行符(RS的默認值)來作為分隔符,讀取完第一條記錄接下來才是第一次進入main代碼塊,然后才是設置RS的值,而且每次awk內部循環執行main時都要為RS賦相同值也沒有必要(性能略微損失)。
RS為單個字符時直接使用該字符作為分隔符;RS為多個字符時被awk識別為正則表達式。
RS的特殊值
如果記錄分隔符不存在於讀入數據中的話,那么我們便可以在一次內部循環的情況下讀取出所有的數據。
RS="\0"和RS="^$":這兩種方式均可以一次性讀取所有數據,區別在於部分文件可能包含\0字符。雖然在正則中^$表示空行,但是在文件中即使包含空行也不會將其作為RS。
[root@c7-server ~]# awk 'BEGIN{RS="\0"} {print "---";print $0;print "---"}' a.txt
--- ID name gender age email phone ... ... 10 Bruce female 27 bcbd@139.com 13942943905 --- [root@c7-server ~]# awk 'BEGIN{RS="^$"} {print "---";print $0;print "---"}' a.txt --- ID name gender age email phone ... ... 10 Bruce female 27 bcbd@139.com 13942943905 ---
RS="":按段落讀取。當段落與段落之間均為空行的時候,按照段落作為分隔符。注意,空格和制表符雖然也是空白看不到的字符,但是不算空行而算空白符。網友們可以自行在a.txt中鍵入幾個空行查看效果。
~]# awk 'BEGIN{RS=""} {print "---";print $0;print "---"}' a.txt
RS="\n+":以至少一個換行符作為分隔符。默認情況下每行數據都視為1條記錄,而該情況下可以將多個連續的換行符作為分隔符,使得空行不會被視為記錄。
當我們使用正則作為分隔符的時候,分隔符可以有多種情況。每次awk遇到滿足正則條件的分隔符時,都會將這次分隔符賦值給RT(Record Termination),我們可以通過查看該值來判斷到底這條記錄是以什么作為分隔符。
~]# awk 'BEGIN{RS="@[[:alnum:]]{1,5}.com"} {print "---"RT"---"}' a.txt
---@qq.com---
---@gmail.com---
---@163.com---
---@189.com---
---@xyz.com---
---@139.com---
---@189.com---
---@qq.com---
---@sohu.com---
---@139.com---
------
最后一個分隔符比較特殊。我猜測可能是因為已經EOF了,就將EOF視為分隔符,雖然它並不滿足於正則條件。
在我們使用正則進行匹配的時候如果想要忽略大小寫,可以使用預定義變量IGNORECASE。
~]# awk 'BEGIN{IGNORECASE=1} /alice|bob/{print $0}' a.txt
1 Bob male 28 abc@qq.com 18023394012
2 Alice female 24 def@gmail.com 18084925203
記錄號
記錄號即“行號”,awk使用NF和FNR兩個預定義變量來保存記錄號,每讀取1條記錄,它們的值就會加1。第一條記錄號的值就是1,以此開始遞增。
~]# awk '{print NR,FNR}' a.txt a.txt
NR會一直遞增,即使數據的來源屬於不同的文件,而FNR在遇到新的文件的時候其值會重回1開始遞增。
至此我們了解到,awk每讀取1條記錄就會設置$0、NR、FNR和RT的值。
讀取每字段數據
awk讀取記錄以后,還會根據預定義變量FS(Field Separator,字段分隔符)將記錄划分成多個字段。其值默認是一個空格(FS=" "),表示將一個至多個空白字符(空格、制表符和換行符)識別為字段分隔符。將第一個字段賦值給$1,第二個字段賦值給$2,依次類推直至將最后一個字段賦值給$NF。預定義變量NF表示這條記錄的字段數量。大家可以自己試試。
awk '{print $1}' a.txt
awk '{print $6}' a.txt awk '{print $NF}' a.txt
引用的字段如果超出最大字段數則反饋空字符串,如果是負數則報錯。
[root@c7-server ~]# awk '{print $7}' a.txt
... ...
[root@c7-server ~]# awk '{print $-1}' a.txt awk: cmd. line:1: (FILENAME=a.txt FNR=1) fatal: attempt to access field -1
根據分隔符划分字段
通過預定義變量FS和選項-F可以用來指定字段分隔符。選項-F和預定義變量FS大同小異,只不過指定的位置不同罷了。
awk 'BEGIN{FS=":"}{print $1}' /etc/passwd awk -F ":" '{print $1}' /etc/passwd
字段分隔符的特性大多數在上面介紹FS時已經介紹過,補充幾點。
如果FS的值為空字符串"",那么會將記錄中的每個字符都識別為字段。空格字符也是字符。
~]# echo "a c" | awk 'BEGIN{FS=""}{print $1;print $2;print $3}'
a
c
如果在記錄中無法找到字段分隔符則將整個記錄($0)賦值給第一個字段$1。
~]# awk 'BEGIN{FS="_"}{print $1;print $2}' a.txt
根據字段寬度划分字段
根據分隔符划分字段的前提條件是文件有合適的分隔符便於我們划分字段。我們copy一份a.txt至b.txt,並且修改某幾行的某幾個字段,使用等量的空格符來替換。
[root@c7-server ~]# cat b.txt
ID name gender age email phone
1 Bob male 28 abc@qq.com 18023394012
2 female 24 def@gmail.com 18084925203
3 Tony male 21 aaa@163.com 17048792503
4 Kevin 21 bbb@189.com 17023929033
5 Alex male 18 ccc@xyz.com 18185904230
6 Andy female ddd@139.com 18923902352
7 Jerry female 25 18785234906
8 Peter male 20 bax@qq.com 17729348758
9 Steven female 23 bc@sohu.com 10 Bruce female 27 bcbd@139.com 13942943905
此時再以字段分隔符的方式來為b.txt划分字段就不合適了。
此時我們通過觀察發現:
- 缺失的字段使用了等量的空格字符填充;
- 為每個字段設定一個最大字符數之后(第一個字段最大字符數是2,第二個字段最大字符數是6,以此類推),字段間距是可知的(字段間距剛好都是2個字符,即使不同也沒事,主要是可知的),每條記錄同字段間的間距相同(例如每條記錄的第一個和第二個字段的間距相同)。
此時我們即可使用預定義變量FIELDFIDTHS來根據字段的字符寬度划分字段。
~]# echo "abbcccddd" | awk 'BEGIN{FIELDWIDTHS="1 2 3 4"}{print $1;print $2;print $3;print $4}'
支持跳躍字符指定字段寬度。
~]# echo "a bb ccc ddd" | awk 'BEGIN{FIELDWIDTHS="1 1:2 2:3 3:4"}{print $1;print $2;print $3;print $4}'
支持通配符*匹配剩余所有字符。
~]# echo "abbcccddd" | awk 'BEGIN{FIELDWIDTHS="1 2 3 *"}{print $1;print $2;print $3;print $NF}' ~]# echo "a bb ccc ddd" | awk 'BEGIN{FIELDWIDTHS="1 1:2 2:3 3:*"}{print $1;print $2;print $3;print $NF}'
因此我們可以使用FIELDWIDTHS來處理b.txt了。注意觀察結果(結果沒有放入博文,請網友自行敲看看)。
~]# awk 'BEGIN{FIELDWIDTHS="2 2:6 2:6 2:3 2:13 2:*"} $1==2||$1==4||$1==6||$1==7||$1==9{print "----";print $1;print $2;print $3;print $4;print $5;print $6;print "----"}' b.txt
根據模式划分字段
預定義變量FPAT的值是一個正則表達式,awk根據這個值去匹配$0,第一次匹配成功賦值給$1,以此類推直到匹配完整個$0。不會修改$0。
FPAT適用於當我們打算使用分隔符取字段時,字段值包含了分隔符的情況。例如如下csv文件。
~]# cat FPAT.csv
Robbins,Arnold,"1234 A Pretty Street, NE",MyTown,MyState,12345-6789,USA
此時我們使用逗號作為分隔符取出的結果就不是我們想要的,因為第三個字段包含了分隔符。另外,這里我們使用了for循環遍歷了所有字段,第一次見此用法的網友照着敲直到功能即可,后面會講解for循環的。
~]# awk 'BEGIN{FS=","}{for(i=1;i<=NF;i++){print $i}}' FPAT.csv
Robbins
Arnold
"1234 A Pretty Street
NE"
MyTown MyState 12345-6789 USA
這時可以采取FPAT。正則的第一部分指明“分隔符”逗號以外的多個字符識別為字段;正則的第二部分指明當遇到兩個雙引號(在awk中需要使用轉義字符表示雙引號\")的時候,將其與其中包裹的任意字符識別為字段。這樣就可以正確分隔這個示例的字段了。
~]# awk 'BEGIN{FPAT="[^,]+|\".*\""}{for(i=1;i<=NF;i++){print $i}}' FPAT.csv
Robbins
Arnold
"1234 A Pretty Street, NE" MyTown MyState 12345-6789 USA
由於正則的貪婪匹配機制,如果記錄中包含2個以上的雙引號就會出問題。
[root@c7-server ~]# cat FPAT.csv
Robbins,Arnold,"1234 A Pretty Street, NE",MyTown,MyState,"12345-6789",USA [root@c7-server ~]# awk 'BEGIN{FPAT="[^,]+|\".*\""}{for(i=1;i<=NF;i++){print $i}}' FPAT.csv Robbins Arnold "1234 A Pretty Street, NE",MyTown,MyState,"12345-6789" USA
結果並不是我們想要的,此時需要改寫正則。
~]# awk 'BEGIN{FPAT="[^,]+|\"[^\"]+\""}{for(i=1;i<=NF;i++){print $i}}' FPAT.csv
Robbins
Arnold
"1234 A Pretty Street, NE" MyTown MyState "12345-6789" USA
patsplit()函數的功能與FPAT預定義變量的功能相同。
至此我們學習了3種划分字段的方式:
- 根據字段分隔符(預定義變量FS和選項-F)划分字段;
- 根據字段的寬度(預定義變量FIELDWIDTHS)划分字段;
- 根據字段的模式(預定義變量FPAT)划分字段。
這三種方式只能選擇一種,它們相互之間是沖突的。
檢查字段分隔的方式
數組變量PROCINFO["FS"]存儲了字段分隔的三種方式,其值分別是FS、FIELDWIDTHS和FPAT。
~]# cat test.awk
BEGIN {
if(PROCINFO["FS"]=="FS"){ print "FS" } else if(PROCINFO["FS"]=="FPAT") { print "FPAT" } else { print "FIELDWIDTHS" } } ~]# awk -f test.awk ~]# awk -v FS=":" -f test.awk ~]# awk -v FIELDWIDTHS="3" -f test.awk ~]# awk -v FPAT="[[:alpha:]]+" -f test.awk
字段與記錄的重建
預定義變量FS的含義我們已經很了解。有一個十分類似的預定義變量叫做OFS(Output FS),它表示當$0(記錄)重新計算(可以理解為重建)的時候使用OFS的值作為輸出字段的分隔符。接下來我們來看幾個重新計算的情況。
1、當修改$0的時候,將使用FS(假定我們就使用FS不使用其他划分字段的方式)重新計算各個字段以及NF值。
awk 'BEGIN{FS=":"}{$0="a:b:c";print NF;for(i=1;i<=NF;i++){print $i}}' a.txt
2、當修改具體的字段的時候,使用OFS重建記錄。注意,哪怕是自我賦值也屬於字段的修改。
awk 'BEGIN{OFS="-"}{$1=0;print $0}' a.txt
awk 'BEGIN{OFS="-"}{$1=$1;print $0}' a.txt
3、為不存在的字段賦值,將新增字段並為不存在的字段(若有)賦空字符串,使用OFS重建記錄。
awk 'BEGIN{OFS="-"}{$(NF+3)=5;print $0}' a.txt
4、增加NF,使用空字符串為新記錄賦值;減少NF,截斷多余記錄。均會使用OFS重建記錄。
# awk 'BEGIN{OFS="-"}{NF+=3;print $0}' a.txt
# awk 'BEGIN{OFS="-"}{NF-=3;print $0}' a.txt
awk讀取記錄以后將數據原原本本存放於$0當中,只要不會發生上述使用OFS重建記錄的事情,即便指定了OFS也無妨。
# awk 'BEGIN{OFS="-"}{print $0}' a.txt
OFS的默認值是1個空格。因此即便沒指定具體的值也會使用單個空格重建記錄。
awk '{$1=$1;print $0}' a.txt
一般我們會先設置OFS的值再重建記錄。所以將其放入BEGIN中。如果先重建再設置OFS,那么第一行會按照默認OFS重建,后續行才按照新OFS值重建。
awk '{$1=$1;OFS="-";print $0}' a.txt
awk '{$1=$1;OFS="-";$1=$1;print $0}' a.txt awk 'BEGIN{OFS="-"}{$1=$1;print $0}' a.txt awk '{$1=$1;print $0}' OFS="-" a.txt
這里如果看不懂的朋友,等后面學習了awk工作流程和變量以后就會明白awk執行的順序了。
根據這個特性我們可以壓縮連續的多個空格。
# echo " a b c d " | awk '{$1=$1;print $0}'
# echo " a b c d " | awk 'BEGIN{OFS="-"}{$1=$1;print $0}'
數據篩選
記錄篩選
1、根據行號(NR或者FNR)篩選記錄。
awk 'NR==2{print $0}' a.txt
awk 'NR>2{print}' a.txt awk 'NR<2' a.txt awk 'NR>=2' a.txt awk 'NR<=2' a.txt
此前已經說過,省略{action}即表示{print}等價於{print $0}。
2、根據正則表達式篩選記錄。
正則匹配,默認使用$0來匹配,可以省略$0。
awk '/qq.com/' a.txt
awk '$0~/qq.com/' a.txt
匹配不包含@的記錄,即整條記錄均由非@字符構成。
awk '/^[^@]+$/' a.txt
awk支持取反,使用取反更易理解。
awk '!/@/' a.txt
3、根據字段篩選記錄。
# awk '$4>24' a.txt
ID name gender age email phone
1 Bob male 28 abc@qq.com 18023394012
7 Jerry female 25 exdsa@189.com 18785234906
10 Bruce female 27 bcbd@139.com 13942943905
第一條記錄的$4是age,age是字符串,24是數字,其在進行比較時會有內部轉換機制,將24識別為字符串,字符串比較根據ASCII編碼(maybe)按字符一一比較,字符a大於字符2。如果我們期望不篩選出age那條,可以將其+0從而轉換成數字。字符串+0等於數字0。
# awk '($4+0)>24' a.txt
1 Bob male 28 abc@qq.com 18023394012
7 Jerry female 25 exdsa@189.com 18785234906
10 Bruce female 27 bcbd@139.com 13942943905
awk '$5~/qq.com/' a.txt
4、組合篩選。
使用邏輯與和邏輯或運算符組合多個條件。
awk 'NR>=2&&NR<=4' a.txt
awk '($4+0)>=20||$3=="male"' a.txt
5、按照范圍篩選(flip-flop)。
awk 'NR==2,NR==4' a.txt
awk '$2=="Kevin",$5~/qq.com/' a.txt
字段處理
字段的篩選即print $X(X表示具體的字段)沒什么好說的,因此講字段的處理。
# awk 'NR>1{$4+=4;print $0}' a.txt
1 Bob male 32 abc@qq.com 18023394012
2 Alice female 28 def@gmail.com 18084925203 ... ...
處理字段目前只接觸到賦值,修改了字段值會導致使用OFS重建$0。
要想使得輸出結果恢復重建前的效果,可以結合外部命令,例如該示例中的column。
# awk 'NR>1{$4+=4;print $0}' a.txt | column -t
1 Bob male 32 abc@qq.com 18023394012
2 Alice female 28 def@gmail.com 18084925203 ... ...
或者在后續學會了字符串處理函數以后來實現。基本思路是取得$0重建前的$4的前后部分保留,然后修改$4的值,最后再將三部分組合。
awk 'NR>1{$6=$6"*";print $0}' a.txt
awk 'NR>1{$6=$6"*";print $0}' a.txt | column -t
數據篩選示例
該示例要求我們從ifconfig的輸出結果中取得ipv4地址(不包含環回地址lo),該示例同時也是常見的運維面試題。
# ifconfig
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 192.168.152.100 netmask 255.255.255.0 broadcast 192.168.152.255 inet6 fe80::7a4:5a06:46b4:9ce5 prefixlen 64 scopeid 0x20<link> ether 00:0c:29:46:79:46 txqueuelen 1000 (Ethernet) RX packets 3151 bytes 258273 (252.2 KiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 1606 bytes 166414 (162.5 KiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536 inet 127.0.0.1 netmask 255.0.0.0 inet6 ::1 prefixlen 128 scopeid 0x10<host> loop txqueuelen 1000 (Local Loopback) RX packets 72 bytes 8088 (7.8 KiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 72 bytes 8088 (7.8 KiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 virbr0: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500 inet 192.168.122.1 netmask 255.255.255.0 broadcast 192.168.122.255 ether 52:54:00:a6:3d:cf txqueuelen 1000 (Ethernet) RX packets 0 bytes 0 (0.0 B) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 0 bytes 0 (0.0 B) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
有3種思路來取得地址。
思路一:ipv4地址位於包含“inet ”(注意有空格)的記錄,因此篩選出該記錄。同時我們要過濾掉換回地址,因此$2不以127打頭的記錄。將2個條件使用邏輯與連接。
# ifconfig | awk '/inet /&&!($2~/^127/)'
inet 192.168.152.100 netmask 255.255.255.0 broadcast 192.168.152.255 inet 192.168.122.1 netmask 255.255.255.0 broadcast 192.168.122.255
思路二:ifconfig輸出信息中包含了3段信息,每段表示1張網卡並以空行作為記錄分隔符。因此結合我們在講解RS時提到的,這里我們以段划分記錄。記錄不包含lo,同時我們取得ip地址所在的字段(手工數一下可知是第6字段)。
# ifconfig | awk 'BEGIN{RS=""}!/lo/{print $6}' 192.168.152.100 192.168.122.1
思路三:基於思路二,假設ip地址所在的字段數比較靠后,那么我們就需要數好幾個字段才可以數到ipv4地址,我們來看一下下面這個輸出結果。
# ifconfig | awk 'BEGIN{RS=""}!/lo/{print "---"$0"---"}'
---ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.152.100 netmask 255.255.255.0 broadcast 192.168.152.255 inet6 fe80::7a4:5a06:46b4:9ce5 prefixlen 64 scopeid 0x20<link> ether 00:0c:29:46:79:46 txqueuelen 1000 (Ethernet) RX packets 3997 bytes 330636 (322.8 KiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 2063 bytes 225016 (219.7 KiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0--- ---virbr0: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500 inet 192.168.122.1 netmask 255.255.255.0 broadcast 192.168.122.255 ether 52:54:00:a6:3d:cf txqueuelen 1000 (Ethernet) RX packets 0 bytes 0 (0.0 B) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 0 bytes 0 (0.0 B) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0---
按段划分並篩選記錄以后,ipv4地址,從我們的視覺上來看,可以理解為每條記錄的第2“行”。不過我們這里因為已經將整個網卡的信息(多行)理解為了1條記錄(行)了,因此我們要將原本的第二行識別為第2個字段,即修改FS的值。
# ifconfig | awk 'BEGIN{RS=""}!/lo/{FS="\n";print $2}'
flags=4163<UP,BROADCAST,RUNNING,MULTICAST> inet 192.168.122.1 netmask 255.255.255.0 broadcast 192.168.122.255
輸出結果中第一行並不是我們所期望的字段信息。這是因為根據RS=""讀取記錄以后會賦值$0,並按照FS的值(沒有另外指定因此使用FS=" ")賦值$1...$NF各個位置參數。賦值完畢以后才執行main代碼塊賦值FS="\n",此時第一條記錄的各位置參數已經確定好了。因此從第二條記錄開始,$2才是我們所想要的信息。
我們只要將FS設置在BEGIN中即可,這也是為什么大多數情況下如果要修改默認的FS和RS都在BEGIN中設置。
# ifconfig | awk 'BEGIN{RS="";FS="\n"}!/lo/{print $2}'
inet 192.168.152.100 netmask 255.255.255.0 broadcast 192.168.152.255 inet 192.168.122.1 netmask 255.255.255.0 broadcast 192.168.122.255
接下來我們將每條記錄的$2賦值給記錄$0本身,設置FS並取第2個字段的信息。按照下面的命令明顯無法取正確。
ifconfig | awk 'BEGIN{RS="";FS="\n"}!/lo/{$0=$2;FS=" ";print $2}'
# 第一條空信息
# 第二條空信息,注意這兩條空信息所取的字段是不同的。
原因此前我們也說了,修改$0($0=$2)會重新划分各字段,而FS=" "在修改$0之后出現,因此第一條記錄依然是按照FS="\n"划分字段。
【第一條空信息】的$0是:inet 192.168.152.100 netmask 255.255.255.0 broadcast 192.168.152.255,根據FS,因此它也會是$1,因此$2為空。
想讓【第一條空信息】取值正確的話,就要重新設置$0。
# ifconfig | awk 'BEGIN{RS="";FS="\n"}!/lo/{$0=$2;FS=" ";$0=$0;print $2}'
192.168.152.100
# 第二條空信息
【第二條空信息】取值錯誤的原因是從第二條記錄開始,FS的值就一直是main中的" ",我們需要在main的結尾再將其設置回BEGIN中的值。
# ifconfig | awk 'BEGIN{RS="";FS="\n"}!/lo/{$0=$2;FS=" ";$0=$0;print $2;FS="\n"}' 192.168.152.100 192.168.122.1
在我們實際使用當中使用思路一和思路二取ipv4地址即可,思路三只是利於我們理解awk的工作原理,看不懂的同學多看看上面的【字段與記錄的重建】。