阿旺的 Linux 開竅手冊

 基礎篇    進階篇    補腦篇    指令索引  


HY-STAR's  Ads IWS's Ads


版權所有, 引用請註明出處

 進階篇

Advanced Chapter 5 : awk

A5.0 awk 文字記錄的資料處理
        awk 基本用法
        awk 程式模式
            自定變數
            內建變數
            BEGIN 和 END
            輸出函數 print 和 printf ( )
            關聯矩陣(Associative Arrays)
            system 執行系統指令
            close 關閉檔案/管線
            getline 讀入資料
            數學函數
            字串函數
            自定函數





















awk


A5.0 awk 文字記錄的資料處理
許多的 UNIX/Linux 指令名稱都源自於很莫名奇怪的縮寫,awk 更是!其指令名稱來自其三位作者 Alfred Aho, Peter Weinberger 和 Brian Kernighan 的姓氏縮寫。

awk 為一直譯語言(Interpreter),大量引用 C 語言的語法,取 C 語言對文字處理和輸出格式的精華,再加上支援原始 C 語言所沒有的對正規表示法的匹配和關聯矩陣(Associative Arrays)的支援。

因此 awk 和 C 語言應用上最大的差別為 C 語言為通用的程式語言,指令和語法多而複雜,而 awk 小而精簡,特別適合用來處理和計算用文字記錄的資料和文字的排版。1980 年代 awk 曾很流行,一直到約 1990 才逐漸被另一通用的直譯語言 Perl 所瓜分。

awk 基本用法
相對於 sed 是以行為單位處理文字,awk 還可以用〝欄〞(Field)來處理文字。

例如以 ls -l 列出冗長檔案資訊的例子,如下共有 8 個欄位(欄位間以空白當間隔)
export TIME_STYLE=long-iso ←設時間格式(不同環境設定會影響〝ls -l〞的輸出格式)
ls -l
drwxr-xy-x  aaa  aaa  4096  2011-09-07  11:44  Desktop 
drwxr-xy-x  aaa  aaa  4096  2011-09-07  11:44  Documents 
drwxr-xy-x  aaa  aaa  4096  2011-09-07  11:44  Music 
drwxr-xy-x  aaa  aaa  4096  2011-09-07  11:44  Pictures 
drwxr-xy-x  aaa  aaa  4096  2011-09-07  11:44  Public 
↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑ 
$1  $2  $3  $4  $5  $6  $7  $8 ←欄位變數

如把 ls -l 的輸出經管線awk 寫成 ls -l | awk {},對 awk 來說毎一欄位會自動儲存在其預設的欄位變數〝$0〞,〝$1〞~〝$N〞(N 為欄位數)。
awk 用在指令其間的參數和敘述可能很抽象和複雜,awk 可能會無法判讀,故一般除檔案和選項以外的參數都會用單引號「'」把其括起來而寫成 ls -l | awk '{}'

上例中第一行共有 8 個欄位,但會產生有 9 筆欄位變數從 $0 到 $8 的值各如下:
變數 內容
$0 drwxr-xy-x 2 aaa aaa 4096 2011-09-07 11:44 Desktop $0 內容為一整行
$1 drwxr-xy-x 欄位=1的字串
$2 2 欄位=2的字串
$3 aaa 欄位=3的字串
$4 aaa 欄位=4的字串
$5 4096 欄位=5的字串
$6 2011-09-07 欄位=6的字串
$7 11:44 欄位=7的字串
$8 Desktop 欄位=8的字串

最特別的欄位變數是〝$0〞,〝$0〞是一整行的內容,而〝$0〞內容有變更時會自動更新其他欄位變數〝$1〞~〝$N〞。

如只單純寫成 ls -l | awk '{}' 是不會有任何的輸出,因沒輸出函數(function)和要輸出那一欄位,awk 內建的輸出函數最常用的為〝print〞,例如我只要輸出 ls -l 檔案的大小欄(欄位=5)和檔名欄(欄位=8)可寫成。。

例:
$ ls -l | awk '{print $5,$8}'←只輸出欄位 5 和 8
4096 Desktop
4096 Documents
4096 Music
以下略

而 print 內的逗號〝,〞代表輸出欄位間隔(Output Field Separator〝OFS〞),預設的輸出欄位間隔為空白(space),即一個逗號〝,〞等於輸出一個空白。user 可以試者不加逗號輸入 ls -l | awk '{print $5 $8}' 便可知其間的差異。

而如果指令不只一個時,指令之間要用〝;〞間隔或寫在下一行;例如 ls -l | awk '{size=$5;file_name=$8;print size,file_name}'。(範例中的〝size〞 和〝file_name〞為自定變數)

知道欄位變數〝$N〞的用法後,我們可以很容易利用這一特性來更改輸出格式,awk 最基本的用法就是改變輸出格式,如下例為用 awk 改變 ls 原來的輸出格式。

例:
$ ls -l | awk '{print "File",$8,"size =",$5,"Byte"}'
File Desktop size = 4096 Byte
File Documents size = 4096 Byte
File Music size = 4096 Byte
以下略

上例中可在函數 print 中添加要輸出的字串,要加的字串用「"」括起來,如上例中的 "File" 和 "size ="。

曾讓 awk 流行的原因為其(欄位)變數還可用來計算,如〝S3*base-1〞等,承上例如我想把檔案大小的欄位以 KiB 顯示我可以用 $5 除以 1024 。

例:
$ ls -l | awk '{print "File",$8,"size =",$5/1024,"KB"}'
File Desktop size = 4 KB
File Documents size = 4 KB
File Music size = 4 KB
以下略
$ ls -l | awk '{print "File",$8,"size =",$5/1024,"KB"}' > reformate.txt ←將新的輸出存成檔案〝reformat.txt〞

awk 的資料輸入除了來自管線〝|〞外也可來自檔案,例如 awk '{print $3}' data.txt 為印出檔案〝data.txt〞的欄位 3。
如需由鍵盤輸入的互動程式,awk 也支援〝-〞標準輸入,如下為任意輸入兩個數字會輸出相乘結果。

例:
$ awk '{print $1*$2}' -   ←最後面的〝-〞 為標準輸入(鍵盤)
3.14  1.41421 ←任意輸入兩個數字
4.44062 ←輸出相乘結果 (按 <Ctrl-D> 來結束)

至於執行 awk 除了上述方法外,也可和 sed 一樣用選項〝-f〞來使用外部的 script 檔或寫成 Shell 程式。
用外部的 script 檔時要拿掉括住〝{}〞的單引號「'」 ,如下的範例:

例:(用外部 awk script 檔)
$ cat awk_scr ←例如有一外部 script 檔〝awk_scr〞,內容如下
{print "File",$8,"size =",$5/1024,"KB"}
$ ls -l | awk -f awk_scr ←用選項〝-f〞來使用外部的 script 檔〝awk_scr〞

例:(寫成Shell 程式)
$ cat awk_scr1 ←例如有一檔〝awk_scr1〞,內容如下
awk '{print "File",$8,"size =",$5/1024,"KB"}'
$ chmod +x awk_scr1 ←讓〝awk_scr1〞具有可執行的權限
$ ls -l | ./awk_scr1 ←執行〝awk_scr1〞


^ back on top ^

awk 程式模式
awk 除了用在基本用法中的輸出模式,還有強大的程式模式,因 awk 本身有自己的 script 直譯語言。至於要不要學 awk 的程式模式?因每個人需求和專才不同,下段的敘述看完自行判斷。

許多的應用可用 C 語言或 shell script awk 皆可達到要求,但 C 語言進入門檻較高,且對小程式來說如用牛刀殺雞太大費周章,而用 shell script 來處理文字常力有未逮。如熟 awk script 語言且才華洋溢,幾乎可完全取代所有過濾程式(如grep/sed/tr/cut 等)外加有計算統計功能,如有文字記錄的資料要處理,awk 可列為第一考慮。且有人測過同一功能用 awk 來完成其執行的速度是 shell script 的 30 倍以上。awk script 語法大量借用 C 語言語法,如果已熟悉 C/C++/Java 等語言,再來學習 awk 的 script 語言就覺得很簡單,但反之可能就比較吃力。

讀者可以不必懂 C 語言也可以精通 awk script 語言(awk script 相對 C 語言簡單許多),但限於篇幅後續的說明是假設讀者已了解 C 語言因此不會特別去解釋 C 語言的指令和語法,反而和 C 語言不一樣的地方才會去說明。

awk 程式結構主要為 [Pattern]'[{Actions}]'[Files],在 awk 的術語中〝Pattern〞可不是正規表示法的樣板,白話的解釋是判斷式,而〝{Actions}〞為要執行的敘述,最後一項 Files 為要處理的文字資料檔案,當然除了檔案也可來自其他的命令經管線awk

Pattern(判斷式)不一定會存在,如果有的話則判斷式成立時則執行後面的 {Actions},否則 {Actions} 不被執行。
例如要篩選檔案大小可用指令〝find -size〞,用 awk 來完成可寫為 ls -l | awk '$5 > 8192 {print $5,$8}',表示欄位 5 的內容如大於 8192,則執行〝print $5,$8〞。
如沒有 Pattern 如 ls -l | awk '{print $5,$8}' 則不管任何情況都會執行 {Actions} 中的敘述〝print $5,$8〞。

〝{Actions}〞也可省略,如省略時預設動作是〝print $0〞,如 awk 'NR <=5' /etc/passwd ,此動作就像 head。(例中的〝NR〞為內建變數)

awk 的 Pattern 提供如下和 C 語言類似的判斷語法:

awk relational operators
Operator Meaning
== 相等
!= 不相等
> 大於
>= 大於或等於
< 小於
<= 小於或等於
&& 條件的 AND 判斷
|| 條件的 OR 判斷

和傳統 C 語言比較不一樣的是其 Pattern 可對正規表示法作匹配判斷,如〝~〞表示有匹配到正規表示法,而〝!~〞為沒有匹配到正規表示法。

語法如下:(實際上目前版本的 awk 可支援到延伸正規表示法)

awk relational operators
Operator Meaning Note
字串~ /正規表示法/[動作(actions)] 字串如可匹配正規表示法,則執行 Actions
[註 5.0]
字串 ! ~ /正規表示法/[動作]
字串如無法匹配正規表示法,則執行 Actions
/正規表示法/[動作]
目前讀入的行如可匹配正規表示法,則執行 Actions
(省略字串和和符號〝~〞的敘述,會用 $0 來匹配正規表示法)
!/正規表示法/[動作]
目前讀入的行如無法匹配正規表示法,則執行 Actions

要匹配的正規表示法或要記得用成對斜線〝/〞括起來;例如 ls /etc | awk '$1 ~ /pr*e/' 表示如果某行的欄位 1 的內容可匹配正規表示法的〝pr*e〞,則輸出該行。
而如果同時省略要匹配的字串和符號〝~〞,此時的意義就可視為搜尋(目前讀入的行如搜尋到匹配的正規表示法則執行Actions),如 awk '/colou*r/' file 。此動作就像指令 grep

{Actions}部份當然不只有 print,如下和 C 語言類似的指令和語法都合法。

List of awk syntax Note
if ( conditional ) statement [ else statement ]
while ( conditional ) statement
do {statement} while (conditional)
for ( expression ; conditional ; expression ) statement
for ( variable in array ) statement
break
continue
{ [ statement ] ...}
variable=expression
[command][&][|]getline [var][<][ file]
print [ expression-list ] [ > expression ]
printf ( ) format [ , expression-list ] [ > expression ]
function( )
next
exit
參考來源
http://www.grymoire.com/Unix/Awk.html

另外 awk 的註解和 sed 一樣皆用〝#〞。

特別注意這三大塊各別用中括號〝[ ]〞括起來表示不一定要同時存在,〝BEGIN {}〞為當資料還沒讀進來時就先執行的部份,而〝END {}〞為資料都讀完才去執行的部份,故〝BEGIN {}〞和〝END {}〞只會各執行一次。而〝{main}〞內為主程式是每筆資料進來都會被執行。

所以 BEGIN {} 時常用來作初始設定,而 END {} 用來跑結束時的運算結果,而每筆資料進來都要運算的部分就放在 {main}。

例如/etc/shadow檔內的欄位間隔為〝:〞並非空白,我可以在檔案都還沒讀進來時在 BEING {}區塊內設定欄位間隔為〝:〞(設定內建變數 FS=":"),而會讀入每筆資料的{main}區塊內檢查看欄位 2 是否為空白來找出看誰沒設帳號密碼。實例如下:

例:(以 root 登入測試才可讀取檔案〝/etc/shadow〞)
# cat awk_nopasswd ←找出沒設帳號密碼的程式
BEGIN { # ←BEGIN{}區塊
       FS=":" # ←設定欄位間隔為〝:〞
       total=0 # ←初始自定變數值為 0
      }

{ #←主程式區塊
    if ( $2 == "" )
   {
       print $1 ": no password"
       total ++
   }
}

END { print "Total no password account=",total} #←END {}區塊
# cat /etc/shadow | awk -f awk_nopasswd ←執行〝找出沒設帳號密碼的程式〞
john: no password
fossett: no password
Total no password account= 2
如果某一程式不用讀取任何檔案,我可以寫在 BEGIN {} 內 。

例:
$ awk 'BEGIN{print "Hello AWK"}'
Hello AWK
上例中如 print 寫在 END{}區塊內,因沒任何資料會讀入,故永遠不會被執行。如寫在 {main} 區塊,如下例 ls | awk '{print "Hello AWK"}',則不管三七二十一,有資料讀入就輸出一次〝Hello AWK〞。

所以 END {}區塊為用來跑結束時的運算結果,如下例為在 END {}區塊內印出內建變數〝NR〞就可知檔案共有多少行(模擬指令 wc -l) 。

例:
$ awk 'END {print NR}' /usr/share/dict/linux.words ←模擬指令 wc -l
479829


^ back on top ^













[註5.0] 〝string ~ /regex/〞 此種寫法在 awk 執行時會展開為〝{if (string ~ /regex/) print}〞,所以寫成外部的 script 檔時〝string ~ /regex/〞此種寫法要去除最外面的"'{ }"。
(例如 awk '$8 ~ /pr*e/展開後等於 awk '{if ($8 ~ /pr*e/) print}')