awk線上例項入門教程 第3部分

helloxchen發表於2010-10-21
awk線上例項入門教程 第三部分
——字串函式和……支票簿?

格式化輸出

雖然大多數情況下 awk 的 print 語句可以完成任務,但有時我們還需要更多。在那些情況下,awk 提供了兩個我們熟知的老朋友 printf() 和 sprintf()。是的,如同其它許多 awk 部件一樣,這些函式等同於相應的 C 語言函式。printf() 會將格式化字串列印到 stdout,而 sprintf() 則返回可以賦值給變數的格式化字串。如果不熟悉 printf() 和 sprintf(),介紹 C 語言的文章可以讓您迅速瞭解這兩個基本列印函式。在 Linux 系統上,可以輸入 "man 3 printf" 來檢視 printf() 幫助頁面。

以下是一些 awk sprintf() 和 printf() 的樣本程式碼。可以看到,它們幾乎與 C 語言完全相同。
  1. x=1
  2. b="foo"
  3. printf("%s got a %d on the last testn","Jim",83)
  4. myout=("%s-%d",b,x)
  5. print myout
複製程式碼
此程式碼將列印:
Jim got a 83 on the last test
foo-1


字串函式

awk 有許多字串函式,這是件好事。在 awk 中,確實需要字串函式,因為不能象在其它語言(如 C、C++ 和 Python)中那樣將字串看作是字元陣列。例如,如果執行以下程式碼:
  1. mystring="How are you doing today?"
  2. print mystring[3]
複製程式碼
將會接收到一個錯誤,如下所示:
awk: string.gawk:59: fatal: attempt to use scalar as array



噢,好吧。雖然不象 Python 的序列型別那樣方便,但 awk 的字串函式還是可以完成任務。讓我們來看一下。

首先,有一個基本 length() 函式,它返回字串的長度。以下是它的使用方法:
  1. print length(mystring)
複製程式碼
此程式碼將列印值:
24


好,繼續。下一個字串函式叫作 index,它將返回子字串在另一個字串中出現的位置,如果沒有找到該字串則返回 0。使用 mystring,可以按以下方法呼叫它:
  1. print index(mystring,"you")
複製程式碼
awk 會列印:
9



讓我們繼續討論另外兩個簡單的函式,tolower() 和 toupper()。與您猜想的一樣,這兩個函式將返回字串並且將所有字元分別轉換成小寫或大寫。請注意,tolower() 和 toupper() 返回新的字串,不會修改原來的字串。這段程式碼:
  1. print tolower(mystring)
  2. print toupper(mystring)
  3. print mystring
複製程式碼
……將產生以下輸出:
how are you doing today?
HOW ARE YOU DOING TODAY?
How are you doing today?



到現在為止一切不錯,但我們究竟如何從字串中選擇子串,甚至單個字元?那就是使用 substr() 的原因。以下是 substr() 的呼叫方法:
  1. mysub=substr(mystring,startpos,maxlen)
複製程式碼
mystring 應該是要從中抽取子串的字串變數或文字字串。startpos 應該設定成起始字元位置,maxlen 應該包含要抽取的字串的最大長度。請注意,我說的是 最大長度 ;如果 length(mystring) 比 startpos+maxlen 短,那麼得到的結果就會被截斷。substr() 不會修改原始字串,而是返回子串。以下是一個示例:
  1. print substr(mystring,9,3)
複製程式碼
awk 將列印:
you


如果您通常用於程式設計的語言使用陣列下標訪問部分字串(以及不使用這種語言的人),請記住 substr() 是 awk 代替方法。需要使用它來抽取單個字元和子串;因為 awk 是基於字串的語言,所以會經常用到它。

現在,我們討論一些更耐人尋味的函式,首先是 match()。match() 與 index() 非常相似,它與 index() 的區別在於它並不搜尋子串,它搜尋的是規則表示式。match() 函式將返回匹配的起始位置,如果沒有找到匹配,則返回 0。此外,match() 還將設定兩個變數,叫作 RSTART 和 RLENGTH。RSTART 包含返回值(第一個匹配的位置),RLENGTH 指定它佔據的字元跨度(如果沒有找到匹配,則返回 -1)。透過使用 RSTART、RLENGTH、substr() 和一個小迴圈,可以輕鬆地迭代字串中的每個匹配。以下是一個 match() 呼叫示例:
  1. print match(mystring,/you/), RSTART, RLENGTH
複製程式碼
awk 將列印:
9 9 3


字串替換

現在,我們將研究兩個字串替換函式,sub() 和 gsub()。這些函式與目前已經討論過的函式略有不同,因為它們 確實修改原始字串 。以下是一個模板,顯示瞭如何呼叫 sub():
  1. sub(regexp,replstring,mystring)
複製程式碼
呼叫 sub() 時,它將在 mystring 中匹配 regexp 的第一個字元序列,並且用 replstring 替換該序列。sub() 和 gsub() 用相同的自變數;唯一的區別是 sub() 將替換第一個 regexp 匹配(如果有的話),gsub() 將執行全域性替換,換出字串中的所有匹配。以下是一個 sub() 和 gsub() 呼叫示例:
  1. sub(/o/,"O",mystring)
  2. print mystring
  3. mystring="How are you doing today?"
  4. gsub(/o/,"O",mystring)
  5. print mystring
複製程式碼
必須將 mystring 復位成其初始值,因為第一個 sub() 呼叫直接修改了 mystring。在執行時,此程式碼將使 awk 輸出:
HOw are you doing today?
HOw are yOu dOing tOday?



當然,也可以是更復雜的規則表示式。我把測試一些複雜規則表示式的任務留給您來完成。

透過介紹函式 split(),我們來彙總一下已討論過的函式。split() 的任務是“切開”字串,並將各部分放到使用整數下標的陣列中。以下是一個 split() 呼叫示例:
  1. numelements=split("Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec",mymonths,",")
複製程式碼
呼叫 split() 時,第一個自變數包含要切開文字字串或字串變數。在第二個自變數中,應該指定 split() 將填入片段部分的陣列名稱。在第三個元素中,指定用於切開字串的分隔符。split() 返回時,它將返回分割的字串元素的數量。split() 將每一個片段賦值給下標從 1 開始的陣列,因此以下程式碼:
  1. print mymonths[1],mymonths[numelements]
複製程式碼
……將列印:
Jan Dec


特殊字串形式

簡短註釋 -- 呼叫 length()、sub() 或 gsub() 時,可以去掉最後一個自變數,這樣 awk 將對 $0(整個當前行)應用函式呼叫。要列印檔案中每一行的長度,使用以下 awk 指令碼:
  1. {
  2. print length()
  3. }
複製程式碼
財務上的趣事

幾星期前,我決定用 awk 編寫自己的支票簿結算程式。我決定使用簡單的 tab 定界文字檔案,以便於輸入最近的存款和提款記錄。其思路是將這個資料交給 awk 指令碼,該指令碼會自動合計所有金額,並告訴我餘額。以下是我決定如何將所有交易記錄到 "ASCII checkbook" 中:
23 Aug 2000 food - - Y Jimmy's Buffet 30.25



此檔案中的每個欄位都由一個或多個 tab 分隔。在日期(欄位 1,$1)之後,有兩個欄位叫做“費用分類帳”和“收入分類帳”。以上面這行為例,輸入費用時,我在費用欄位中放入四個字母的別名,在收入欄位中放入 "-"(空白項)。這表示這一特定項是“食品費用”。:) 以下是存款的示例:
23 Aug 2000 - inco - Y Boss Man 2001.00


在這個例項中,我在費用分類帳中放入 "-"(空白),在收入分類帳中放入 "inco"。"inco" 是一般(薪水之類)收入的別名。使用分類帳別名讓我可以按類別生成收入和費用的明細分類帳。至於記錄的其餘部分,其它所有欄位都是不需加以說明的。“是否付清?”欄位("Y" 或 "N")記錄了交易是否已過帳到我的帳戶;除此之外,還有一個交易描述,和一個正的美元金額。

用於計算當前餘額的演算法不太難。awk 只需要依次讀取每一行。如果列出了費用分類帳,但沒有收入分類帳(為 "-"),那麼這一項就是借方。如果列出了收入分類帳,但沒有費用分類帳(為 "-"),那麼這一項就是貸方。而且,如果同時列出了費用和收入分類帳,那麼這個金額就是“分類帳轉帳”;即,從費用分類帳減去美元金額,並將此金額新增到收入分類帳。此外,所有這些分類帳都是虛擬的,但對於跟蹤收入和支出以及預算卻非常有用。

程式碼

現在該研究程式碼了。我們將從第一行(BEGIN 塊和函式定義)開始:

balance,第 1 部分
  1. #!/usr/bin/env awk -f
  2. BEGIN {
  3. FS="t+"
  4. months="Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"
  5. }
  6. function monthdigit(mymonth) {
  7. return (index(months,mymonth)+3)/4
  8. }
複製程式碼
首先執行 "chmod +x myscript" 命令,那麼將第一行 "#!..." 新增到任何 awk 指令碼將使它可以直接從 shell 中執行。其餘行定義了 BEGIN 塊,在 awk 開始處理支票簿檔案之前將執行這個程式碼塊。我們將 FS(欄位分隔符)設定成 "t+",它會告訴 awk 欄位由一個或多個 tab 分隔。另外,我們定義了字串 months,下面將出現的 monthdigit() 函式將使用它。

最後三行顯示瞭如何定義自己的 awk 。格式很簡單 -- 輸入 "function",再輸入名稱,然後在括號中輸入由逗號分隔的引數。在此之後,"{ }" 程式碼塊包含了您希望這個函式執行的程式碼。所有函式都可以訪問全域性變數(如 months 變數)。另外,awk 提供了 "return" 語句,它允許函式返回一個值,並執行類似於 C 和其它語言中 "return" 的操作。這個特定函式將以 3 個字母字串格式表示的月份名稱轉換成等價的數值。例如,以下程式碼:
  1. print monthdigit("Mar")
複製程式碼
……將列印:
3



現在,讓我們討論其它一些函式。

財務函式

以下是其它三個執行簿記的函式。我們即將見到的主程式碼塊將呼叫這些函式之一,按順序處理支票簿檔案的每一行,從而將相應交易記錄到 awk 陣列中。有三種基本交易,貸方 (doincome)、借方 (doexpense) 和轉帳 (dotransfer)。您會發現這三個函式全都接受一個自變數,叫作 mybalance。mybalance 是二維陣列的一個佔位符,我們將它作為自變數進行傳遞。目前,我們還沒有處理過二維陣列;但是,在下面可以看到,語法非常簡單。只須用逗號分隔每一維就行了。

我們將按以下方式將資訊記錄到 "mybalance" 中。陣列的第一維從 0 到 12,用於指定月份,0 代表全年。第二維是四個字母的分類帳,如 "food" 或 "inco";這是我們處理的真實分類帳。因此,要查詢全年食品分類帳的餘額,應檢視 mybalance[0,"food"]。要查詢 6 月的收入,應檢視 mybalance[6,"inco"]。

balance,第 2 部分
  1. function doincome(mybalance) {
  2. mybalance[curmonth,$3] += amount
  3. mybalance[0,$3] += amount
  4. }
  5. function doexpense(mybalance) {
  6. mybalance[curmonth,$2] -= amount
  7. mybalance[0,$2] -= amount
  8. }
  9. function dotransfer(mybalance) {
  10. mybalance[0,$2] -= amount
  11. mybalance[curmonth,$2] -= amount
  12. mybalance[0,$3] += amount
  13. mybalance[curmonth,$3] += amount
  14. }
複製程式碼
呼叫 doincome() 或任何其它函式時,我們將交易記錄到兩個位置 -- mybalance[0,category] 和 mybalance[curmonth, category],它們分別表示全年的分類帳餘額和當月的分類帳餘額。這讓我們稍後可以輕鬆地生成年度或月度收入/支出明細分類帳。

如果研究這些函式,將發現在我的引用中傳遞了 mybalance 引用的陣列。另外,我們還引用了幾個全域性變數:curmonth,它儲存了當前記錄所屬的月份的數值,$2(費用分類帳),$3(收入分類帳)和金額($7,美元金額)。呼叫 doincome() 和其它函式時,已經為要處理的當前記錄(行)正確設定了所有這些變數。

主塊

以下是主程式碼塊,它包含了分析每一行輸入資料的程式碼。請記住,由於正確設定了 FS,可以用 $ 1 引用第一個欄位,用 $2 引用第二個欄位,依次類推。呼叫 doincome() 和其它函式時,這些函式可以從函式內部訪問 curmonth、$2、$3 和金額的當前值。請先研究程式碼,在程式碼之後可以見到我的說明。

balance,第 3 部分
  1. {
  2. curmonth=monthdigit(substr($1,4,3))
  3. amount=$7
  4. #record all the categories encountered
  5. if ( $2 != "-" )
  6. globcat[$2]="yes"
  7. if ( $3 != "-" )
  8. globcat[$3]="yes"
  9. #tally up the transaction properly
  10. if ( $2 == "-" ) {
  11. if ( $3 == "-" ) {
  12. print "Error: inc and exp fields are both blank!"
  13. exit 1
  14. } else {
  15. #this is income
  16. doincome(balance)
  17. if ( $5 == "Y" )
  18. doincome(balance2)
  19. }
  20. } else if ( $3 == "-" ) {
  21. #this is an expense
  22. doexpense(balance)
  23. if ( $5 == "Y" )
  24. doexpense(balance2)
  25. } else {
  26. #this is a transfer
  27. dotransfer(balance)
  28. if ( $5 == "Y" )
  29. dotransfer(balance2)
  30. }
  31. }
複製程式碼
在主塊中,前兩行將 curmonth 設定成 1 到 12 之間的整數,並將金額設定成欄位 7(使程式碼易於理解)。然後,是四行有趣的程式碼,它們將值寫到陣列 globcat 中。globcat,或稱作全域性分類帳陣列,用於記錄在檔案中遇到的所有分類帳 -- "inco"、"misc"、"food"、"util" 等。例如,如果 $2 == "inco",則將 globcat["inco"] 設定成 "yes"。稍後,我們可以使用簡單的 "for (x in globcat)" 迴圈來迭代分類帳列

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/24790158/viewspace-1040124/,如需轉載,請註明出處,否則將追究法律責任。

相關文章