Common threads: Awk by example, Part 3
字串函數和…支票?
概要:在awk系列的最後一篇,Daniel向你介紹awk重要的字串函數並展示如何從開始寫一個完整的支票結算程式。 一路走下來,你將學會如何寫自己的函數以及如何使用awk的多維陣列。本篇結束時,你將有更多的awk經驗,可以編寫更加強大的指令碼。
格式化輸出
儘管大多數時候awk的print語句都能滿足要求,但有時還是不夠。Awk還提供了兩個很好用的老朋友printf()和sprintf()。是的,這些函數跟他們在C語言中的同名函數是一樣的。printf()將在標準輸出裝置上列印一個格式化字串,而sprintf()將返回一個格式化的字串,這個傳回值還可以賦給某個變數。 如果你還不熟悉printf()和sprintf(),找一本C語言的介紹性書來讀一讀,你將很快能瞭解這兩個基本的函數。你也能在Linux系統上輸入"man 3 printf"來查看printf()的man說明。
這兒是一些awk的sprintf()和printf()的代碼例子。如你所見,所有用法看起來都跟C語言中一樣:
x=1 b="foo" printf("%s got a %d on the last test\n","Jim",83) myout=("%s-%d",b,x) print myout |
這個代碼將列印:
Jim got a 83 on the last test foo-1 |
字串函數
Awk有很多的字串函數,這是個好事。因為在awk中你不能像C、C++和python等其他語言一樣,把字串當做一個字元數組來處理,所以你真的很需要字串函數。例如你執行下面的代碼:
mystring="How are you doing today?" print mystring[3] |
你將得到一個像下面一樣的錯誤:
awk: string.gawk:59: fatal: attempt to use scalar as array |
哦,好吧!儘管不如Python的序列類型方便,awk的字串函數還是可以做這些工作的。讓我們來看看這些函數。
首先,我們有基本的length()函數,它會返回字串的長度。這兒是如何使用它的例子:
這段代碼將打出下面的值:
OK,讓我們繼。下一個字串函數叫index,他將返回一個子串在另一個字串中出現的位置,如果找不到這個子串,就返回0。使用mystring作為例子,我們可以這樣調用:
print index(mystring,"you") |
Awk列印:
我們繼續來看兩個更容易的函數, tolower() 和toupper()。你可能猜到了,這些函數將分別返回全部是大寫或全部是小寫字串。要注意的是tolower() 和toupper()返回新的字串,而不是修改原來的字串。這段代碼:
print tolower(mystring) print toupper(mystring) print mystring |
…將產生下面的輸出:
how are you doing today? HOW ARE YOU DOING TODAY? How are you doing today? |
到目前為止,一切順利,但如果我想從字串中提取一個子串或者甚至是單個字元,那該咋辦呢?substr()函數就是幹這個的。這兒顯示如何調用substr():
mysub=substr(mystring,startpos,maxlen) |
Mystring應該是你想從中提取子串的一個字串變數或者是一個原始的字串。 startpos應該設定為開始字元的位置。 maxlen 應該包含你想要提取的子串的最大長度。注意我說的最大長度;如果length(mystring)的值小於startpos+maxlen,你的結果將被截斷。substr()不會修改原始字串,而是返回一個新字串。這兒是一個例子:
print substr(mystring,9,3) |
Awk將打出:
如果你經常使用那些用數組索引來訪問字串的語言編程,那麼要牢記awk中是用substr()來代替這種方法的。你將用它來提取單個字元和子串,因為awk是一個基於字串的語言,你會經常用到它的。
現在,我們來繼續看一些更棒的函數,第一個叫做match()。Match() 有點像index(),除了像index()一樣搜尋子串,他還可以搜尋Regex。Match()將返回匹配項的開始點,或者返回0(如果找不到匹配項)。另外,math()也會設定兩個變數,分別叫RSTART和RLENGTH。RSTART包含傳回值(第一個匹配項的位置),RLENGTH指出匹配的字元長度(如果找不到匹配項,則為-1)。使用RSTART、RLENGTH、substr()和一個小的迴圈,你能很容易的在你的字串中迭代每個匹配項。這兒是一個match()調用的例子:
print match(mystring,/you/), RSTART, RLENGTH |
Awk將列印:
字串替換
現在,我們來看兩個字串替換函數sub()和gsub()。這兩個傢伙跟我們已經見過的函數有些輕微不同的地方在於:他們實際修改原始字串。這兒是個模版,顯示如何調用sub():
sub(regexp,replstring,mystring) |
當你調用sub()時,它將在mystring中找到第一個匹配regexp的字元序列,並使用replstring替換這個字元序列。sub()和gsub()有相同的參數,他們唯一的不同點在於,sub()只替換第一個匹配項(如果有匹配項的話),而gsub()將執行一個全域替換,替換字串中的所有匹配項。這兒是一個調用sub()和bsub()的例子:
sub(/o/,"O",mystring) print mystring mystring="How are you doing today?" gsub(/o/,"O",mystring) print mystring |
因為第一個sub()直接修改了mystring,我們必須重設mystring到他的原始值。當執行上述的代碼時,awk將輸出下面的內容:
HOw are you doing today? HOw are yOu dOing tOday? |
當然,更加複雜的Regex也是可以的。我將留下來讓你自己去測試一些複雜的Regex。
我們最後給你介紹一個叫做split()的函數,以此來結束字串函數的內容。Split()的工作是用來分割一個字串,並將分割後的多個部分放入一個使用整數索引的數組中。這兒是一個split()調用的例子:
numelements=split("Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec",mymonths,",") |
當調用split()時,第一個參數是待分割的字串或者字串變數。第二個參數是split()用來填充已分割部分到其中的數組名字。第三個參數指定分割字串的分隔字元。Split()返回分割後的字串數量。Split()將每個分割後的字串放到一個索引從1開始的數組中,因此下面的代碼:
print mymonths[1],mymonths[numelements] |
....將列印出:
特殊字元串形式
一個快捷方法 –當調用length()、sub()、或 gsub()時, 你可以不寫最後一個參數,awk將使用$0(當前的整行)代替。列印一個檔案中每行的長度可使用下面的awk指令碼:
財務的樂趣
幾周前,我決定使用awk寫一個我自己的支票結算程式。我使用一個tab分隔的文字檔來記錄最近的存款和提款記錄。當時的想法是使用一個awk指令碼來處理這個資料,指令碼會自動合計總額並告訴我餘額。這兒是我如何記錄我所有的存取款事務到我的“ASCII支票”例子:
23 Aug 2000 food - - Y Jimmy's Buffet 30.25 |
這個檔案中使用一個或多個tab符分隔每個欄位。日期欄位($1)後,有兩個欄位分別叫“消費類別”和“收入類別”。當我像上面這行一樣輸入一個消費類記錄時,我在消費類別欄位使用一個4個字元描述的簡稱,在收入類別欄位使用一個“-”。上面的記錄表示這個特殊的記錄是一個“食品消費”。下面這兒是一個存款記錄:
23 Aug 2000 - inco - Y Boss Man 2001.00 |
在這種情況下,我在消費類別欄位使用一個“-”,在收入類別欄位填了“inco”。“inco”是我的常規(薪水類別)收入的簡稱。使用類別簡稱允許我產生收入和支出分類。至於記錄的其餘部分,所有其他的欄位都很容易自解釋。Cleared欄位("Y" or "N")記錄該事務是否已經提交到我的銀行賬戶。除此之外,還有個事務描述和一個正數表示的該事務發生的美元總額。
用來計算當前餘額的演算法不是很難。Awk僅需要一行接一行的讀入每行。如果一行有消費類別但無收入類別(標記為“-”),這行就是借項。反之,如果一行有收入類別但無消費類別,這行就是貸項。如果一行既有消費類別又有收入類別,那麼此行記錄的是一個“類別轉移”,也就是說該記錄的美元金額值要從消費類別中減去,並增加到收入類別。此外,所有這些類別都是虛擬,但對於跟蹤收入和支出以及預算編製很有用。
代碼
是時候來看看代碼了。我們將從第一行開始,然後跟一個BEGIN塊和一個函數定義:
balance, part 1
#!/usr/bin/env awk -f BEGIN { FS="\t+" months="Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec" } function monthdigit(mymonth) { return (index(months,mymonth)+3)/4 } |
在任何awk指令碼中增加第一行“#!...”,將允許直接從shell中執行該指令碼,當然需要先用"chmod +x myscript"給指令碼添加執行許可權。剩下的行定義了我們的BEGIN塊,在awk開始處理我們的支票檔案前會執行BEGIN塊的內容。我們設定了FS變數為“\t+”,這告訴awk我們使用一個或多個tab分隔欄位。另外,我們定義了一個叫months的字串,monthdigit()函數會用到它,我們馬上來介紹這個函數。
最後三行展示了如何定義你自己的awk函數。定義格式很簡單——輸入“function”,然後跟著函數名以及放在圓括弧中的由逗號分隔的參數列表。之後,將函數需要執行的代碼塊放在一個大括弧“{}”中。所有函數都能訪問全域變數(比如我們的months變數)。此外,awk提供了一個返回語句,允許函數返回一個值,這個返回操作跟C、python或其他語言中的返回語句操作是類似的。這個特殊的函數將一個3個字元表示的月度名字轉換為等價的數字值。例如,下面這個代碼:
....將列印:
現在,讓我們繼續來寫更多的函數。
財務函數
這兒是三個為我們執行記賬功能的函數。我們很快會看到的主函數將按順序處理支票檔案的每一行記錄,處理一行記錄時會調用這三個函數中的一個來將適當的事務記錄到一個awk數組中。有三個基本類型的事務,他們是貸(doincome)、借(doexpense)和轉移(dotransfer)。你可能注意到了這三個函數都接受一個叫做mybalance的參數。mybalance是一個二維數組的預留位置,我們將傳遞一個二維數組作為入參。到目前為止我們還沒有處理過二維數組,然而正如下面你將看到的,處理二維數組的文法相當簡單。僅僅使用一個逗號來分隔維度,就可以處理了。
我們將按如下所述的方式把資訊記錄到mybalance中。數組第一個維度範圍是0到12,用來代表月份,0表示是整年。第二個維度是四字元表示的類別,比如“food”或“inco”,這是我們實際在處理的類別。所以,要找到整年的食物類別結餘,你可以查看mybalance[0,"food"]。要找到6月份的收入,你可以查看mybalance[6,"inco"]。
balance, part 2
function doincome(mybalance) { mybalance[curmonth,$3] += amount mybalance[0,$3] += amount } function doexpense(mybalance) { mybalance[curmonth,$2] -= amount mybalance[0,$2] -= amount } function dotransfer(mybalance) { mybalance[0,$2] -= amount mybalance[curmonth,$2] -= amount mybalance[0,$3] += amount mybalance[curmonth,$3] += amount } |
當調用doincome()或其他任何函數時,我們在兩個地方記錄當前事務——mybalance[0,category] 和 mybalance[curmonth, category],分別是全年的類別結餘和當前月的類別結餘。這允許我們在後面產生年度或月度收支分類報表。
如果你仔細看看這些函數,你將發現mybalance引用的數組是傳入的參數。此外,我們也用到了幾個全域變數:curmonth,儲存的是目前記錄的月份數字值,$2(消費類別),$3(收入類別),以及金額($7,美元金額)。當doincome()和他的朋友們被調用時,所有這些變數都已經正確的設定為為當前正在處理的記錄的對應值。
主代碼塊
這兒是主代碼塊,它包含解析輸入資料的每一行的代碼。記住,因為我們已經正確設定了FS變數,我們能使用$1引用第一個欄位,使用$2引用第二個欄位,以此類推。當doincome()和它的朋友們被調用時,函數能訪問curmonth、$2、$3和金額的當前值。看一下這個代碼,我們後面來解釋。
balance, part 3
{ curmonth=monthdigit(substr($1,4,3)) amount=$7 #record all the categories encountered if ( $2 != "-" ) globcat[$2]="yes" if ( $3 != "-" ) globcat[$3]="yes" #tally up the transaction properly if ( $2 == "-" ) { if ( $3 == "-" ) { print "Error: inc and exp fields are both blank!" exit 1 } else { #this is income doincome(balance) if ( $5 == "Y" ) doincome(balance2) } } else if ( $3 == "-" ) { #this is an expense doexpense(balance) if ( $5 == "Y" ) doexpense(balance2) } else { #this is a transfer dotransfer(balance) if ( $5 == "Y" ) dotransfer(balance2) } } |
在主代碼塊中,頭兩行設定curmonth為一個1到12之間的整數值,並設定amount為欄位7的值(為了代碼更容易理解)。然後我們有四行比較有趣的代碼,我們在一個叫globcat的數組中寫入資料。Globcat,或者叫全域分類數組,是用來記錄在支票檔案中遇到的所有分類——"inco", "misc", "food", "util"等等。例如,如果$2== "inco",我們設定globcat["inco"]為"yes"。以後我們能使用簡單的"for (x in globcat)"迴圈來迭代訪問我們的類別列表。
後面的二十多行,我們分析$2和$3,並適當的記錄事務。如果 $2=="-"並且$3!="-", 表明這是一個收入記錄,所以我們調用doincome()。 反之,我們調用doexpense();如果$2和$3都包含分類,我們調用dotransfer()。每次我們都傳遞"balance"數組到這些函數,以便於適當的資料都記錄在這個數組中。
你也注意到了有幾行說"if ( $5 == "Y" ), 在balance2中記錄相同的事務"。 我們這兒到底是幹什麼呢?你應該還記得$5的值要麼是 "Y"要麼是"N",表示是否當前事務已經提交到銀行賬戶。 因為僅提交到銀行賬戶的事務記錄在balance2中,balbnce2將包含實際賬戶的結餘,而"balance"將包含所有的事務,無論事務是否已經提交。你能使用balance2來核對你的資料條目(因為它應該與你銀行帳號餘額保持一致),使用"balance"來確保你不會透支你的賬戶(因為它會考慮到任何你已經寫了但還沒兌現的支票)。
產生報告
在主代碼塊重複的處理完每個輸入記錄後,我們現在有了一個相對完整的按月按類別的借貸記錄。現在,我們需要做的事情是寫一個END塊來產生報告,此種情況下一個最適和的代碼如下:
END { bal=0 bal2=0 for (x in globcat) { bal=bal+balance[0,x] bal2=bal2+balance2[0,x] } printf("Your available funds: %10.2f\n", bal) printf("Your account balance: %10.2f\n", bal2) } |
這段代碼列印出一個看起來如下的概要報告:
Your available funds: 1174.22 Your account balance: 2399.33 |
在我們的END塊中,我們使用"for (x in globcat)"結構迭代每一個類別,基於所有的事務記錄,計算出結餘。我們實際上計算出兩個餘額,一個是可用的資金,另一個是賬戶餘額。要執行這個程式來處理你自己記錄在一個叫"mycheckbook.txt"檔案中的財務事項,只要將上面的所有代碼放到一個命名為"balance"的文字檔中,使用"chmod +x balance"修改檔案的許可權,然後輸入"./balance mycheckbook.txt"開始執行。這個結算指令碼將計算你所有的事務並列印出兩行餘額摘要。
增強
我使用了一個這個程式的更加進階的版本來管理我個人和生意上的財務事項。我的版本(因為空白間所限,我無法放到這裡)列印出按月度的收入和支出,包括年度總和,淨收入和一推其他的東西。更勝一籌的是,我使用HTML格式輸出資料,以便我能在一個Web瀏覽器中查看J如果你覺得這個程式有用,我鼓勵你增加這些特性到這個指令碼中。你並不需要配置他來記錄任何附加的資訊,所有你需要的資訊都在balance和balance2中。僅更新END塊就可以達到要求!
我希望你喜歡這個系列。關於awk更多的資訊請參考下面列出的資源。
Resources
- Read Daniel's earlier installments in the awk series: Awk by example, Part 1 and Part 2 on developerWorks.
- If you'd like a good old-fashioned book, O'Reilly's sed & awk, 2nd Edition is a wonderful choice.
- Be sure to check out the comp.lang.awk FAQ. It also contains lots of additional awk links.
- Patrick Hartigan's awk tutorial is packed with handy awk scripts.
系列結束