那些年我用awk時踩過的坑——awk使用注意事項


由於項目經歷原因,經常使用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的問題,不妨一起發出來討論一下吧:)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM