由於項目經歷原因,經常使用awk處理一些文本數據。甚至,我特意下載了一個windows上的awk:gawk.exe,這樣在windows上也能享受awk處理數據的方便性,。
俗話說,“常在河邊走,哪能不濕鞋”,使用awk過程中碰上過不少坑,這里稍總結一下,希望對大家有幫助。
1 FS問題
看看這兩個awk腳本:
cat demo_1.txt demo_2.txt 1|2|3|4| 1|@|2|@|3|@|4|@| awk -F '|' '{print $2}' demo_1.txt; # 腳本1 awk -F '|@|' '{print $2}' demo_2.txt; # 腳本2
腳本原目的是達到的目的是分別按'|'和分隔'|@|',輸出demo.txt第二列。但實際上,第一個腳本這樣寫沒錯,但第二個腳本卻是錯的。
為什么呢?
因為豎線在正則表達式中是一個特殊字符,表示匹配豎線左右的字符組之一。如果想使用豎線本身,需要對用轉義符。
但為什么第一個命令也一樣使用了豎線卻沒有問題呢?
這就涉及到awk在一個規定:
如果FS設置了不止一個字符作為字段分隔符,將作為一個正則表達式來解釋,否則直接按該字符做為分隔符對每行進行分割。
所以第一個命令使用了豎線做分隔符沒問題,第二個命令就出錯了。
2 正則表達式與反斜杠號問題
繼續上面的問題討論,如果demo.txt是按"|@|"做為分隔符的,要輸出demo.txt第二列,正確的答案應該是怎么寫呢?
答案是:
awk -F '\\|@\\|' '{print $2}' demo.txt;
注意這里,FS的值是'\\|@\\|',而不是簡單的'\|@\|'(這樣寫會報錯,提示:awk: 警告: 轉義序列“\|”被當作單純的“|”)。為啥要這樣寫呢?
先來看一個試驗:
echo|awk -F '|@|' '{print FS}' # 腳本1 echo|awk -F '\|@\|' '{print FS}' # 腳本2 echo|awk -F '\\|@\\|' '{print FS}' # 腳本3
可以看到第一和第二個腳本,FS值是一樣的。原因是awk先要解析用戶輸入的字符串,並將解析結果賦值給FS,然后再調用split類函數,把FS當成函數參數傳進去。
而split需要再對FS進行一次解析,編譯成正則表達式。awk解析字符串給FS變量賦值時會把'\|'認為是'|',從而導致傳進split函數時,分隔符已。
因此,如果想讓awk正確分割記錄,需要使FS='\\|@\\|',這時awk會把\\解析成轉義字符'\',這樣豎線就能被當普通字符處理國。
3 關聯數組訪問問題
曾經碰上過這樣一個場景:文件a.txt包含少量用戶余額(userid|amt),約100行記錄,文件b.txt包含了所有用戶的余額(userid|amt),約有100萬行記錄。
現在要求關連a.txt和b.txt(使用userid),找出在a.txt與b.txt都存在的userid,並輸出其中b.amt大於a.amt的記錄。
當時我先寫了以下腳本:
awk -F '|' 'BEGIN{ while(getline < "a.txt") { v_user_map[$1] = $2; } } { v_amt_a = v_user_map[$1]; if ((v_amt_a != "") && v_amt_a < $2) print $0; }
看起來邏輯似乎沒有問題,於是開始跑。但是跑起來發現效率遠比自己想象的低,而且發現程序運行過程消耗的內存越來越多。
這明顯是有問題的,理論上應該是BEGIN那段語句會消耗一些內存,之后應該就不需要再消耗才對。
由於寫過c++代碼,里面也有類似關聯數組的數據結構,我很快猜測並實驗證明原因:v_amt_a = v_user_map[$1]; 這一句。
雖然這里沒有給v_user_map[$1]賦值,但是awk會默認賦值為空,導致v_user_map數組元素越來越多,占用內存空間越來越大,查找效率越來越低。
知道問題就好解決了,查了一下awk幫助手冊,發現可以這樣寫:
awk -F '|' 'BEGIN{ while(getline < "a.txt") { v_user_map[$1] = $2; } } { if ($1 in v_user_map) { if (v_user_map[$1] < $2) print $0; } }
使用in操作符來判斷元素是否在關聯數組里面,這樣就不會有默認賦值。
4 內存限制問題
如果awk是32位程序(可以使用file命令判斷),那么上面的腳本1,很可能跑着跑着就core了。因為默認情況下,32位的awk最多只能消耗256M內存。
如果申請內存超過這個數就會發生異常退出。
解決方法是使用64位程序,或者修改環境變量“export LDR_CNTRL=MAXDATA=0x80000000”。(AIX4.3以上有效)
5 getline返回值問題
注意樓上的getline用法,while(getline < "a.txt")循環讀取文件直到結束。這樣寫其實是不太規范的,有隱患。
曾經我以為getline讀到文件尾會把$0置空,后來實踐發現實際不是這樣的。geline在碰上文件尾時會返回0,但$0還是保持最后一行的記錄不變。於是就改成這種寫法。
不過這種寫法,有時也會碰上問題,原因:getline返回值有三種情況:1 正常讀取到一條記錄 0 達到文件尾 -1 文件不存在或其它錯誤。
如果a.txt不存在,getline會返回-1,導致死循環。我以前曾經碰上過因為這個原因導致程序掛死,所以特別提出來讓大家注意。
建議大家使用函數前最好先看看幫助文檔里面關於函數描述。
6 管道問題
先來看這個腳本:
ls -1rt demo.txt list.txt echo -e "\n\n" | awk '{ while("ls -1rt" | getline) { print NR " : " $0 > "list.txt";}}'
猜猜看:腳本運行完后list.txt里面的內容是什么?
答案:
cat list.txt 1 : demo.txt 1 : list.txt
相信有不少朋友會覺得詫異:
有些人會認為list.txt里面應該只有一行數據,就是ls -1rt命令輸出內容的最后一行。
有些人會認為應該有6條數據才對,因為ls -1rt執行了三次。
有這種想法的人,多半是不知道awk一個規定: 默認情況下同一個文件或者管道只打開一次,如果需要重復打開,需要先close。
上面的腳本由於沒有顯式close文件和管道,list.txt和ls -1rt都只打開/執行了一次,所以輸出結果如上。
再猜猜看:下面這個腳本運行完后list.txt里面的內容是什么?
echo -e "\n\n" | awk '{ while("ls -1rt" | getline) { print NR " : " $0 > "list.txt";} close("list.txt"); close("ls -1rt");}'
7 輸出單引號問題
大家知道,awk腳本一般是用單引號括起來的,形如:awk '{ print "do something"; }' 。
因此,在awk中要使用單引號是比較麻煩的事情。網上找awk輸出單引號一般可以找到以下方法:
echo | awk '{ print "'\''"; }'
很多人因此就誤會了,以為awk腳本由於使用了單引號做為腳本開始結束標志,所以在awk腳本里面是不能直接使用單引號的。
其實這是誤會了,看下面的腳本你就知道。
cat demo.awk { print "'"; } echo | awk -f demo.awk '
可見,awk腳本是可以直接使用單引號的,也不需要使用單引號把腳本括起來。 之所以在命令行需要用這么別扭的寫法,是因為shell的關系:使用單引號括起來的內容,不會被shell當成特殊字符處理。
因為awk腳本里面經常需要$n來獲取第幾個字段的內容,而$在shell里面是有特殊意義的,代表變量開始。 如果不用單引號括起來,就會出問題。
'{ print "'\''"; }' 這段可以這樣理解:腳本分三段
1、 '{ print "' ; 2、 \'; 3、 '"; }';
每段被shell解析后是這樣的
1、 { print " ; 2、 '; 3、 "; } ;
三段合起來就是傳給awk的腳本內容:{ print "'"; }。理解了這個之后,在windows使用awk碰上以下問題,你就知道怎么解決了:
C:\Users\hch>awk '{print "";}' awk: '{print awk: ^ invalid char ''' in expression
8 自動隱式轉換問題
在c語言里面,我們習慣了整數相除,結果還是整數。所以5/2結果是2,不是2.5。
然而在awk里面,由於沒有明確指定變量類型,所以在變量計算過程經常會發現隱式轉換,整數相除結果可能是小數。
舉例:
echo | awk '{v_result = 5 / 2; print v_result}' 2.5
如果我們想要實現c語言的整數相除效果,要怎么辦呢? 可以使用int函數,如下:
echo | awk '{v_result = int(5 / 2); print v_result}' 2
9 中文豎線問題
實際工作中,經常碰上文件中每行記錄里面用豎線'|'做為分隔符的,如"a|b|c|d"。如果文件里面沒有中文,這樣做是沒問題的。
但如果有中文,特別是gbk編碼在中文時,這樣做就容易出問題了。
gbk編碼中,中文由兩個字節組成,第一個字節取值范圍是[128, 256),第二個字節取值范圍是[0, 256)。
如果第二個字節值正好是'124',也就是'|'字符的asscii碼,awk處理時就會誤以為這個字節是分隔符,從而導致分割字符串時出現錯亂。
那有哪些中文是這樣的呢? 可以用以下腳本輸出gbk編碼中包含豎線的特殊中文:(其它編碼類似)
echo|awk '{for(i = 128; i < 256; i++) { printf("%c| ", i); } }' #終端編碼要是GBK
€| 亅 倈 億 剕 厊 啢 噟 坾 墊 妡 媩 寍 峾 巪 弢 恷 憒 抾 搢 攟 晐 東 梶 榺 檤 殀 泑 渱 潀 瀨 焲 爘 ▅ ﹟ 獆 珅 瑋 瓅 畖 瘄 皘 眧 瞸 硘 磡 祙 秥 穦 竱 箌 簗 粅 紎 絴 緗 縷 纜 羭 聕 脇 膢 舼 苵 莬 葇 蓔 蕓 藎 蘾 蛗 蝲 蟶 衸 褆 襹 觸 詜 諀 謡 讄 貄 質 趞 踻 軀 輡 迀 遼 鄚 醸 鈢 銃 鋦 鍇 鎩 鐋 鑭 閨 陓 雦 靯 韡 顋 飢 饇 駖 騶 髚 魘 鮸 鰘 鱸 鴟 鵿 鷟 鹼 鼃 齶
碰上這種情況暫時我沒有發現太好的處理方法,建議使用比較長的分隔符,減少碰上問題的概率,如'|@|'。
如果分隔符不可變,那可以考慮使用iconv轉換編碼,處理完后再轉換回來。
10 函數名與變量名沖突
awk內置了很多函數,如果不小心把變量名字取得跟這些函數名字一樣,程序就會報錯。提示很不清楚,就只是說錯了,不說原因,特別坑。
例如以下這個報錯:
awk '{ if (NR == FNR) { sub[$1] = $2; } else { print sub[$1]; } }' subsid_amt.txt subsid.txt awk: { if (NR == FNR) { sub[$1] = $2; } else { print sub[$1]; } } awk: ^ syntax error
由於這個腳本是晚上加班到深夜時寫的,當時頭腦不清醒,看到報錯蒙了好久:怎么看語法都是對的,但是運行卻總是提示語法錯了。
所以現在我寫比較復雜的awk腳本,變量名都習慣前面加上v_后綴,這樣可以減少名字沖突的概率。
暫時就總結了這些。如果大家也碰上過使用awk的問題,不妨一起發出來討論一下吧:)