19 12
發新話題

Perl 學習手札

10. Perl的檔案存取
檔案系統在寫程式時是非常重要的一個部份,尤其對於Perl的使用者來說,因為Perl能夠處理大量而且複雜的資料,所以經常被拿來作為Unix作業系統的管理工具,尤其對於Unix-like系統管理員而言,在進行系統日誌的管理時,存取檔案,讀取檔案內容並加以分析就是最基本的部份。當然,你還可能進行目錄的修改,檔案權限的維護等等跟系統有密切關係的操作。
10.1 檔案代號 (FileHandle)
當你的Perl想要透過作業系統進行檔案存取時,可以利用檔案代號取得和檔案間的繫結,接下來的操作就是透過這個檔案代號和實體的檔案間進行溝通。也就是說,我們要進行檔案操作時,可以先定義相對應實體檔案的代號,以便我們用更簡便的方式對檔案進行存取。
而所謂的檔案代號其實就是由使用者自行命名,並且用來跟實體檔案進行連結的名稱,他的命名規則還是依循Perl的命名規則,大家對於這個規則應該相當熟悉了,不過我們還是再次提醒一下:可以數字,字母及底線組成,但是不能以數字作為開始。而且一般來說,我們幾乎都習慣以全部大寫來作為檔案代號,因為檔案代號並不像其他變數,會使用某些符號作為識別,所以幾乎約定成俗的全部大寫習慣也是有存在的道理。
當然,你也可以依照自己的習慣來為檔案代號命名(註一),這表示所謂的全部大寫絕對不是一種鐵律,就像Perl程式語言本身,希望以最少的限制來進行程式設計的工作。
10.2 預設的檔案代號
對於檔案的輸出,輸入而言,其實就跟平常時候,你利用Perl在進行其他的操作非常接近,有時候只是輸出到不同的媒介上。所以Perl其實已經預定了幾種檔案代號,讓你不需要每次寫Perl的程式就必須去重新定義這些代號,很顯然的,幾乎大部份的程式都會需要這些檔案代號。
這六個預設的檔案代號分別是:STDIN,STDOUT,STDERR,DATA,ARGV,ARGOUT,看起來相當熟悉吧?沒錯,因為很多時候,我們其實就是靠這些預設的檔案代號在進行程式的輸出,輸入。只是我們還沒有瞭解這些其實就是檔案代號。換個角度來看,其實即使我們都不知道他們是預設的檔案代號,我們就能運作自如,那麼對於檔案代號的使用顯然就不是太難。不過,我們還是要再來看看這六個Perl預設的檔案代號。其中有些我們已經使用過了,我們就先對其中幾個預設的檔案代號來進行介紹:

STDIN:這也就是我們常看到的「標準輸入裝置」,當Perl開始執行時,它預設接受的外部資訊就是從這裡而來。就像我們之前曾經看過的寫法:
引用:
my $input = <STDIN>;                  # 從標準輸入裝置取得資料
print $input;
這時候,當我們從鍵盤輸入時,Perl就可以正確的取得資訊,並且透過STDIN取得使用者用鍵盤打入的一行字串。因此他的運作方式就是以一個檔案代號來進行。當然,你可以透過系統函式庫的配合,讓你的標準輸入轉為其他設備之後你就進行其他運用,不過這顯然不是這裡的主題,還是讓我們言歸正傳。對於Perl來說,他從檔案系統讀入資料是以行為單位,因此即使是利用STDIN,Perl還是會等到使用者鍵入換行鍵時才會有所動作。

STDOUT:相對於標準輸入,這就是所謂的標準輸出,也就是在正常狀況下,你希望Perl輸出的結果就是透過STDOUT來進行輸出的。而一般來說,我們所使用的就是螢幕輸出。你可以看看這個程式裡的寫法:
引用:
my $output = "標準輸出";

print "$output\n";
print STDOUT "$output\n";
沒錯,就像我們所預期的,Perl透過螢幕印出了兩行一模一樣的結果,也就是印了兩行「標準輸出」。原因非常簡單,因為當我們使用print的指令時,Perl會使用STDOUT當作預設的檔案代號,所以一般狀況下,如果我們沒有指定檔案代號時,Perl就會自動輸出到STDOUT。所以事實上,我們早就開始使用檔案代號了,只是我們自己並沒有發覺。或說,Perl原來的期望就是希望使用者都可以在最沒有負擔的狀況下任意輸出到螢幕,或從鍵盤輸入,畢竟Perl程式設計師那麼的怕麻煩,一般的鍵盤輸入,螢幕輸出又是使用的那麼頻繁,當然要讓程式設計師以最簡單的方式達成。而且非常顯然,這個目的也算達到了。

STDERR:標準的錯誤串流,也就是程式錯誤的標準輸出。正常而言,當程式發生錯誤時,程式可以發出錯誤訊息來通知使用者,這時候這些錯誤訊息也能透過檔案代號處理,把這些訊息丟進錯誤訊息串流。不過這樣說實在不太容易理解,那我們來玩個遊戲吧:
引用:
my $output = "標準輸出";

print "$output\n";
print STDERR "$output\n";
我們一開始定義了一個字串$output,一開始我們先直接從標準輸出印出這個字串,接下來我們便要求Perl把這個字串送出到錯誤串流中。這樣會發生甚麼有趣的事呢?讓我們來看看:
引用:
[hcchien@Apple]% perl stderr.pl
標準輸出
標準輸出
[hcchien@Apple]% perl stderr.pl > error.txt
標準輸出
第一次,我們直接執行了stderr.pl這支程式,而結果顯然有點平淡無奇。於是我們第二次執行時,就在後面加上了">error.txt",對於熟悉Unix操作的人大概知道,這樣的方式其實是把程式執行時的錯誤訊息導向檔案"error.txt"了。所以STDOUT只輸出了第一行的print結果,而系統也產生了另外的error.txt的檔案,因為我們把標準錯誤串流送到了這個檔案裡,所以我們可以發現檔案裡正好有我們輸出到標準錯誤串流的字串。這樣的作法對於可能把Perl拿來進行系統管理的腳本程式時,就可以發揮很大的功能。因為我們也許希望某個程式可以幫我們進行一些日常的瑣事,而在處理這些瑣事的同時,如果發生甚麼異常狀況,可以把錯誤訊息存在某個檔案中,這樣一來我們就可以只檢查這個日誌檔案。

ARGV:我們可以直接利用參數來讀取某些檔案的內容,使用者只需要在執行程式時,在程式後加上檔案名稱作為參數,然後在程式中我們就可以直接讀到檔案的內容了。還是用個例子比較容易理解:
引用:
my $input = <ARGV>;
print "$input\n";

於是我們試著執行它,並且加上參數"error.txt"

[hcchien@Apple]% perl argv.pl > error.txt
標準輸出
沒錯,當我們用了剛剛得到的error.txt當參數時,程式裡面直接使用預設檔案代號ARGV來讀取檔案內容,所以當我們印出來時,就可以看到剛剛寫入檔案的內容了。不過由於Perl讀檔案的性質,其實我們只印出了檔案內的第一行,不過這部份我們稍後會再提到,這裡暫且略過不談。

不過Perl的ARGV其實非常好用,讓我們來看看使用陣列形式的@ARGV。也就是程式的參數,跟我們曾經提過的副常式參數有幾分相似。它也是把取得的參數放入陣列中,然後在程式裡,就可以直接叫用陣列,取出參數,就像這樣:
引用:
my $input = shift @ARGV;
print "$input\n";

我們用同樣的方式執行,可以看到這樣的結果:

[hcchien@Apple]% perl argv.pl error.txt
error.txt
另外,我們也可以對ARGV進行一般檔案代號的操作方式,不過這些將在稍後提到檔案操作時再來討論。

10.3 檔案的基本操作
我們剛剛提到了一些Perl預設的檔案代號,這些檔案代號都是由Perl自動產生的。因此當我們開始執行Perl的程式時,就可以直接使用這些檔案代號。可是除此之外,當我們希望自己來對某些檔案進行存取時,就必須手動控制某些程序。所以現在應該來關心一下,當我們要手動進行這些檔案的控管時,應該怎麼做呢?
10.3.1 開檔/關檔
最基本的,我們要先開啟一個檔案,也就是我們必須將檔案代號和我們想要存取的檔案接上線。首先,我們可以使用open這個指令來開啟檔案代號,並且指定這個檔案代號所對應的檔案名稱,所以我們使用的指令應該會會這樣:
引用:
open FILE, "file.txt";
open OUTPUT, "<output.txt";  # 從檔案輸出
open INPUT, ">input.txt";  # 輸入到檔案
open append, ">>append.txt";  # 附加在現有檔案結尾
其實要開起一個檔案代號非常的容易,至少從上面的例子來看,應該還算是非常的平易近人。那麼我們只需要稍微的解釋一些特殊的部份,大部份的人應該就可以輕鬆的開始使用檔案代號了。
首先,最基本的語法也就是利用open這個指令來結合檔案代號跟系統上實際的檔案。所以我們看到了所有的敘述都是以open接下檔案代號,接著是檔案的名稱。這樣一來,我們就把檔案代號跟檔案名稱連接起來,當然,前提是沒有錯誤發生。不過不管如何,這看起來應該非常容易了。接下來,看看在檔案名稱前面有一些大,小於符號,這些又是甚麼意思呢?這些符號主要在於對於檔案操作需求不同而產生不同的形式。首先我們看到的是一個小於(<)符號,這個符號代表我們會從這個檔案輸出資料,其實如果你對Unix系統有一點熟悉,你會發現這些表示方式跟在一般使用轉向的方式接近。所以當你使用小於符號時,就像把檔案的資料轉向到檔案代號中。如果你可以想像小於符號的方向性,那麼大於符號也就是同樣道理了。大於符號也就是把資料從檔案代號中轉入實際的檔案系統裡,也就是寫入到某個檔案中。而如果系統中沒有這個檔案,Perl會細心的幫你建立這個檔案,然後你透過檔案代號送出的資料就會由Perl幫你寫入檔案中。不過有一個部份必須要特別注意的地方,也就是如果你透過大於符號建立的檔案繫結,Perl會把你指定的檔案視為全新的檔案,就如我們所說的,如果你的系統中沒有這個檔案,Perl會先幫你建立一個新的檔案。不過如果你的系統本來就已經存在同樣的檔名,那麼Perl會把原來的檔名清空,然後再把資料寫入。 當然,這樣就遇到問題了,因為如果你的程式正在監視網站伺服器,而你希望只要伺服器有狀況發生就把發生的狀況寫入日誌檔。這時候你大多會希望保留舊的日誌,那麼如果Perl每次都清空舊的日誌內容就會讓我們造成困擾。這時候我們總會希望Perl能把新的狀況附加在原來的檔案最後面的位置,那麼我們就應該使用兩個大於(>>)的符號,這也就是">>"跟">"的不同之處。
既然你開啟了一個檔案代號,最好的方式就是在你使用完後要歸回原處(從小媽媽就這麼告誡我們)。因此如果你不再使用某個檔案代號時,你最好養成關閉這些檔案代號的習慣,對了,應該還要提醒的是「適時」關閉不需要的檔案代號。雖然Perl會在程式結束時自動幫你關閉所有還開著的檔案代號,不過有些時候,你如果沒有在檔案處理完之後就儘快處理的話,恐怕會有讓系統資源的負擔增加。
至於關閉檔案代號的方式也是非常簡單,你只要使用close這個關鍵字,然後告訴Perl你所要關閉的檔案代號,這樣就沒問題了。因此你如果需要關閉檔案代號,你只需要這麼做:
引用:
close FILE;
沒錯,就是這麼容易。不過卻也相當重要,至少你應該考慮好你自己的系統資源管理。否則等到等到持續拖累系統資源時才要怪罪Perl時可就有失公允了。另外,Perl也會在你關閉檔案代號時檢查緩衝區是否還存有資料,如果有的話,Perl也會先把資料寫入檔案,然後關閉檔案。另外,檔案也可能因為你的開啟而導致其他人無法對它正常的操作,因此盡可能在完成檔案操作後馬上關閉檔案代號是重要的習慣。

10.3.2 意外處理
有些時候,當我們想要開啟檔案時卻會發現一些狀況。例如我們想要從某個已經存在的檔案中讀入某些資料,可是卻發生檔案不存在,或是權限不足,而無法讀入的狀況。我們先看看以下的例子:
引用:
#!/usr/local/bin/perl

use strict;

open FILE, "<foo.txt";
while (<FILE>) {
    print $_;
}
在這裡,我們希望開啟一個檔案"foo.txt",並且從檔案中讀取資料,接著再把檔案內容逐行印出。不過非常可惜,我們的系統中並沒有這個檔案。不過Perl預設並不會提醒你這樣的狀況,而且如果你沒有使用任何的警告或中斷,Perl也能安穩的執行完這個程式,當然結果是「沒有結果」。可是當我們在寫程式,或是使用者在跟程式進行互動時,實在難保這些時候都不會甚麼錯誤會發生,也許只要把檔案名稱打錯,可是Perl卻不會自動的警告你。於是我們應該考慮發出一些警告,讓發生錯誤的人可以即時修正錯誤。當然,你可以使用warnings來讓Perl對於人為的錯誤發生一些警告,不過我們還有另外一種方法可以讓你更輕易的掌握錯誤發生的狀況,也就是讓程式「死去(die)」。
die函式就像他的字面意思,他可以讓程式停止執行,也就是讓程式「死去」。因此當我們希望程式在某些狀況下應該停止執行時,我們就可以使用die函式來達成。而檔案發生問題的狀況則是die函式經常被使用的地方。因為很多時候我們一但開啟了某個檔案,大多就會把操作內容圍繞著這個被開啟的檔案,可是如果檔案其實沒有被正確的開啟,就很容易產生一些難以預料的問題,因此我們可以在檔案開啟失敗時就讓程式停止執行。以剛剛的程式作為例子,我們就可以把開啟檔案的部份寫成:
引用:
open File, "foo.txt" or die "開啟檔案失敗: $!";
在這裡,有幾個地方需要解釋的,首先自然就是die的用法。我們先嘗試開啟foo.txt這個檔案,接著用了一個邏輯運算元'or',後面接著使用die這個敘述。根據我們對or運算符的瞭解,程式會先嘗試開啟檔案"foo.txt",如果成功開啟,就會傳回1,因此or後面的敘述就會被省略。相反的,如果開啟檔案失敗,open敘述會傳回0。如此一來,Perl就會去執行or後面的敘述,因此他就會die了,也就是只執行到這裡為止。
利用die結束程式的執行時,我們會希望知道程式為甚麼進入die的狀況,因此我們便利用die印出目前的情況。這聽起來就像程式說完遺言之後就不動了。而die的列印就跟我們一般使用print沒甚麼不同,因此我們可以加上可以提醒程式寫作者或使用者的字串。不過在剛剛的例子,我們看到了一個不尋常的變數:"$!"。這是Perl預設的一個變數,他會儲存系統產生出來的錯誤訊息。因為當我們透過Perl要進行檔案的存取時,其實只是透過Perl和作業系統進行溝通,因此一但Perl對作業系統的要求產生失敗的狀況,他便會從作業系統得到相關的錯誤訊息,而這個訊息也會被存入$!這個變數中。
所以如果我們執行剛剛改過的那個程式,就可以得到像這樣的結果:
引用:
[hcchien@Apple]% perl ch3.pl
開啟檔案失敗: No such file or directory at ch3.pl line 5.
因為檔案不存在的原因,導致這一支Perl程式無法繼續執行而在執行完die之後就停止了。而且die這個指令也在我們的要求下,傳達了系統的錯誤訊息給我們,問題發生在你要開啟檔案時卻沒有發現這個檔案或資料夾。所以利用die這個指令,你就可以在程式無法正確開啟檔案時,就馬上中斷程式,以避免不可預知的問題產生。
既然提到die,我們就順便來談一下die的親戚,"warn"吧!當你發生一些狀況,可能導致程式發生無法正常運作時,你會希望使用die來強制中斷程式的執行。可是有些時候,錯誤也許並沒有這麼嚴重,那麼你就只需要發出一些警告,讓執行者知道程式出了一點問題,讓他們決定是否應該中斷程式吧!我門把剛剛的程式改成這樣:
引用:
#!/usr/local/bin/perl

use strict;

open FILE, "<foo.txt" or warn "open failed: $!";
while (<FILE>) {
    print $_;
}

print "程式在這裡結束了\n";
你應該發現了,我們把die改成了warn,然後最後加了一行列印的指令,告訴我們程式的結尾在那裡。接下來我們來試著執行這支修改過的程式,你會看到這樣的結果:
引用:
[hcchien@Apple]% perl ch3.pl
open failed: No such file or directory at ch3.pl line 5.
the end of the script
10.3.3 讀出與寫入
在我們可以正確的開啟檔案代號之後,接下來我們就可以開始存取檔案中的資料,當然最主要的就是讀取,以及寫入檔案。
透過檔案代號來讀取檔案內容倒是不太有甚麼困難。我們大多使用鑽石符號(<>)來進行檔案內容的讀取。所以我們可以像這樣進行檔案操作:
引用:
#!/usr/local/bin/perl -w

use strict;

open LOG, "/var/log/messages";          # 打開這個日誌檔
while (<LOG>) {                          # 利用鑽石符號讀入資料
    print if (/sudo/);                  # 符合比對的資料就列印出來
}
看起來非常容易,不是嗎?
我們先用剛剛瞭解的方式開啟了一個檔案代號,並且利用這個檔案代號聯繫到檔案"/var/log/messages"。在一些Unix系統中也許會看到這個檔案,它會紀錄一些使用者登入或是使用root權限的消息。而在這個檔案中,如果有使用者利用sudo這個指令進行某些操作時也會被記錄下來。因此我們就可以透過這個檔案知道伺服器上有些甚麼狀況正在發生。
接下來我們透過鑽石符號開始逐行讀取日誌檔案中的資料,透過迴圈while讀取檔案中的資料時,while會把所讀到的資料內容放進Perl的預設變數$_中,一直到檔案結束,傳回EOF時,迴圈便會結束。因此我們就將所讀取的資料進行比對,以sudo這個關鍵字作為比對樣式,把符合的結果印出來。
這樣一來,只要系統中有人使用sudo進行系統操作時,我們就可以檢查出來,而且印出來的結果會像是這樣:


如果你是負責管理一些Unix的伺服器,利用這樣簡單的方式,確實可以幫忙你完成不少工作。很顯然,利用檔案的操作,你還可以進行更多對日誌檔案的分析。例如你可以分析網站伺服器的各項資料,雖然其實已經有很多人用Perl幫你完成這樣的工作了。(註二)
基本上,從檔案內讀取內容的方式就是這麼容易,因此你可以簡單的運用檔案的內容進行所需要的工作。還記得我們在介紹open時的說明嗎?我們有幾個開啟檔案的方式包括了幾種描述子,例如大於(>),小於(<),以及兩個大於(>>)。而且我們也都簡單的描述過他們的差異,現在也許就是測試這些描述子的好時機,我們先來看看小於符號用於開檔的時候,會有甚麼影響。
我們之前也提過小於符號用在開檔作為描述的話,是用來表示從檔案內讀取資料。那我們是不是就只能允許使用者讀取資料呢?先來看看這個小小的程式吧:
引用:
open LOG, "<log.txt" or die $!;
while (<LOG>) {
    print $_;
}
print LOG "write to log" or die $!;
假設我們已經有了"log.txt"這個檔案,否則程式就會掛在中間,沒辦法繼續執行。那麼我們來看看執行結果吧:
引用:
file for log
Bad file descriptor at ch3.pl line 9, <LOG> line 1.
第一行就是原來log.txt裡面的內容,我們可以很輕鬆的讀出其中的資料,並且印出來,可是當我們要將資料寫入時,卻出現了錯誤訊息。沒錯,當初我們在開啟這個檔案時,只要求Perl給我們一個可以讀出資料的檔案,如今要求寫入,果然就遭到拒絕。
看來一但我們使用了小於符號作為開啟檔案代號的描述子,那麼我們就不能輕易的把資料寫入所開啟的檔案中。想當然爾,Perl應該也不會讓我們在開啟一個利用大於符號指定為寫入的檔案中把資料讀出吧?要想測試這樣的結論,我們只需要把剛剛的程式修改一個字元,也就是把小於符號改成大於,那麼就讓我們來看看執行後的結果吧:
我們嘗試著執行被我們修改了一個字元的程式,結果發生了甚麼事呢?檔案沒有輸出任何結果。好像很出乎意料?其實一點也不,而且正如Perl所要求我們的,我們使用了大於符號表明我們想要把資料寫入檔案log.txt,因此當我們想要從檔案讀取資料並且逐行印出結果時就無法成真。不過我們接下來去看看log.txt的內容。正如我們所預料的,程式已經正確的把字串"write to log"寫到檔案log.txt裡面了。
既然使用大於符號跟小於符號都符合我們的期待,那麼如果我們甚麼描述子都沒有使用,會是甚麼樣的情況呢?我們只需要使用剛剛的測試程式,並且把描述子全部取消,再來試試結果如何吧!
結果我們發現,Perl還是可以讀出檔案的內容,可是卻無法寫入。也就是跟我們使用小於符號時是一樣的狀況,這點其實對於經常必須使用檔案的人來說其實是非常重要的。所以如果你有機會使用檔案的存取時,可別忘了這一點。
另外,大於符號與兩個大於的差別我們也曾經提過,這部份對於可能使用Perl來進行日常管理工作的人更是必須牢記。我們之前提過,一樣是開啟一個可以寫入的檔案,使用一個大於符號(>)的時候,Perl會判斷你是否已經有存在這個檔名的檔案,如果檔案已經存在,那麼Perl將會清空檔案內容,把他視作一個新的檔案來進行操作。如果在系統中檔案並不存在,那麼Perl就會跟系統要求開啟一個新的檔案。當然,在你使用兩個大於符號的時候,Perl會把你要寫入檔案的內容以附加的方式存入。當然,如果你的系統中並沒有這個檔案,那麼Perl也會先開啟一個新檔,並且把你所要求的內容寫入檔案中。這對於想要建立類似日誌檔的需求有著絕對的幫助,例如你可能會需要Perl來作為監控網路的狀況,這時候你會需要每次有新狀況時就把它記錄下來,而且需要保留原來的紀錄。那麼如果你還是使用大於符號的話,你可就要小心原來的資料內容遺失了。
當然,我們知道開啟檔案時可以利用三種描述子去指定所要開啟檔案代號的狀態,不過如果你甚麼都沒加的狀況下,Perl又會作怎麼樣的處理呢?我們繼續用剛剛的例子來進行實驗吧。我們把開啟檔案的描述子拿掉,其他的部份一切照舊。所以你的程式就像這樣:
引用:
open LOG, "log.txt" or die $!;
print LOG "write to log\n" or die $!;
接著我們發現,這樣的結果就跟我們使用小於符號的效果是相同的,也就是Perl只會從檔案中讀出資料,卻無法寫入。

有了基本讀寫檔案的能力之後,我們還必須瞭解該怎麼樣透過Perl去控制系統的檔案以及資料夾。這樣才能確實掌握系統的檔案管理,尤其當你希望使用Perl來進行系統管理時,也就會更需要這樣的能力,所以我們接下來就要討論利用Perl對檔案系統的操作。

習題:
1. 試著將下面的資料利用perl寫入檔案中:
引用:
Paul, 26933211
Mary, 21334566
John, 23456789
2. 在檔案中新增下列資料:
引用:
Peter, 27216543
Ruby, 27820022
3. 從剛剛已經存入資料的檔案讀出檔案內容,並且印出結果。

註一:不過當你打算這麼作的時候,也許要考慮這支程式未來只有你在維護,否則你這樣的動作很可能會因為接下來維護的人需要花更多的時間來看懂程式而提高不少維護成本。
註二:其實跟這章主題不太有關,不過例如awstats就是這類型的工具。

TOP

11. 檔案系統
上一章我們提到了一些關於在Perl當中使用檔案代號來進行檔案存取的工作,不過要能靈活運用這些操作,你應該要有對於系統本身的檔案架構有一些認識。因為運用檔案代號,實際上你也是在操控整個系統的目錄跟檔案。所以我們接下來就要簡單提醒大家一些基本的事項,並且告訴大家應該怎麼利用Perl去進行檔案的操作。
11.1 檔案測試
我們在上一章曾經嘗試打開一個檔案,並且從檔案內讀出其中的內容。不過我們也遇到了一些問題,也就是檔案可能會因為不存在而使資料讀取發生問題。因此我們利用die的方式來判斷,假如程式無法打開這個檔案代號,那麼就中止程式繼續進行。當然,找不到檔案是我們設法開啟檔案代號時可能發生的錯誤之一。我們也許還可能發生其他問題,比如沒有權限打開指定的檔案等等。不過對於這當中的某些狀況,我們其實在準備開啟檔案時可以先進行測試,也就是所謂的檔案測試,在說明可以進行測試的項目之前,我們可以先來看看這個例子:
引用:
#!/usr/local/bin/perl -w

use strict;

my $logfile = "/var/log/messages";  # 先指定檔案到變數 $logfile
if (-e $logfile) {                  # 判斷檔案是否存在
    open LOG, $logfile or die "$!";  # 開啟檔案代號
    my $line_num = 1;
    while (<LOG>) {
        print "$line_num\t$_\n";
        $line_num++;
    }
} else {
    print "檔案不存在\n";
}
這個程式的主要工作在於讀出系統日誌檔的內容,並且幫忙加上行號印出。當然,我們先指定要開啟的檔案名稱是"/var/log/messages"這個檔案,接下來便利用檔案判斷的參數"-e"來確定檔案是否存在。如果這個檔案確實存在,我們就打開檔案代號"LOG",用來聯繫"$logfile"這個檔案,也就是"/var/log/messages"。當然,這時候我們雖然確定檔案存在,可是因為還是可能存在其他導致無法正常開啟檔案的狀況,因此我們還是決定一但開啟失敗就利用die印出錯誤訊息,然後中斷程式。如果檔案開啟沒有問題,我們就可以開始一行一行把資料讀進來,然後加上行號後輸出了。
這樣看來,"-e"的判斷似乎功用不大,因為我們判斷如果檔案不存在,好像也沒有特殊的動作。所以我們來讓"-e"看起來能有些幫助:
引用:
#!/usr/local/bin/perl -w

use strict;

while (-e (my $logfile = shift)) {  # 判斷檔案是否存在
    open LOG, $logfile or die "$!";  # 開啟檔案代號
    my $line_num = 1;
    while (<LOG>) {
        print "$line_num\t$_";
        $line_num++;
    }
}
這樣看起來好像有趣了一些,我們來看看到底改了甚麼。首先,我們把原來指定給變數$logfile的檔案取消,讓$logfile變成是使用者由執行時輸入的參數。接著我們依然檢查了這個檔案是否存在,如果存在則打開並加上行數印出。
其實並不困難,我們只需要以指定的參數就可以用來檢查檔案的屬性。所以我們來看看到底有那些參數可以使用:
引用:
-A  檔案上次存取至今的時間
-B  檔案被判斷為二進位檔
-C  檔案的 inode 被更改至今的時間
-M  檔案上次修改至今的時間
-O  目前實際使用者是否為該檔案或目錄的擁有者
-R  目前實際的使用者具有讀的權限
-S  檔案代號是否為 socket
-T  檔案判斷為文字檔
-W  目前實際的使用者具有寫的權限
-X  目前實際的使用者具有執行的權限
-c  字元型檔案
-e  檢查檔案或目錄是否存在
-f  判斷檔案是否為文字檔
-g  檔案或目錄具有 setgid 屬性
-k  檔案或目錄設定了 sticky 位元
-l  檔案代號是一個符號連結
-o  目前的使用者是否為該檔案或目錄的擁有者
-r  目前的使用者具有讀的權限
-s  檔案或目錄存在而且有內容
-t  檔案代號是 TTY 裝置
-u  檔案或目錄具有 setuid 屬性
-w  目前的使用者具有寫的權限
-x  目前的使用者具有執行的權限
-z  檔案或目錄存在而且沒有內容
其中有許多是關於系統本身的相關知識,例如使用者id,群組id等等。這部份建議各位應該能夠針對自己所使用的作業系統,去找到相關的參考書籍。其他例如在Unix系統上使用,大多則採用相類似的權限判斷方式。當然,其中有些部份是僅供參考,例如檔案是否為文字檔,或是二進位檔。對於big5檔案來說,Perl就可能會誤判成二進位檔。
當然,很多時候我們還是需要在對檔案進行存取之前,先確定他們相關的狀況。例如是否能夠有足夠的權限,或是我們可以得到檔案最後被修改的時間等等。大部份的時候,這些判斷可以給我們當作很好的參考。例如我們可以設定時間清除過久沒有更新的檔案等等。這些工具對於使用Perl來管理日常工作的管理者來說更是能夠提供非常好的幫助。

11.2 重要的檔案相關內建函式
對於系統中的檔案系統,Perl大多數的時候總是透過底層的作業系統去進行操作,因此你會發現很多的函式和作業系統提供的函式大多非常接近(註一)。這樣其實也非常能夠幫助使用者用簡單的方式記憶,而不需要多背另一套指令函式。例如我們剛剛提到的檔案測試,也就是Perl所提供第一個屬於檔案操作的函式。因此如果你想要獲得更精準的說明,你可以考慮使用"perldoc -f -X"來查看所有的測試符號。
接下來,我們來看看還有那些函式是我們可以善加利用的部份。Perl在處理檔案代號或其他檔案相關的函式多達十幾個,其實已經足以應付大多數的使用。接下來我們將挑出幾個經常被使用的內建函式,讓讀者可以開始熟悉該怎麼在Perl中控制檔案系統。

chdir:就像你在大多數作業系統下所使用的指令一樣,你可以利用chdir來切換目前工作的目錄。因此我們可以使用下面的方式來指定我們想要操作的目錄:
引用:
chdir "/tmp" or die $!;
open LOG, ">log.txt" or die $!;
print LOG "write to log\n" or die $!;
沒錯,我們只是小小的修改了剛剛的程式,把原來沒有指定目錄的狀況,改成在目錄"/tmp"下了一個檔案log.txt,並且寫入字串。就像你在大多數Unix作業系統中的狀況,你也可以單獨使用chdir而沒有附帶任何的參數,這時候系統會根據你的環境變數$ENV{HOME}來決定應該切換到哪一個目錄。

chmod:對於熟悉Unix的系統對於此應該也是非常的熟悉,這個函式就是呼叫系統中chmod的操作,來修改檔案或是目錄的權限。如果你對於系統的權限結構還不太熟悉,建議你先看一些相關的文件,可以瞭解Unix系統下對於權限的限制跟實作的方式。當然,Perl並不太願意改變大家的使用習慣,所以如果你經常使用Unix下的chmod指令,那麼你可以繼續你的使用習慣,就像這樣:
引用:
chmod 0444, 'log.txt';
不過也有比較具有彈性的用法,例如你可以這樣使用:
引用:
$mode = 0644;
chmod $mode, 'log.txt';
有些部份通常會讓你搞錯,因此你必須特別注意。如果你剛剛把$mode這個變數寫成下面的形式,那麼可能執行之後,可能會發生一些讓你意想不到的狀況。
引用:
$mode = "0644";
chmod $mode, 'log.txt';
我們直接來看看實際的狀況吧!它的權限目前是0444。如果我們想要把它利用剛剛的權限來修改它,那麼會發生甚麼事呢?
引用:
Inappropriate file type or format at ch3.pl line 6.
Perl毫不留情的給了我們一個錯誤訊息,告訴我們這樣指定檔案權限是不被允許的。很多人可能已經一頭霧水了,我們加了引號之後到底有甚麼差別呢?你還記得我們剛剛指定權限的作法嗎?
引用:
$mode = 0644;
其實當你在使用這樣的純量賦值時,Perl會把你所指定的數字設定為八進位。可是當你幫它加上引號之後,也就是使用了$mode = "0644"後,它就變為一個字串了。可是chmod所需要的可不是字串,而是一個八進位的數字,所以如果你使用了引號來定義權限的值,別忘了把他轉為八進位制,所以我們可以改寫成這樣:
引用:
$mode = '0644'; chmod oct($mode), 'log.txt';
當然,最省力還是前一種的方式,不過既然方法不只一種,使用者可以選擇自己最容易接受的方式。至於直接使用八進位的變數定義,應該是最被推薦的使用方式。不需要繁雜的轉換手續,也減少打字跟錯誤的機會。

chown:修改檔案或是資料夾的擁有者也是你在管理Unix系統會遇到的狀況。其中這包括了使用者id(uid)跟群組id(gid),使用的方式則是將使用者id跟群組id利用串列的方式來描述,配合上想要修改的檔案,所以指令的形式應該是:
引用:
chown LIST;
用實際的例子來看,我們則可以寫成這樣:
引用:
chown $uid, $gid, 'log.txt';
至於後面的檔案,則可以利用串列的方式表示,或直接以陣列方式。也就是說,你當然可以用這樣的方式來表達chown的形式:
引用:
chown $uid, $gid, @array;
直接使用陣列確實是有相當的好處,我們可以利用樣式比對找出我們要的所有檔案,然後一次進行相關的修改。例如在系統的使用上,我們常常使用星號(*)作為萬用符號,比如你可以利用這樣的方式找出所有Perl的檔案:
引用:
ls *.pl
而在Perl中,也有相關的用法,也就是glob。因為這個功能非常重要,所以我們接下來就來看看glob的用法。

glob:他的語法其實相當簡單,也就是利用glob接上一個樣式,作為比對的標準。所以你可能會這麼使用:
引用:
@filelist = glob "*.pl";
這樣的方式就跟你在系統下尋找符合某些條件的檔案用法一樣,所以你可以把利用glob所傳回來的檔案串列放入一個陣列之中。然後再針對這個陣列進行chown或是chmod相關的操作。也許你會考慮,這樣的作法跟你在shell底下的運作有甚麼差別嗎?其實很多時候,Perl可以利用這些方式把你日常必須重複進行的工作處理掉。
不過其實有時候你也許會看到某種寫法,就像這樣:
引用:
@filelist = <*.pl>;
這樣得出的結果其實跟你使用glob有著異曲同工之妙,也就是取得目前目錄下的檔案,並且依據你所描述的樣式傳回你需要的檔案。因此你可以輕易的取得你想要的檔案,例如你想要印出目錄下的所有附屬檔名是txt的檔案,那麼你只需要這麼作:
引用:
for $file (<*.txt>) {
    open FH, $file;
    print $_ while (<FH>);
}
看出這其中有一些奧妙了嗎?我們利用角符號代替了glob的工作,可是同時角符號也被我們拿來作為讀取檔案內容的運算。確實是如此,那麼Perl會如何分辨其中的差別呢?其實由於檔案代號必須符合Perl的命名原則,因此Perl可以藉此判斷你目前的語境下是裡是用角括號來處理檔案代號或是進行glob的處理。當然,其中會有一些例外,比如你用這樣的方式來表達檔案代號:
引用:
open FH, $file;
$filehandle = "FH";
print $_ while (<$filehandle>);
這時候角括號裡面放的其實是一個Perl的純量變數,不過這個純量變數卻是被指定到另外一個檔案代號,所以Perl還是會以對待檔案代號的方式來對待它。這應該一點都不讓人意外,不過你現在應該可以應付大多數的狀況了。

link:你有時候會需要把檔案建立起鏈結,在系統底下,你可以直接使用"ln"這個指令來達成你需要的目的。而透過Perl,則可以利用link來達到一樣的工作。他的語法就像這樣:
引用:
my $res = link "/home/foo", "/home/bar";
這樣的意思就是把"/home/foo"這個檔案連結到"/home/bar",或者你可以說"/home/bar"是"/home/foo"的一個連結。至於link這個指令則會有回傳值,如果連結成功,則回傳值為真值,相反的,如果連結失敗,則會傳回偽值。我們來試試這個例子:
引用:
#!/usr/local/bin/perl

use strict;

my $ret = link "log.txt", "log.bak";
open FH, "log.bak" or die $! if ($ret);
print $_ while (<FH>);
執行之後,我們就可以看到資料夾中多了一個叫做"log.bak"的檔案,不過如果你需要真正瞭解他的運作,我們還是建議你去看看關於Unix下關於檔案及資料夾的解釋,其中inode這個觀念可以幫助你確實瞭解這樣的連結所產生的意義。不過在這裡,我們就暫且先不深入的探討Unix下的相關部份。

mkdir:接下來,我們應該來告訴大家,該怎麼開啟自己的一個資料夾。這個指令跟你在Unix底下的使用非常接近,你只需要使用這樣的方式就可以了:
引用:
mkdir PATH;
這看起來跟你在命令列下的用法一模一樣,而且就是這麼簡單。所以你幾乎不需要學習新的東西就可以很輕鬆的在Perl底下新增一個資料夾。另外,你還可以透過umask來指定這個新資料夾的權限。而用法也是跟剛剛類似,唯一的差別只是把你希望指定的umask放在敘述的最後。所以看起來應該就像這樣:
引用:
mkdir PATH, umask;
所以你可以把新增加的這個資料夾指定某個特殊的權限,例如你希望開一個所有人都可以任意存取的資料夾,那麼就可以這樣寫:
引用:
mkdir foo, 0777;
rename:接下來我們來看看如何使用Perl來幫你的檔案改名字,其實當你開始利用Perl來對檔案進行操作時,修改檔名是非常有用的一項工具。我們可以先來看看一個實際的範例:
引用:
my $file = "messages.log";
if (-e $file) {
   rename $file, "$file.old";
}
open FH, ">$file";
print FH, "接下來就可以寫入資料";
在實際運用時,如果我們可以適時的搭配檔案的測試運算,那就可以產生出很不錯的效果。就這個例子,我們先利用"-e"來判斷檔案是否存在。如果檔案存在,我們就把檔案更名,也就是再檔案結尾加上".old",在這裡,我們就看到了rename 的用法,也就是:
引用:
rename $oldfile, $newfile;
當我們正確的把檔案改了名字之後,就可以安心的把新的資料寫入檔案了,你應該注意到了,我悶在這裡因為是使用了大於(>)符號來進行開啟檔案代號的動作,所以如果之前沒有先把檔案更名,那麼舊有的資料就會被取代了。

rmdir:就像你在作業系統下的作法一樣,你可以利用rmdir來刪除一個資料夾。不過也跟你在終端機前使用rmdir一樣,如果資料夾裡面還有存在其他檔案,rmdir就會產生失敗,而且會傳回偽值,很顯然,這是相對於刪除成功所傳回的真值。所以如果你是Unix系統的慣用者,也許你應該非常熟悉這個函式,你只需要這麼指定:
引用:
rmdir FILENAME;
stat:其實如果你想要更靈活的使用我們介紹的這些函式來對檔案系統進行控制時,你應該要先瞭解stat這個重要的函式。為甚麼stat這個函式這麼重要,也許我們來看看下面的範例就能夠很快的瞭解了:
引用:
my @ret = stat "log.txt";
print "$_\n" for (@ret);

於是我們試著執行這個程式,會看到這樣的結果:

234881034          # 裝置編號
1183005                  # inode 編號
33060                  # 檔案模式(類型及權限)
1                  # 檔案的連結數目
501                  # uid
501                  # gid
0                  # 裝置辨識
17                  # 檔案大小
1078894964          # 最後存取時間
1078894638          # 最後修改時間
1078939576          # inode 修改的時間
4096                  # 檔案存取時的區塊大小
8                  # 區塊的數目  
毫無疑問,我們確實可以利用stat個函式得到相當多的檔案相關資訊,因此如果你想要對檔案進行操作之前,也許可以先利用stat來得到相關的訊息。
我們剛剛利用一個陣列來儲存stat的回傳值,這樣也許不容易分辨各個值所代表的意義,所以你當然可以改用這樣的方式來取得相關的資料:
引用:
($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,
$blocks) = stat("log.txt");
另外,有些時候你也許會看到有人使用lstat來取得檔案的相關資訊,不過基本上這兩個函式所進行的工作應該是一樣的,所以除非你想要多打一些字,否則還是可以直接使用stat就好了。

unlink:就像你使用的rm一樣,unlink也可以讓你刪除系統中的某些檔案。而且unlink的用法十分簡單,基本上就是傳進你想要刪除的檔案串列。意思就是說,如果你搭配著glob或是角括號(<>)使用,那麼你就可以過濾出某些特殊的檔案,並且加以刪除。相信大家經過上面幾個函式的訓練,應該可以很輕易的使用這個函式,就像這樣:
引用:
my @files = <*.txt>;
unlink @files or die $!;
當然,別忘了要刪除檔案千萬要非常的小心,可別因為一時大意就把資料全部的毀了(註二)。當然,我們剛剛說了,在unlink後面所連接的參數是一個串列,所以你可以使用任何表達串列的方式,其中當然包括一一列出你所要刪除的檔案。所以如果有一個程式寫的像這樣:
引用:
#!/usr/bin/perl
use strict;
unlink @_ or die $!;
那麼他看起來像不像陽春的rm指令呢?其實有時候玩玩也是還滿有趣的。

utime:Perl另外也提供了一個讓你修改檔案時間的函式,也就是utime。utime的用法也是傳入一個串列,所以基本上會是:
引用:
utime LIST;
不過不太一樣的地方在於你必須指定你所要修改的時間參數,所以其實比較常看到的用法也許會比較接近這樣的形式:
引用:
utime $atime, $mtime, @files;
其中第一個參數就是檔案存取時間,第二個參數就是檔案最後一次修改的時間。

11.3 localtime

這個函式看起來跟檔案操作並沒有甚麼直接的關係,不過我們剛剛看到了一些不太友善的數字,也就是對於檔案相關時間的描述。例如我們利用stat傳回來的日期都是這樣子的表示方式:
引用:
1078894964          # 最後存取時間
1078894638          # 最後修改時間
1078939576          # inode 修改的時間
這時候,我們就可以使用localtime來轉換成一般人可以接受而且使用的資訊。localtime會傳回一個串列,分別代表用來表示時間的各個欄位,所以你可以利用這樣的方式取得你需要的欄位:
引用:
@realtime = localtime($timestamp);

只是如果你使用這樣的方式,恐怕自己也很難很快的使用,所以也許可以換一個方式:

($sec, $min, $hour, $day, $mon, $year, $wday, $yday, $isdat) =
localtime ($timestamp);
所以如果你想要取得檔案最後修改的正確時間,你可以利用下面的方式達成:
引用:
my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,
$blocks) = stat("log.txt");
($sec, $min, $hour, $day, $mon, $year, $wday, $yday, $isdat) =
localtime ($mtime);
呼,確實有一點冗長。不過確實可以讓你正確的取得大部份的資訊。

習題:
1. 列出目前所在位置的所有檔案/資料夾名稱。
2. 承一,只列出資料夾名稱。
3. 利用Perl,把目錄下所有附檔名為.pl的檔案修改權限為可執行。


註一:當然所謂的接近,指的是和Unix系統的接近。
註二:上面的程式就讓本書內容差點付之一炬,幸好作者使用了版本控制系統來進行備份。

TOP

第十二章 字串處理

我們前面兩章提到了許多關於檔案的操作,現在我們應該可以很輕鬆的從檔案中取得我們需要的資訊了。不過如果只是空有一大堆的資訊,卻沒有辦法處理的話,只怕也沒有甚麼幫助。不過既然對於Perl來說,大多數的東西都是由數字跟字串組合而成,那麼一但我們可以用簡單的方式來整理字串的話,那麼應該就可以讓這些資訊變得相當有用。

12.1 簡單的字串形式
我們在講解變數的時候已經提過關於Perl是如何對待字串的,雖然對於不少其他程式語言來說,字串其實只是字元的串列。可是Perl卻簡化了這樣的觀念,因此反而讓字元變成長度為一的字串。就像對待數字一樣,Perl並不會要求程式設計師去強制規定某些變數只能放整數,某些變數只能放浮點數。
這樣寬鬆的規定確實讓程式設計師省了許多麻煩,不過當你的分類越粗略時,要怎麼有效的對這些資料進行處理就顯得更加重要。而就像許多人對於Perl的印象,它在處理字串時非常的具有威力。這當中的原因除了正規表示式之外,Perl對於字串的控制顯然也有一些有趣的部份。
對於字串最基本的操作應該就是長度了,我們經常會要求知道字串的長度,這時候,只需要使用length這個函式就可以取得你所指定字串的長度了。
引用:
my $string = "string";
print length($string);          # 長度是6
當然,有時候你會被某些控制字元所欺騙,因為他們也是佔有長度的,就像這樣:
引用:
my $string = "string\n";
print length($string);          # 這時候長度變成7了
取得字串長度只是第一步,接下來我們可能需要找出字串的相關性。例如我們會想要取得某個字串的其中一段。不過我們可能會先需要知道這個子字串在原來字串的位置這時候就可以使用index來取得相關的訊息了。用實際的例子我們可以很容易看到index的用法:
引用:
my $mainstring = "Perl Mongers";
my $substring = "Mongers";
print index($mainstring, $substring);  # 印出 5
看來相當容易對吧,Perl會告訴你子字串第一個字母所在的位置,只是字串是由0開始算起。也就是說,如果你在字串的一開始就找到符合的子字串,那麼Perl就會傳回0。不過如果Perl發現你所指定的子字串不在原來的字串中,那麼就會傳回-1。
當然,有些人會關心中文字串的處理,我們先來試試下面的例子:
引用:
my $mainstring = "台北Perl推廣組";  # Big5 碼的中文
my $substring = "推廣組";
print index($mainstring, $substring);  # 印出8
其實index傳回的是位元,所以如果你要利用index來找到某些中文字在在字串中是位於第幾個位元那麼就沒有問題。當然,如果你要的是以中文的角度來看,那麼「字」的觀念在這裡顯然並不存在。

另外,就像正規表示式一樣,index在進行比對時,也會確定找到你所需要的字串就停止了。所以index傳回的就永遠會是第一次找到子字串的位置。實際的結果會像是這樣:
引用:
my $mainstring = "perl training by Taipei perl mongers";
my $substring = "perl";
print index($mainstring, $substring);          # 結果是0
因為Perl在一開始就找到了比對成功的字串"perl",因此它馬上傳回0,然後就停止比對了。可是這樣有時候是不是會有些不方便呢?所以我們來看看Perl對於index這個函式的描述。
引用:
index STR,SUBSTR,POSITION
index STR,SUBSTR
我們好像發現一些署光,沒錯,根據上面的語法,其實我們還可以使用index的第三個參數,也就是位置。所以你可以要求index從第幾個位元開始找起,例如:
引用:
my $mainstring = "perl training by Taipei perl mongers";
my $substring = "perl";
my $first = index($mainstring, $substring);          # 先找到第一次出現perl的地方
print index($mainstring, $substring, $first+1);          # 接下去找
這樣的用法就可以讓你找到下一個出現子字串的地方。當然,如果你沒有加第三個參數的話,那麼index會把它預設為0。也就是我們一開始一直使用的方式。
不過如果你不知道子字串會出現多少次,可是你又想找到最後一次出現的位置,那麼你會想要怎麼作呢?用個迴圈好像是我們目前可以想到的作法,所以我們就來試試吧:
引用:
my $mainstring = "perl training by Taipei perl mongers";
my $substring = "perl";
my ($pos, $current);
my $pos;
my $current = -1;
until ($pos == -1) {                                    # 到找不到正確字串為止
    $pos = index($mainstring, $substring, $current + 1);  # 從上次找到的位置往下找
    $current = $pos unless ($pos == -1);
}

print $current;                                            # 印出 24
看起來好點小小的複雜,因為我們必須用一個迴圈去搜尋所有的子字串,一直到它找到最後一個。不過有沒有可能從字串的尾端去找,那麼我們就只需要找到第一個符合的字串,因為對於從字串開頭而言,那就會是最後一次比對成功的子字串了。
看來這樣的需求不少,因此Perl的開發者也就提供了另外一個函式,也就是rindex,基本上rindex的使用方式跟index幾乎一模一樣,只不過它是從字串尾端開始找起。既然如此,我們就改用rindex來完成剛剛的工作:
引用:
my $mainstring = "perl training by Taipei perl mongers";
my $substring = "perl";
print rindex($mainstring, $substring);          # 同樣印出 24
這樣顯然方便了許多,不過對於rindex來說,如果我們指定了第三個參數,那其實是用來表示搜尋的上限。也就是我們要求rindex在某個位置之前的就不找了。這樣描述似乎太過籠統,我們不如來看看實際的運作情形吧:
引用:
my $mainstring = "Taipei perl mongers";
my $substring = "perl";
print rindex($mainstring, $substring, 4);  # 結果傳回 -1
其實參數的意義也就是「以這裡為開始搜尋的起點」,所以如果我們把參數設定為4的話,Perl就只會從第四個位元往回進行比對,所以當然不會比對成功。

利用index找出子字串的位置之後,我們還可以利用substr來取出某個字串內的子字串。我們先看看substr的標準語法:
引用:
substr EXPR,OFFSET,LENGTH
substr EXPR,OFFSET
最簡單的方式就是只有指定要處理的字串跟另一個我們想取得子字串的起始點,所以你可以讓它看起來像這樣:
引用:
my $string = "substring";
print substr($string, 3);  # 果然印出 string 了
如果你沒有傳入長度這個參數,那麼Perl會預設幫你取到字串結束。所以我們剛剛取得的字串就是"string",如果你想要的只是"str"三個字母,你就可以指定長度,也就是像這樣:
引用:
my $string = "substring";
print substr($string, 3, 3);  # 這樣就只會印出 str
有時候如果字串太長,也許從字串結尾開始算起會比較容易,就像index搜尋子字串的位置,可以利用rindex來要求Perl從字串尾端找起,那麼substr要如何使用類似的方式呢?答案就是利用負數的起始點,這樣說好像不如直接看個範例:
引用:
my $string = "Taipei Perl Mongers";
print substr($string, -12, 4);  #印出 Perl
另外,我們之前使用過正規表示式來進行取代的工作,例如下面的字串,我們想把"London"以"Taipei"取代,所以可以利用正規表示式,作這樣的處理:
引用:
my $string = "London Perl Mongers";
$string =~ s/London/Taipei/;
當然,有些時候使用正規表示式未必比較方便。或是我們可以取得的資料有限,這樣的情況下,也許可以利用substr來進行字串替換。substr也可以進行替換,別擔心,你沒看錯,我們就來實驗看看,利用substr來把"London"換成"Taipei"。
引用:
my $string = "London Perl Mongers";
substr($string, 0, 6) = "Taipei";
print $string;                          # 就會印出 "Taipei Perl Monger"
這樣看起來好像沒甚麼,顯然不夠絢麗,我們來把它改寫一下吧!
引用:
my $string = "London Perl Mongers";
print substr($string, 0, 6) = "New York";
print $string;                          # 你完全不需要考慮字串長度
字串長度對Perl來說並不是個問題,所以我們可以很安心的使用長度不相等的字串來進行替換,Perl可以自動的幫你處理長度的問題。其實這種需求顯然相當的高,所以這也是substr的另一種標準語法,也就是說,我們可以把剛剛的語法用這種方式來取代:
引用:
my $string = "London Perl Mongers";
substr($string, 0, 6, "New York");  # 使用第四個參數
print $string;                          # 也是會替換為 New York Perl Mongers
12.2 uc 與 lc

字串中,偶而會有一些惱人的狀況,也就是字串的大小寫問題。例如你弄了一個會員帳號系統,因此這個系統必須讓管理者可以開帳號,使用者可以登入等等。有許多牽涉到帳號的輸入,比對問題,這時候如果還有字母大小寫的問題,也許會更讓人氣餒,尤其目前的大多數使用者幾乎都習慣了大小寫不分的使用狀況。所以有時候也許需要藉由系統自動轉換的方式來避開這一類瑣碎的事。

uc也就是upper case的意思,所以很清楚的,它會幫你把字串中的英文字母傳換成大寫,然後回傳,就像這樣:
引用:
my $string = "I want to get the uppercased string";
print uc $string;    # 結果就變成了 "I WANT TO GET THE UPPERCASED STRING"
怎麼樣,一點都不意外吧!而且依此類推,lc 則是轉成小寫之後回傳,這應該不需要重新舉例了。
這樣一來,我們雖然可以取得全部大小或全部小寫的字串,可是在更多時候,我們其實只要字首的大小就好了,那麼可以怎麼作呢?也許可以考慮使用ucfirst,看這個函式名稱就覺得它是我們想要的東西。,既然如此,那我們就直接來試一下吧:
引用:
my $string = "upper case";
print ucfirst $string;          # 印出 Upper case
就像我們所預期的一樣,我們讓Perl把第一個字母印出了大寫,不過這完全是意料之中?相對應於ucfirst,Perl也提供了lcfirst這個函式,而且正如大家所猜想的一樣,它會把字串的第一個字母轉為小寫。

12.3 sprintf

我們已經非常習慣使用print來印出我們執行程式所得到的結果了,可是很多時候print印出的結果卻未必讓人滿意,不滿意的原因有很多時候是因為它的輸出格式無法依照我們的要求,或者說我們需要花更多的力氣才能達到我們所期待的樣子。所以這時候,sprintf就可以派上用場了。sprintf主要是可以幫助我們作格式化的列印指令。例如你總是希望印出兩位數的小數點,那麼這時候,你應該就會非常需要sprintf來幫助你。我們來看看我們可以怎麼作呢?
引用:
my $num = 21.3;
my $formatted = sprintf "%.2f", $num;          # 先設定好格式
print $formatted;
當然,sprintf的功能相當的豐富,如果你打算使用的話,應該先來看看sprintf提供甚麼樣的強大功能:
引用:
%%   百分比符號
%c   字元   
%s   字串
%d   包含正負號的十進位整數
%u   不包含正負號的十進位整數
%o   不包含正負號的八進位整數
%x   不包含正負號的十六進位整數
%e   以科學符號表示的符點數
%f   固定長度的十進位浮點數
%X   使用大寫表示的%x
%E   使用大寫表示的%E
其他還有一些不同的格式指定方式,當你開始使用的時候,你可以參考printf的說明文件。

12.4 排序

對於字串的另一個重頭戲,也就是排序了。因為當我們有了資料之後,要怎麼讓資料可以更容易的讓人可以進行檢索,或如何進行有效的整理就是非常重要的議題了,而排序正是這些議題的第一門課程。所謂的排序其實主要在進行的也就是「比較」,「交換」的工作,因此我們可以先從Perl如何交換兩個變數的值來看起。
在傳統的方式,或其他程式語言目前的實作方式還是如此,也就是使用另一個變數來作為暫存的變數。例如我們如果想要把$a跟$b兩個變數里面的值進行交換,那麼可能的作法也許會是這樣:
引用:
$tmp = $a;          # 先把$a的值放進暫存變數
$a = $b;          # 把$b的值指定給$a
$b = $tmp;          # 從$tmp中取得$a原來的值,並指定給$b
可是在Perl當中,我們就可以輕鬆一些了。我們如果要交換兩個變數的值,只需要使用這樣的方式就可以了:
引用:
($a, $b) = ($b, $a);
這樣看起來好像有點差距,可是又相差不大,部過一但變數夠多,你利用其他方式可能只會讓自己變得頭昏腦脹,不然你試著自己弄一個四個變數的狀況,然後用原來的方式寫寫看,我想總還是很難比這樣看起來更方便了吧:
引用:
($a, $b, $c, $d) = ($b, $c, $d, $a);
能夠輕鬆的交換變數內的值之後,我們如果利用排序的結果來決定是否要把兩個正在進行比較的變數值交換,那麼最後就可以完成整個串列的排序。如果你學過某些相關的內容,應該會覺得非常熟悉,這似乎是某種被稱為「泡沫排序法」的方式。當然,你可以使用其他在資料結構那堂課中所學的其他排序,好吧,不過暫時先忘了這些課本上的東西。我們先來看看最基本的排序方式:
引用:
sub my_sort {
  my ($a, $b) = @_;
  ($a, $b) = ($b, $a) if ($a > $b);
  .......                        # 繼續其他運算
}
利用比較,交換的方式,我們似乎完成了一個簡單,可以用來排序的副常式。不過既然每次排序我們都需要這樣的東西,那麼Perl很顯然的,應該會有更簡易的方式。於是我們發現了一個新的運算符:<=>。
有人稱這個符號為太空船符號,確實是有幾分像,那麼它有甚麼便利性呢?我們實際利用這個符號來進行排序吧。這裡還有一個很大的特點,當我們在進行比較時,通常會定義兩個變數來表示正在進行比較的值,很多時候我們都用$a跟$b來代表這兩個值。只不過如果每次我們都需要這兩個變數,那不是很累人嗎?Perl也非常體諒我們打字的辛苦,所以$a跟$b已經被設為Perl排序時的內建變數。意思也就是說,以後如果你在Perl中要進行排序,你不需要自己另外定義這兩個變數。
引用:
my @array = (6, 8, 24, 7, 12, 14);
my @ordered = sort { $a <=> $b } @array;
print @ordered;  
# 結果變成 6, 7, 8, 12, 14, 24
你可能會很好奇,這樣的方式難道不能直接用sort來作嗎?我們之前學過,直接使用sort這個函式來對陣列進行排序。所以現在的狀況應該可以使用同樣的方式來進行排序。那麼何不來試試呢?

好啊,這已經讓我快要一頭霧水了。因為上面的例子實在讓人很想改寫成這樣:
引用:
my @array = (6, 8, 24, 7, 12, 14);
my @ordered = sort @array;
print @ordered;                                                 
# 這次輸出 12, 14, 24, 6, 7, 8
聰明的你可能已經看出排列出來的結果了,沒錯,sort預設會使用字串排列的方式,這時候,我們應該先提示一下sort的語法:
引用:
sort SUBNAME LIST          # 你可以使用副常式
sort BLOCK LIST                  # 或使用一個區塊
sort LIST                          # 這是我們一開始說的方式
因此,如果你沒有指定區塊或是副常式,Perl預設會使用字串的方式去進行排序,也就是我們第二次看到的結果了。那麼如果我要強制Perl使用字串比對,或是針對字串進行比對,那應該怎麼寫呢?你可以參考另一個和<=>相對應的運算符,也就是'cmp',這也就是比較的意思。讓我們直接來試試這樣的比較方式吧:
引用:
my @array = (6, 8, 24, 7, 12, 14);
my @ordered = sort { $a cmp $b } @array;
print @ordered;                                                 
# 這次還是輸出 12, 14, 24, 6, 7, 8
沒錯吧,果然和我們第二次只使用sort的結果是一樣的。特別要注意的就是'cmp'這個東西,如果你要進行字串的排序,可不能使用太空船符號。另外,我們還可以直接進行遞減的排序,而且非常簡單,我們直接利用第一個例子來試試吧:
引用:
my @array = (6, 8, 24, 7, 12, 14);
my @ordered = sort { $b <=> $a } @array;
print @ordered;                                                 
# 遞減排序: 24, 14, 12, 8, 7, 6
其實一但可以利用區塊或副常式來進行獨特的排序方式,我們可以玩出不少其他的花樣。例如你可以對雜湊進行排序,或是比對多個值來進行排序。其中雜湊的排序是非常常用的。尤其我們知道,雜湊的安排是依據系統計算出存取的最佳化方式,因此大多數的時候,我們拿到一個雜湊通常是沒有甚麼順序性。要能夠對於其中的鍵或值排序都是非常重要的,而透過sort的方式,我們就很容易做到了。
引用:
my %hash = (john, 24, mary, 28, david, 22);
my @order = sort { $hash{$a} <=> $hash{$b} } keys %hash;
print @order;                                  # 依序是 david john mary
雖然只有三行程式,不過我們還是應該來解釋一下其中到底發生了甚麼事,否則看起來實在讓人有點頭暈。第一行的問題應該不大,或者說如果你第一行看起來有點吃力,那你可能要先翻回去看看雜湊那一章,至少你應該要懂得怎麼定義一個雜湊,然後指定雜湊的鍵跟值。這裡所用的方式一點也不特別,我們只是用串列來賦值給一個雜湊。最複雜的應該是第二行 (除非你覺得最後一行要印出一個陣列對你而言太過困難),我們先看等號左邊,那裡定義了一個陣列,因為我們希望可以得到一個依照雜湊值排序過的雜湊鍵陣列。這聽來好像不難,讓我們先想像一下,我們該怎麼取得這樣的陣列呢?
首先我們應該先拿到包含所有雜湊鍵的陣列,也就是利用keys這個函式取得的一個陣列。拿到這個陣列之後,我們就可以來進行排序了。排序的重點在於區塊內的那一小段程式。我們還是使用了Perl預設的兩個變數,也就是$a跟$b,分別代表從陣列(keys %hash)拿出來準備比較的兩個數值。部過我們並不是直接對變數$a,$b進行比較,而是以他們為鍵,而取的雜湊值來進行排序。

12.5 多子鍵排序

很多時候,我們會希望排序的根據不單單只是一個單純的鍵值,例如在剛剛的例子中,如果我們希望當排序時,在遇到年齡相同的時候,還能以名字排序,那麼我們就會需要多子鍵排序。另外還有非常常見的就是網路上經常看到的IP,我們如果要按照順序將IP排序,那麼這是沒有辦法依照正常的方式來進行排序的。例如我們看到這些IP:
引用:
140.21.135.218
140.112.22.49
140.213.21.4
140.211.42.8

依照正常字串排序之後會變成:

140.112.22.49
140.21.135.218
140.211.42.8
140.213.21.4
這看起來實在不太對勁,因為是藉由字串的關係,所以21被排在112的後面。所以我們想要作的其實是把每一個部份都拆開來,然後進行數字的比對。所以我們可以這麼作:
引用:
#!/usr/bin/perl -w

use strict;

my @ip = ("140.21.135.218", "140.112.22.49", "140.213.21.4", "140.211.42.8");
my @order = sort ipsort @ip;          # 直接叫用副常式
print "$_\n" for @order;


sub ipsort {
    my ($a1, $a2, $a3, $a4) = $a =~ /(\d+).(\d+).(\d+).(\d+)/;          # 分為四個數字
    my ($b1, $b2, $b3, $b4) = $b =~ /(\d+).(\d+).(\d+).(\d+)/;
    $a1 <=> $b1 or $a2 <=> $b2 or $a3 <=> $b3 or $a4 <=> $b4;          # 進行多子鍵排序
}
這個程式的重點在於兩個部份,第一個部份是直接叫用副常式進行排序。所以我們看到在這裡,我們呼叫了副常式ipsort來幫我們進行多子鍵的排序部份。而且我們一樣可以直接在副常式之中使用預設變數$a,$b。在我們把排序的程式放進副常式之後,我們就開始進行ip的拆解工作,利用正規表示式把每一個ip都拆解成四個部份。所以我們就分別有了$a1...$a4以及$b1...$b4這樣的子鍵。然後利用子鍵來進行排序,並且利用or來作為是否進行下一個子鍵排序的關鍵。因為太空船符號的比較會傳回-1, 0 或是 1,因此如果是0就表示兩者相等,於是繼續比對下一個子鍵。利用這樣排序之後,我們就可以得到這樣的結果:
引用:
140.21.135.218
140.112.22.49
140.211.42.8
140.213.21.4
習題:
1. 讓使用者輸入字串,取得字串後算出該字串的長度,然後印出。
2. 利用sprintf做出貨幣輸出的表示法,例如:136700以$136,700,26400以$26,400表示。
3. 利用雜湊%hash = (john, 24, mary, 28, david, 22, paul, 28)進行排序,先依照雜湊的值排序,如果兩個元素的值相等,則依照鍵值進行字串排序。

TOP

13 模組與套件

Perl之所以可以這麼受到歡迎,除了本身有許多專為懶人設計的語法以及相對於其他程式語言,更接近自然語言的用法之外,豐富的模組資源更是讓Perl能持續維持高人氣的主要因素。而數以千計的模組不但能吸引住眾多的Perl開發者,更能讓這些開發者貢獻出其他的模組,如此一來,便會造成「網絡效應」,而持續讓更多人願意投入Perl的懷抱。
對於Perl的使用者來說,如果你不會使用各式各樣的模組,那麼你對Perl的使用率可能不到十分之一。因此能夠寫Perl的人,可能也因此對於程式碼重用的部份鄉對於其他程式語言的程式設計師有更深的感受。當然,大多數的Perl程式設計師總是需要學會如何開始使用模組,緊接著便會瞭解如果善用模組,找到自己所需要的資源。再下一階段就是如何寫出自己的模組。
可惜很多程式設計師,或是專案管理員對於這方面並不重視,他們總是只看著手邊的東西。而不肯多花時間把手邊的程式碼整理成模組,很多人不相信自己還會重新用到這些程式碼,或者不認為同樣的這些程式碼如果整理成模組,可以讓許多人節省更多的時間。當然,對於這些程式設計師或管理專案的人而言,更不用提要怎麼進行好一個專案,版本控制,分支,合併了。 (註一)
不論如何,一但你開始使用Perl,你應該就必須有足夠的能力去使用各式各樣的模組。而且還必須瞭解模組與套件的結構,因為你可能會需要對於你使用的Perl模組進行除錯的工作,雖然這些事情未必經常發生。不過你從這裡開始,就會開始慢慢的學會如何寫好自己的模組。所以現在就開始來進入的世界吧!
13.1 關於程式的重用
我們之前提到過可以節省程式碼寫作的時間,大幅提昇程式可重用性的方式就是副常式。可是如果你沒有好好管理你的程式碼,等到下一次你需要同樣的函式時,你還是必須重寫一次。當然,很多人這時候就會利用複製,貼上的方法。把原來的副常式複製到新的程式之中,這樣一來,就可以再度使用同樣的副常式了。
可是利用這樣的方式還是會有一些問提存在,就像我們在描述副常式時所說的,當你一再使用複製,貼上這樣的方式時,很容易就會造成管理上的問題。因為你還是沒有辦法統一的管理一個套件,讓你以後只要修改模組,接著就可以一次修正使用相同模組的所有程式。
而不會像你使用拷貝,貼上的方式,你一但找到副常式的一個錯誤,就必須同時修正所有使用這個副常式的程式,當然還可以能因為你忘了某個程式中忘了修改而讓自己踩到地雷。所以既然你都已經使用了副常式,除非你確定某些副常式只會在目前的程式使用,否則他們都有機會成為模組。
另外,當你開始使用獨立的模組之後,你更需要做好檔案的管理,因為你可能會公開給所有的人使用,就像CPAN (Comprehensive Perl Archives Network,Perl綜合典藏網) 上面所有的模組一樣,或是開放給公司內部使用。不管如何,你的程式一但公開釋出之後,就應該考慮使用者的使用性以及如何更新版本,修正錯誤的問題。這其實是非常嚴肅的問題,因為一但沒有辦法做好程式碼的管理,很容易會加重負擔,反而增加管理成本。這個部份對於部份許多公司或個人來說,都還需要更深的著墨,我們應該在附錄用一些篇幅,介紹這個部份,雖然它們並不屬於Perl的範圍之內。
13.2 你該知道的 CPAN
也許你不太同意我們剛剛所說的部份,不過如果你確定你要開始使用Perl來解決生活,或工作上的問題時,你大概很難不先知道,而且學會如何使用CPAN。
剛剛說了,CPAN就是Perl典藏網,可是葫蘆裡到底賣的是甚麼藥呢?其實CPAN上主要的就是上面的許多模組,目前已經有好幾千個模組在CPAN上。所以你可以在上面取得這些模組的原始碼,文件,有些模組可能還提供其他的二進位檔案,讓你在沒有辦法編譯時也可以使用。
目前的CPAN,上面已經充滿了各式各樣的模組,大部份的需求幾乎都可以有現成的模組來解決你的問題,或七成以上的問題。當然,如果你想要在CPAN上找到符合需求的模組確實需要花點功夫,因為浩瀚CPAN大海,既然有各種各樣的模組,雖然你可以使用搜尋的功能,可是如果你完全沒有頭緒,恐怕是需要一點時間來適應CPAN這個圖書館了。不過接下來的章節,我們會在各個部份介紹相關使用的模組,這些模組很大部份的時候幾乎都是你在寫相關程式時一定會用到的模組。另外,我們也會在附錄整理五十個在CPAN上非常有用的模組,這個清單將會包括很多領域的部份模組,相信可以作為一個參考清單及介紹。
接下來,也許你終於花了一些時間找到了一個符合你需求的模組,那麼你在開始使用之前,必須先安裝這個模組。最傳統的方式,你可以下載這個模組的原始碼,然後試著自己編譯。這時候,你可以先到CPAN網站上搜尋你需要的模組。

(圖一),然後下載原始檔,接著就開始編譯,像這樣。

(圖二)
不過這樣確實有點辛苦,尤其Perl的使用上,我們會經常大量的安裝模組,如果每次都要這樣子來一步一步來就會顯得相當吃力。因此我們必然需要更方便的工作來幫助我們完成安裝模組的工具,而在你安裝完Perl之後,其實Perl就會給你一個叫做cpan的工具程式,而他正是幫助你完成大量安裝Perl模組的好幫手。很多時候,你可以直接進入cpan的命令列中。

(圖三)
不過如果你在Win32的作業平台上,CPAN對你的幫助顯然就小的多了,尤其當大多數的Windows使用者並沒有安裝相關可以提供編譯這些模組的編譯器,那麼他們所需要的,應該是能夠提供Windows平台上的二進位檔安裝了。當然,這時候你應該使用由ActivePerl所提供的ppm程式,以便讓你可以容易的安裝Perl模組。
cpan是目前的Perl版本內附的CPAN模組安裝程式工具,不過下一個階段的取代性程式也正在發展當中,而且目前也已經相當穩定,不久之後將會取代cpan,成為Perl內附的工具程式,這個新的工具就是cpanplus。就像這個模組的名稱,他正是cpan的加強版,例如他可以幫你確認目前機器上安裝的模組版本,以及該模組的最新釋出版本,提醒你該升級。你可以輕鬆的解除某個模組的安裝或是下載某個模組,解除安裝等等。

(圖四)
很可惜,截至目前為止,cpanplus還是只能在Unix的環境下執行,主要的問題還是因為編譯器的問題,就如我們剛剛說的,絕大多數的Windows系統中並沒有編譯器,所以如果你的Windows環境下能夠裝起合適的編譯器,當然還是可以使用這些方便的工具。另外,Mac OS X的系統預設也並沒有安裝編譯器,所以如果你希望使用cpan/cpanplus的話,就必須安裝相關的套件。

13.3 使用CPAN與CPANPLUS

如果你只是想要學習Perl的語法,那麼也許你不需要使用CPAN,不過當你要開始利用Perl來完成某些工作,或作業。而且完全不想自己重新開始,那麼學會使用CPAN/CPANPLUS就是一件非常重要的工作了。正如我們所說的,如果你在Linux/*BSD的作業系統中,一般而言,你一但裝好了perl,核心安裝也會自動把CPAN這麼模組安裝進去。所以也就有命令列的執行程式"cpan",如果你是第一次執行cpan,會需要作一些設定,以確定你電腦內的環境以及各種程式的位置。完成設定之後,你會看到提示符號,就像這樣:
引用:
cpan>

這就表示你可以開始使用cpan了。
當然,你可以利用help取得完整的cpan使用說明,不過我們還是就一些常用的功能進行介紹。最常用的大概就是install了,幾乎還無疑問,你可以利用install來安裝需要的模組。所以如果你想安裝CPANPLUS這個模組,就只需要這麼作:
引用:
cpan>install CPANPLUS
利用cpan安裝模組,它會幫你進行完整的步驟,也就是一般我們手動從原始碼安裝時會進行的步驟:
引用:
perl Makefile.PL
make
make test
make install
所以有時候你會在使用cpan安裝的過程中遇到測試不過或其他狀況,這時候你可以使用強迫安裝的方式來要求cpan進行強制安裝。使用的方式也非常簡單,就只要在install時加上force的選項:
引用:
cpan>force install CPANPLUS
另外,如果你只想下載某個模組,而不想進行編譯或安裝,那麼就使用get指令。
引用:
cpan>get CPANPLUS
類似的指令則有make,test,install,clean等等,這些都是針對某個模組進行安裝相關的操作。而如果你想查詢某個模組的相關資料,你可以使用i這個指令,就像這樣:
引用:
cpan> i CPANPLUS
Strange distribution name [CPANPLUS]
Module id = CPANPLUS
    CPAN_USERID  AUTRIJUS (Autrijus Tang <autrijus@autrijus.org>)
    CPAN_VERSION 0.049
    CPAN_FILE    A/AU/AUTRIJUS/CPANPLUS-0.049.tar.gz
    MANPAGE      CPANPLUS - Command-line access to the CPAN interface
    INST_FILE    /usr/local/lib/perl5/site_perl/5.8.4/CPANPLUS.pm
    INST_VERSION 0.049
我們可以知道模組的名稱,版本,作者等等各種資訊。另外,i也可以使用正規表示式,所以如果你使用
引用:
cpan>i /CPANPLUS/
就會傳回一串內容有關CPANPLUS的模組。不過因為使用i這個指令會傳回所有關於模組,作者,散佈或集結而成的模組(例如Bundle::CPAN)等等資訊。而如果你想單純的搜尋其中一個部份,就可以使用作者(a),模組(m),散佈(m)跟集結(b)。我們繼續用CPANPLUS作例子:
引用:
cpan> d /CPANPLUS/
Distribution    A/AU/AUTRIJUS/CPANPLUS-0.049.tar.gz
Distribution    B/BD/BDULFER/CPANPLUS-Shell-Tk-0.02.tar.gz
Distribution    K/KA/KANE/CPANPLUS-0.042.tar.gz
Distribution    M/MA/MARCUS/CPANPLUS-Shell-Curses-0.06.tar.gz
4 items found
這樣應該清楚多了,這表示我們只要搜尋內容有CPANPLUS的相關散佈檔案,而不想要包山包海的把所有相關資訊都收集起來,當然你也可以只使用m或a來取得相關的資訊。
接下來,還有一個你也許會常用到的功能,也就是"reload index"。CPAN上的模組幾乎無時無刻不在更新,所以你的電腦裡面的各種資料其實會需要經常更新,這時候你只需要利用這樣的方式,CPAN會自動取網路上找到最新的內容索引:
引用:
cpan> reload index
Fetching with LWP:
  訪客無法瀏覽此圖片或連結,請先 註冊登入會員
Going to read /Users/hcchien/.cpan/sources/authors/01mailrc.txt.gz
Fetching with LWP:
  訪客無法瀏覽此圖片或連結,請先 註冊登入會員
Going to read /Users/hcchien/.cpan/sources/modules/02packages.details.txt.gz
  Database was generated on Tue, 11 May 2004 09:34:08 GMT
Fetching with LWP:
  訪客無法瀏覽此圖片或連結,請先 註冊登入會員
Going to read /Users/hcchien/.cpan/sources/modules/03modlist.data.gz
Going to write /Users/hcchien/.cpan/Metadata
最後,如果你要離開,就請使用quit,CPAN會移除某些暫存檔,讓你平安的回到地面。
CPAN在perl的使用上確實是非常方便的,不過有些地方還是讓人有點感覺不夠,例如你如果順手想要安裝CPANPLUS,那麼你可能需要進入cpan的命令列下,或是利用Perl的單行模式執行這樣的指令:
引用:
>perl -MCPAN -e"install CPANPLUS"
這倒還好,雖然指令長了一點,不過總是一次可以解決。只是我們都知道,因為在Perl中使用其他各式各樣的模組是縮短開發時程跟降低成本的好方法,所以大部份模組其實都還是用了其他模組,我們就說這個要被安裝的模組必須依賴其他某些模組,可是在CPAN裡卻沒辦法幫我們完成這些相關性的安裝工作。所以如果你安裝某一個模組卻發現它必須依賴其他模組時,CPAN會發出錯誤訊息給你,然後就停擺了。當然在我們的期望中,如果它可以「順便」幫我們把其他需要的模組也安裝進去,那顯然會減少許多手動的工作。因此在這樣的需求下,CPANPLUS也就因應而生了。
CPANPLUS在使用上有一些不同於CPAN的地方,例如你可以直接在shell下面執行CPANPLUS的安裝手續,就只要這麼打:
引用:
>cpanp -i SVK
接著,神奇的事情就要發生了,當我們安裝一個模組,而它所依賴的其他模組並不存在時,系統就會自動詢問使用者是不是要同時安裝相關的模組,就像這樣:
引用:
[root@Apple]# cpanp -i IO::All
CPANPLUS::Shell:efault -- CPAN exploration and modules installation (v0.03)
*** Please report bugs to <cpanplus-bugs@lists.sourceforge.net>.
*** Using CPANPLUS::Backend v0.049.  ReadLine support suppressed in batch mode.

Installing: IO::All
Warning: prerequisite Spiffy 0.16 not found. We have 0.15.
Checking if your kit is complete...
Looks good
Writing Makefile for IO::All

Spiffy is a required module for this install.
Would you like me to install it? [Y/n]:
接下來,CPANPLUS也有自己的終端機,你只需要用"cpanp"就可以進入:
引用:
[root@Apple]# cpanp
CPANPLUS::Shell:efault -- CPAN exploration and modules installation (v0.03)
*** Please report bugs to <cpanplus-bugs@lists.sourceforge.net>.
*** Using CPANPLUS::Backend v0.049.  
*** ReadLine support available (try 'i Term::ReadLine:erl').

CPAN Terminal>
CPANPLUS還有一個非常好用的功能,就是列出目前系統中還沒更新的模組清單,所以你只要進入cpanp,然後使用"o"就可以得到系統中需要更新的模組,不過這通常需要花費一段時間:
引用:
CPAN Terminal> o

    1     3.04       3.05   CGI                                      LDS      
    2     1.06       1.08   Digest                                   GAAS      
    3                1.05   Encode::CN::HZ                           DANKOGAI  
    4     0.56       0.59   ExtUtils::AutoInstall                    AUTRIJUS
    ......
    ......
另外,還有跟CPAN比較不同的部份在於你使用CPANPLUS可以解安裝某個模組,也就是使用"u"這個選項,也就是代表uninstall的意思。你還可以利用cpanp進行本地端的perl模組管理,例如使用e來新增某些目錄倒你自己的@INC中。至於在cpan中使用reload index的工作,在cpanp中只需要按下x就可以了。
其實在不久之後,CPANPLUS將取代CPAN在Perl核心中的地位,因此現在開始熟悉CPANPLUS似乎也不是甚麼壞事。

13.4 使用模組

看了一堆長篇大論,我們終於要開始寫程式了,或是你根本直接跳過前面的敘述來到這裡。不管如何,我們假設你已經學會怎麼裝模組了,而現在該來學習怎麼使用這些已經存在你硬碟中的模組了吧!
先來用一個簡單的模組吧,這個模組對於將來你在寫程式的除錯時會有相當的助益。就像下面這一段程式碼所寫的樣子:
引用:
use strict;
use Data:umper;          # 說明我們要使用的模組名稱

my %hash = ("john", 24, "mary", 28, "paul", 22, "alice", 19);
print Dumper(%hash);          # 這就是模組裡面提供的函式
當我們決定要使用某個模組時,我們就用關鍵字"use",也就是「使用某某模組」的意思,還真是口語化。接下來,你就可以使用模組內提供的函式了。所以我們在接下來的地方,定義了一個雜湊,是包含了名字,以及他們的年紀。程式最後,我們用了Dumper這個函式來印出雜湊hash裡的所有內容,而Dumper其實就是Data::Dumper這個模組所提供的函式。
最基本,對於模組的使用大概就是這個樣子。當然,這也是最簡單的方式。在你使用use來指定你所要使用的模組時,Perl會載入模組,並且把模組匯入。而當你在使用use這個指令時,其實還可以指定匯入模組中的某些函式。例如我們找到一個模組,叫做Cwd,它主要的功能是可以幫助我們找到目前的路徑,如果你是一個Unix的使用者,那麼他就非常接近ped這個Unix指令。這個模組提供了不少函式,不過我們有時候並不想全部用到,所以你雖然可以像原來的方式,這麼使用它:
引用:
use Cwd;

my $dir = cwd();

print $dir;          # 印出目前的目錄
另外一種方式則是在use後多加一個參數,用來表示要匯入的函式。就像這樣:
引用:
use Cwd qw(abs_path);                  # 我只想用abs_path

my $dir = Cwd::abs_path;          # 這時候,要加上完整的模組名稱

print $dir;
在你使用模組的時候,你也許還要注意另外一件事,也就是Perl能不能正確的載入你的模組。大多數的時候,你會從CPAN安裝模組,這些狀況下其實並不會有太大的問題,因為系統都會幫你安排好你的模組所應該擺放的位置。可是如果你利用其他方式取模組,或準備安插自己的模組時,有時候卻會因為Perl找不到你指定的模組而無法進行載入。因為對於Perl來說,它有一個模組載入的路徑,而這些路徑其實是被紀錄在@INC底下的,所以如果你沒有安裝某個模組,或是你的模組並不在Perl的載入路徑內的話,那麼它就會告訴你無法找到這個模組。
因為@INC也是Perl內建的陣列變數,所以如果你想要知道系統中的@INC,你也可以直接用這樣的方式印出來:
引用:
print "@INC\n";          # 別懷疑,只需要這樣
或者你直接在命令列底下使用perl -V也可以看到相關的內容。不過這樣會輸出非常多的內容,只怕在這裡列出來會佔去一大頁,所以還是由各位自己試試看吧。
當然,如果你的模組地處邊緣,沒有辦法被@INC含蓋進去的話,也不用擔心,你可以自己指定程式內使用的模組路徑。其中一種方式是直接修改@INC變數,只是這個部份牽扯到關於模組載入的時機,也就是如果Perl在編譯的時候無法找到指定的模組,它就會開始不高興,然後大聲哭鬧。所以你在程式碼執行的部份修改了@INC這個變數對於停止Perl的問題並沒有太大的幫助 (註二)。既然解決方法不只一種 (There is more than one way to do it),我們顯然可以試試其他辦法。有另外一個方式,也就是直接使用use指令,就像這樣:
引用:
use lib "/home/hcchien/pm/";

use Personal;
其實你還是有其他各式各樣的方式,不過在這裡我想還是這樣就足夠了。至少這也是目前我遇過最常用的方式,如果某一天你覺得這樣的方式已經不敷使用,相信你已經有能力找到更多方式來幫助你解決問題了。

13.5 開始寫出你的套件

經過一番努力,你應該要慢慢熟悉怎麼使用cpan/cpanplus從CPAN安裝各式各樣的模組了 (註三)。接下來,你應該蓋上這本書,然後開始寫Perl程式了。或是你已經可以自己寫一些程式,然後拿來解決一些日常生活上的問題,或工作上的需要。接下來,你已經準備把手上已經寫好的程式碼集結起來,先把它們集結出一個套件吧。
我們剛剛一直在討論怎麼使用CPAN上豐富的模組資源,不過現在我們應該回過頭來看看模組的組成元素。其中非常重要的一個部份,也就是套件。套件其實就是你在寫Perl程式時的零件箱,也就是你可以放進一堆可以重新使用的小零件,那麼在程式裡面,你就可以直接拿出來,兜起來,很快就可以寫好自己的程式。
我們先來用個簡單的例子吧,雖然大多數的工作已經有了模組可以讓我們使用,可是要讓大家簡單明暸的看懂套件的寫法,我們還是來看看下個這個程式吧:
引用:
#!/usr/bin/perl -w

use strict;

my @grades = (67, 73, 57, 44, 82, 79, 67, 88, 95, 70);;

my $adv = adv(@grades);                              # 叫用 adv 這個副常式
print $adv;

sub adv {
    my @input = @_;
    my $total;
    $total+=$_ for (@input);          # 算總和
    $adv = $total/scalar(@input);  # 求平均
}
這程式相當簡單,我們在主要的程式部份看到兩個重點,第一個就是定義一個陣列,在這裡我們定義了一個關於成績的陣列,裡面放滿了一堆學生的成績。第二個重點則是在程式裡面叫用了adv這個副常式。至於在副常式adv裡面,我們取得了主程式傳來的成績陣列,緊接著計算總和,然後算出平均。接下來,我們希望開放這個方便的副常式給其他程式使用,所以我們必須把它放進套件中,就像這樣:
引用:
package Personal;  # 套件的開始

sub adv {
    my @input = @_;
    my $total;
    $total+=$_ for (@input);
    $adv = $total/scalar(@input);
}

1;                  # 回傳一個真值
於是我把它儲存為另一個檔案,叫做Personal.pm,接下來我們就可以開始使用這個套件了。也就像我們之前所說的方式,我們還是使用use這個指令。所以原來的程式就會變成這樣:
引用:
use strict;
use Personal;                          # 現在我們直接使用這個套件了

my @grades = (67, 73, 57, 44, 82, 79, 67, 88, 95, 70);;

my $adv = Personal::adv(@grades);       # 然後呼叫套件的adv
print $adv;
乍看之下,好像不過就是把原來的程式切成兩半,實在一點也沒甚麼特殊的。不過事實當然絕非如此,現在我們假設又要寫另一個程式了,這次要算的是一大群人的平均年齡。沒錯,我們又需要用到算平均的函式了。所以我們寫了這樣的程式:
引用:
#!/usr/bin/perl -w

use strict;
use Personal;

my %member = (  'john' => 22,
                'mary' => 42,
                'paul' => 27,
                'alice' => 19,
                'josha' => 37  );

my @age = values %member;  # 取出所有人的年齡,放入陣列

my $adv = Personal::adv(@age);  # 還是使用了adv這個函式
print $adv;
這樣的用法應該不難理解,我們現在有了自己的套件檔案,也就是Personal.pm。接下來,我們只要再度需要使用adv的部份,就只需要載入Personal.pm就可以。當然,使用者也可以自己不斷加入新的函式,來讓自己的函式庫越來越豐富。
一般來說,我們都以.pm來作為分辨一般Perl程式與套件的方式。而我們在套件的一開始,則是以關鍵字package來表明這個套件的名稱,就像我們剛剛寫的方式:
引用:
package Personal;
當我們開始使用套件的時候,其實套件內部就像是另一個獨立的程式一樣,你在使用一個套件時,並沒有辦法提供一個全域變數來供所有人使用。而且這當然是完全合理的邏輯,否則不是會讓人一團混亂嗎?不過在套件內部確實也有一些機制來保持一個專屬的空間,避免套件與套件之間,套件與程式之間,所有的變數名稱,副常式變成像一盤義大利麵一樣,全部打結混在一起。因此,我們在將自己的套件完成到某個程度之後,使用套件名稱作為檔名儲存起來,就可以開始使用了。以剛剛的套件為例,我們就把他儲存為Personal.pm。
Perl使用的方式就是所謂的「符號表 (symbol table)」。一般沒有被定義套件名稱的的部份,其實都是被委以main這個套件,當然你也可以隨時使用package來定義套件名稱。套件的有效範圍是從你使用package宣告開始,一直到這個區塊的結束,或是另一個package的宣告。所以這也就是某個套件的有效命名空間,也就是說在這有效範圍內的變數命名都會是屬於這個套件的命名空間下。用簡單的表示方式就會是:
引用:
$PACKAGE:VARIABLE
這樣的方式其實就讓人比較可以理解為甚麼我們會使用類似Cwd::abs_path這樣的方式來呼叫某個副常式。而且你也可能偶而會發現這樣的程式錯誤:
引用:
Undefined subroutine &main::adv called at ch3.pl line 14.
也就是Perl在main這個命名空間下並沒有找到adv這個副常式。

而就像很多情況看到的,我們可以使用包含的關係來使用套件的命名空間,所以你當然也可能看到類似這樣的方式:
引用:
$PACKAGE1:PACKAGE2:VARIABLE
套件與模組的使用對於大多數的程式寫作都是非常重要的議題,而對於Perl來說更是如此,因為他可以讓你大量減少程式寫作的時間。至少在看完這一章之後,你應該可以開始學習使用CPAN上廣大的資源。

習題:
1. 試著在你的Unix-like上的機器裝起CPANPLUS這個模組。
2. 還記得我們寫過階乘的副常式嗎?試著把它放入套件My.pm中,並且寫出一個程式呼叫,然後使用這個副常式。


註一:許多專案管理的方式就是採用拷貝,複製的方式,和我們所提的方式顯然大異其趣。
註二:我們確實可以在程式碼中要求Perl在編譯期間就修改變數@INC,不過我們不打算在這裡把這件事情搞的這麼複雜。
註三:千萬別小看這部份,如果沒有訪客無法瀏覽此圖片或連結,請先 註冊登入會員 ,你寫Perl會感覺太辛苦了。

TOP

14 參照 (Reference)

對於一個剛開始使用Perl的使用者來說,要深切的瞭解參照確實是非常困難的一件事。可是如果不使用參照,你就會發現有很多時候,事情會變得相當複雜。因此大略的瞭解Perl參照的使用實在是相當重要。不過對於許多從事Perl教學的人來說,Perl的參照是既複雜又困難的事,因此在大約20小時的教學中,並不容易包含參照的使用。所以有些給Perl入門使用者的書籍也會避開這個題目,而在更進階的書籍再專文介紹。可是我以為如果失去了參照的使用,很多相當方便的使用方式都無法被實作,讓人無法領略Perl的方便性。可是要如何以簡單的方式概略的介紹Perl的參照也是另外一項複雜的議題。不過雖然如此,我們還是來嘗試以比較淺顯易懂的方式來介紹參照的常用方式。

14.1 何謂參照
就像C程式語言造成許多程式設計師的困擾一般,參照之於Perl也有類似的效果。當然造成這個狀況的原因大概也是因為參照的抽像觀念也足夠跟C的指標媲美。其實這也許只是危言聳聽,我們應該想辦法讓指標變得更容易使用,而且根據Perl的80/20定律,我們只需要學會其中的百分之二十,就可以應付百分之八十的狀況。
「參照」,其實跟所謂的指標在意義上是非常接近的,也就是某個變數指向另外一個變數。比如我們有一個陣列變數像是這樣:
引用:
my @array = (1...10);
那麼我希望使用一個純量變數$ref來表示@array這個陣列時跟怎麼辦呢?這時候,我們只要表明它是一個陣列,而且它被儲存的位置在那裡。我們就可以順著這個線索找到@array這個陣列,而且得到他的內容。所以我們可以用這樣子來表示$ref變數:
引用:
ARRAY(0x80a448)
這樣看起來就非常清楚,我們有一個陣列,位址在0x80a448。所以我們可以藉由這樣的資料取得@array這個陣列的詳細資料。這就跟我們在Unix系統下使用檔案連結的方式有點接近,我們透過某個連結資訊來找到被連結的檔案。
而在Perl裡面,所謂的參照,其實確實是建立了另外一個符號,就像儲存一般的變數一樣,不過這次我們得到的是一個純量變數。Perl確實可以使用純量變數來指向任何其他的資料結構,也就是另一個純量變數,陣列或雜湊,就像我們剛剛看到的樣子。
我想我們可以用一個比較簡單的方式來解釋參照,例如妳們家的成員可以組成一個資料結構的型態(你可以使用陣列或雜湊,看你想要儲存的資料內容而定),於是如果想要用一個純量的資料型態來取得家裡成員的內容,那麼我們也許可以用門牌號碼來代表(當然,如果是有戶政單位的作業疏失造成門牌號碼重複等各種錯誤可不在我們討論的範圍內)。

14.2 取得參照

當然,你其實大可不必擔心怎麼找出參照的位址,因為這個部份可以由Perl代勞。我們來看看怎麼取得參照:
引用:
my @array = (1...10);
my $ref = \@array;  # 取得陣列@array的參照

print $ref;
當然,這個程式的執行結果就會看起來像是我們剛剛寫的樣子,只是位址會有所差異。接下來,我們也可以來看看怎麼利用一個參照來取得被參照的資料結構內容。就以剛剛的例子來看,我們定義了一個陣列@array,然後利用反斜線(\)來取得@array的參照。那麼我們要怎麼取得$ref所參照的@array內容呢?其實我們只需要這麼作就可以了:
引用:
print @$ref;          # 利用參照找回陣列
很顯然的,我們可以利用純量變數來取得另一個純量變數,或是陣列,或是雜湊的參照。當然,取得的方式都是類似的,所以我們只要這樣:
引用:
$scalar = "1...10";
@array = (1...10);
%hash = (1...10);
$scalar_ref = \$scalar;
$array_ref = \@array;
$hash_ref = \%hash;
這樣看起來應該就清楚多了,不過很多人可能還是沒辦法想像這樣的參照能有甚麼很大的用途。其實參照很重要的用途之一,就是在增加資料結構的彈性,或者你也可以說它是增加資料結構的複雜性。我們先來看看一個實例:
引用:
@john = (86, 77, 82, 90);
@paul = (88, 70, 92, 65);
@may = (71, 64, 68, 78);
我們現在假設有一個考試,總共考了四個科目,上面的陣列是這三個學生每一科的成績。可是我們如果要針對某個科目,一次取得每個學生的成績就會顯得很麻煩,因為我們可能需要$john[0], $paul[0], $mat[0]這樣的方式。因此在其他程式語言的實作方式則是使用所謂的「多維陣列」,例如你可以定義一個陣列,那麼它的結構會看起來樣這樣子:
引用:
grades[0][0] = 86;
grades[0][1] = 77;
grades[0][2] = 82;
.....
.....
grades[2][2] = 68;
grades[2][3] = 78;
很可惜,Perl的陣列並不接受這種方式的定義,也就是並沒有提供所謂的多維陣列的概念。可是卻不能依此推論Perl的資料結構太過簡陋,因為透過參照的方式可以讓Perl的三種資料結構都獲得最充分的運用。所以我們來看看該怎麼使用Perl的參照方式來實作多維陣列。
首先我們還是有三個陣列,分別代表三個學生的各科成績,就像剛剛的例子一般。接下來的重點就是把這三個陣列的參照放進另外一個陣列中,就像這樣:
引用:
@grades = (\@john, \@paul, \@may);
所以現在看來,@grades這個陣列中其實已經包含了@john,@paul,@may三個陣列了。這樣就可以實作出一個多維的陣列。各位應該可以想像整個陣列中夾雜著陣列參照的狀況。

我們其實並不是忠實的表達出資料在電腦記憶體中儲存的形式,不過在概念上卻可以清楚的看出利用參照來表達複雜資料結構的方法。也就是達到過去我們曾經在其他個種程式語言中所使用的多維陣列的方式。當然,在實際的使用上,如果我們想要達到過去利用其他語言做出來的多維陣列的,還是需要一點點小小的轉換,因為我們必須不只一次的在陣列中使用陣列參照,如此一來,如果要取出最後一層的陣列值就會讓人有點頭痛。而不像過去我們使用多維陣列的方式,如果有多維陣列,我們幾乎就只要這麼寫:
引用:
array[3][2][4];
既然如此,那麼為甚麼Perl不直接給我們多維陣列就好了呢?首先,在實際運用上,我們最常用的還是以一維跟二維陣列為主,所以利用參照的方式就可以容易的達成這個需求。其次,一但利用參照的方式,我們就可以使用更有彈性的資料結構,而不單只是一個多維陣列。聽起來好像非常神秘,不過其實仔細想想,確實很有道理。還記得嗎?我們說過陣列的元素其實就是一堆的純量所組成的,而且參照本身就是一個純量值,只是利用這個純量值,我們可以取得被參照的資料儲存在記憶體的位置。然後還有一個提示,也就是參照可以用來取得各種在Perl中原來就有的資料結構型式(註一)。說到這裡,也許你已經看出一些端倪,也就是利用參照的方式,你可以把大多數的資料都以純量的方式來表示,因此就有各式各樣的運用方式。下面就是一些我們可能會運用到的方式:
引用:
my @array = ('john', 'paul', 'ken');
my %info = ( 'date' => '3/27',
          'people' => \@array,
       'place' => '台北車站' );
這也許是某一次活動的資料,我們先取得了一個陣列,其中是參加活動的人員。接下來,我們會有或動的其他資料,例如活動的日期,地點。然後我們還希望把參加的人員也一起放入活動資料中,所以我們就使用了雜湊來儲存這些資料,可是雜湊中關於人員這個部份,我們是以陣列參照來表示。這就是另外一個非常典型使用參照來活化資料結構的例子。當然,如果你還有力氣,可以看看更複雜的例子,就像這樣:
引用:
my @john_grades = (65, 87, 92, 77, 53);
my %john = ( id => '7821434',
          birth => '1983/11/12',
       grades => \@john_grades );
......
......
my %students = ( john => \%john,
                  ......
           ...... );
這個例子顯然可以好好來解釋一下,就像魔術一般,我們用了雜湊跟陣列的多次排列,把學生的資料全部堆在一起了。首先我們先拿到學生的成績,這是一個陣列,而且是最簡單的陣列,所有的成績依序排列在陣列中。接下來,我們要取得某個學生的資料,其中包括了他的個人成績。因此我們使用了一個雜湊來儲存學生的個人資料,而在成績的部份,則是使用了陣列參照。這樣子,我們可以完整的描述一個學生的資料,接下來我們只需要另外一個雜湊來收集所有學生的資料就可以,而在這裡,我們這個雜湊的每個鍵是學生的名字(在假設學生不會同名的狀況下),然後把剛剛取得的單一學生資料取參照,作為這個整合參照的值。
這樣的寫法看起來確實複雜了許多,不過這是因為我們使用了逐步講解的方式,所以把整個過程的詳細的列出來。不過很多時候我們其實會使用匿名陣列或匿名雜湊的方式來表現。那麼就可以讓整個架構看起來容易,也清楚一些。我們把這種匿名陣列/雜湊寫法放在這裡,也許可以讓大家參考一下:
引用:
#!/usr/bin/perl

use strict;

my %students = ( john => { id => 'foo',                  # 這是雜湊的第一對鍵值
                     tel => '11223344',
                     grades => [34, 56, 78]},
           paul => { id => 'bar',                        # 第二對鍵值從這裡開始
                     tel => '223344',
                     grades => [44, 55, 66]},
          );
這樣的寫法顯然乾淨許多,雖然你可能還有點不習慣,不過可以確定的是,至少你不會再看到一大堆的變數名稱,而且每一個部份的相互關係也清楚了不少。當然,這時候你完全可以先忽略這個部份,不過為了讓你之後回過頭來看這一段程式碼時可以瞭解其中的奧妙之處,我們還是先說明一下這種方式的解讀方式。
首先,我們定義了一個學生的雜湊,這也就是我們最終想要的資料處理方式。接下來,我們看到雜湊%students中包含了兩對的鍵值。其中第一對的鍵就是john,而其相對應的值則是一個雜湊參照。這時候出現了第一個匿名的雜湊,他包含了三對的鍵值,其中的鍵分別是id,tel,grades。而grades對應的值則是一個匿名陣列的參照,這個陣列一共有三個值,分別代表三個科目的成績。因此在雜湊%students的第一對鍵值中,我們包含了兩個參照,分別為一個雜湊參照與另一個陣列參照。接下來的第二對鍵值則是以paul為對應的鍵,並且包含著一個結構相同的值。
好吧,你可以暫時先忘了這麼複雜的部份,至少你暫時應該可以使用最簡單的參照結構來實作一個二維陣列。

14.3 參照的內容

我們剛剛以經學到利用各種方式得到某個資料型態的參照,並且可以把取得的參照值放入其他的資料型態內,組成其他比較複雜的資料儲存形式。可是接下來我們總會在程式當中取出這些值,因此該怎麼解開參照,讓他們指向原來所代表的那一個資料內容呢?
我們先看看這樣一個簡單的參照:
引用:
my @aray = qw/John Paul May/;                  # 一個陣列
my $array_ref = \@array;                  # 取得這個陣列的參照
接下來,我們用大括號將參照包起來,並且恢復他應該有的資料型態代表符號,在這個例子中就是@號。所以看起來應該像是這樣子:
引用:
print @{$array_ref};                          # 印出JohnPaulMary
當然,我們也可以利用陣列的方式來取得某個索引的值,也就是這樣:
引用:
print ${$array_ref}[1];                          # 這樣就跟 $array[1] 一樣
看起來好像不太困難,那我們來依樣畫葫蘆,試試看雜湊參照的解法。當然,還是先建立一個雜湊吧,並且取得他的參照吧:
引用:
my %hash = qw/John 24 Paul 30 May 26/;
my $hash_ref = \%hash;
接下來,好像並不困難,我們只要把{$hash_ref}視為一個雜湊變數的名稱,所以要取得雜湊中,雜湊鍵為"John"的值就只需要這麼作:
引用:
print ${$hash_ref}{John};                  # 果然印出 24
print ${$hash_ref}{Paul};                  # 結果是 30
print ${$hash_ref}{may};                  # 正如我們的期待,就是 26
當然,你也可以把%{$hash_ref}當成一個一般的雜湊來運作,所以你幾乎可以毫無疑問的這麼使用:
引用:
for (keys %{$hash_ref}) {
    print ${$hash_ref}{$_}."\n";          # 印出 24, 26, 30
}
你是一個很簡單的例子,我們可以直接把%{$hash_ref}當成一般的雜湊來操作。所以一般使用於雜湊的函數也可以直接用於%{$hash_ref}上,相同的狀況,我們也可以在解開陣列參照之後,用相同的方式來操作。所以如果用剛剛的例子,我們也可以這麼寫:
引用:
my @aray = qw/John Paul May/;
my $array_ref = \@array;
for (@{$array_ref}) {
    print "姓名:$_\n";
}
14.4 利用參照進行二維陣列

我們在前面已經提過了利用參照來實作二維陣列的方式,可是為甚麼這一小節還要再重新解釋一次呢?主要是因為我們剛剛可以利用參照建立一個簡單的二維陣列,可是我們卻還不知道怎麼能靈活的操作這個陣列。而且利用參照來營造一個二維陣列是非常常見的參照使用方式,所以我們必須再詳細的逐步解釋二維陣列的建構,以及解構。最後並且引申出利用雜湊的值包含陣列參照的運作與利用。
如果你還不熟悉,我們先來建立一個二維陣列。我們假設這是一個日期與氣溫的對照,每天定時量測當地氣溫三次,分別紀錄於陣列中。所以我們以比較繁雜的手續建立起這樣的一個二維陣列:
引用:
my @d1 = (24.2, 26.3, 23.4);                  # 每天的溫度
my @d2 = (23.5, 27.5, 22.6);
my @d3 = (25.2, 28.7, 24.8);
......
......
my @d30 = (19.8, 22.1, 19.2);

my @daily = (\@d1, \@d2, \@d3, ...... , \@d30); # 當月每天的溫度
雖然複雜,不過終於把整個陣列建立起來了。目前我們已經有了30個陣列,各代表了第一天到第三十天中每天的溫度紀錄,接下來就是定義一個陣列包含了這三十個陣列的參照值,而這個陣列也就包含了這個月每天三次溫度的紀錄。於是我們可以利用參照的方式取得某一天的溫度,例如${$daily[4]}[0]就代表了第五天的第一次測量。這次你一定受不了了,這麼複雜的結構並沒有為程式設計師帶來比較舒適的環境,反正讓人徒增困擾。因為我們必須為每一天先建立一個陣列,然後再將陣列參照放入另一個陣列中,接著解參照,取出第二層陣列中的值。
很顯然,如果我們只有這種方式可以使用,那麼負責Perl設計與維護的那些黑客們一定自己先受不了。所以我們的另一個方式就是「匿名陣列」,「匿名雜湊」,而且這個作法我們剛剛已經稍微看過了。現在我們再來瞭解一下它們的用法。首先在賦值上,陣列所使用的是中括號[],也就是當你在對陣列取值時的符號。而對於匿名雜湊,則是使用大括號{},同樣的,也是利用你對雜湊取值時所用的方式類似。所以剛剛的例子如果重新定義陣列@daily就應該要寫成:
引用:
@daily = ([24.2, 26.3, 23.4], [23.5, 27.5, 22.6], [25.2, 28.7, 24.8]...);
看起來好像跟其他程式語言的方式比較接近了,可以取值應該怎麼作呢?還是要先解參照,然後取出陣列的某個值,然後再來解參照嗎?很慶幸的,這種複雜的工作實在不適合用來放在這種可能在日常生活中會大量使用的二維陣列中,因此我們也可以用很方便的方式來取得其中的值。所以要取值的方法就像這樣:
引用:
print $daily[2][1];
這樣真的清爽多了,如果你用過其他程式語言的二維陣列,其實大概也都是這樣的寫法。當然,你可以作的絕對不只二維陣列,你可以用同樣的方式來實作多維陣列,就像你可以很容易的造出一個三維陣列。
引用:
@demo = ([[2, 4, 5], [3, 2], [2, 6, 7]], [4, 7, 2], [[1, 3, 5], [2, 4, 6]]);
現在回想起來,如果這個陣列要一個一個把名字定義出來,然後取它們的參照,放入其他陣列中......,這實在太辛苦了。於是匿名陣列節省了我們不少的時間,當然,想必也降低了很多錯誤的機會。

11.5 陣列中的參照,參照中的陣列,陣列中的陣列

這個標題實在太繞口令了,雖然我們應該直接取標題為:「匿名雜湊與匿名陣列」,不過這樣的標題好像非常不容易平易近人,所以還是維持這個冗長的標題吧。
在上一節其實已經利用匿名陣列了,也就是我們用來實作二維陣列的輕鬆愉快版本。另外,我們也嘗試過在雜湊裡面放入陣列,可是既然我們可以方便的利用匿名陣列來進行多維陣列的實作,那麼利用類似的方式,把匿名雜湊,匿名陣列的交互使用,顯然可以讓整個資料結構更具有彈性。
還記得我們怎麼整理學生的資料嗎?那時候我們已經用了這樣結構的處理方式。學生的個人資料項目是一個匿名雜湊,而每個學生的成績則是由匿名陣列來組成的。因此我們就可以用簡單的方式來取出需要的值,所以我們就可以這麼用:
引用:
print $students{john}{grades}[2];
這樣應該非常方便,你並不需要手動去解參照,或者進行甚麼繁雜的手續。而就像一般的陣列或參照的用法一樣,用中括號來取得陣列的值,或是用大括號才使用雜湊。而匿名雜湊也是常用的方式,它們可能被隱藏在陣列或雜湊中,就像我們剛剛看到學生資料的例子,就是一個「雜湊中的雜湊」實作的例子。
另外很常用的的一種匿名雜湊方式則是陣列中的雜湊,很好的一個例子就是從資料庫擷取出來的資料,這時候我們常常會把每一筆資料依據欄位的名稱,跟所得到值存放在雜湊中,然後將每筆這樣的雜湊存入陣列中所以一個陣列看起來會像是這個樣子:
引用:
@data = ( { 'column1' => 'data1',
            'column2' => 'data2' },
    { 'column1' => 'data3',
      'column2' => 'data4' } );
如果你的資料儲存形式像是這個樣子,在陣列中放入匿名雜湊,那麼你如果要取出某個值,就只需要這麼寫:
引用:
print $data[1]{column2};  # 這樣你就可以得到data4
其實你也許不太習慣,為甚麼在使用匿名陣列,或匿名雜湊時,總會有不同於正常指定陣列或雜湊的方式呢?不過我們可以來看看這樣的狀況:
引用:
my @array = ((3, 5, 7, 9), (1,4, 8, 6), (2, 5, 4, 2));
這時候,我們知道最外面一層是一個陣列,我們利用串列的方式指定了三個元素給這個陣列,而這三個元素卻都是串列,也就是說,我們希望把這三個串列放入陣列中。可是這時候問題就出現了,因為很明顯的,我們必須在最外層的陣列裡面定義三個變數,才能利用參照的方式把串列放入陣列裡,可是在一般使用的時候,不管陣列或參照,我們都可以使用串列的方式來賦值。像這樣的兩種形式其實都是可能的:
引用:
@temp = (3, 5, 7, 9);
%temp = (3, 5, 7, 9);
所以如果我們利用剛剛的方式,希望把三個串列利用匿名陣列或匿名雜湊放進陣列@array的話,就會造成Perl的錯亂,因為它無法清楚的明白你所需要的是匿名的陣列或是雜湊。這也就是你必須清楚的表示你的需求,因此你如果希望使用匿名陣列或雜湊,就必須適當的分別清楚,所以依據你自己的需求,你就必須作不同的定義,就像這樣:
引用:
my @array = ([3, 5, 7, 9], [1,4, 8, 6], [2, 5, 4, 2]);
my @array = ({3, 5, 7, 9}, {1,4, 8, 6}, {2, 5, 4, 2});
因為在Perl當中,你都是利用最簡單的陣列,雜湊的資料結構,配合上參照(當然還包括匿名陣列與雜湊)的方式,來組成更複雜的資料結構,例如多維陣列,或是陣列中的雜湊,雜湊中的陣列等等。也就因此,你可以有更大的彈性來玩弄各種結構的組成。比如你可以在陣列中的各個不同的元素裡,擺放不同資料結構的參照,所以你當然可以這麼作:
引用:
my @array = ({3, 5, 7, 9}, [1, 4, 8, 6], {2, 5, 4, 2});
所以,你對於這個變數的取值就有可能是:
引用:
print $array[0][2];  # 得到的結果是7
print $array[1]{8};  # 這裡會印出6
其實參照的用法並不僅只於這些資料結構上的變化,你還可以取得副常式的參照,當然也可以使用匿名副常式的方式。就像你在使用陣列或雜湊的參照一般。參照的用法非常的靈活,而且運用非常的廣泛,Perl的物件導向寫法也是參照的運用。不過我們不希望剛入門的使用者被大量的參照困擾,所以等各位寫過一陣子的Perl之後可以再去參考其他的Perl文件,瞭解更多關於Perl參照的用法。

習題:
1. 下面程式中,%hash是一個雜湊變數,$hash_ref則是這個雜湊變數的參照。試著利用$hash_ref找出參照的所有鍵值。
引用:
%hash = ( name => 'John',
          age => 24,
    cellphone => '0911111111' );
$hash_ref = \%hash;
2. 以下有一個雜湊,試著將第一題中的雜湊跟這個雜湊放入同一陣列@array_hash中。
引用:
%hash1 = ( name => 'Paul',
            age => 21,
     cellphone => '0922222222',
     birthday => '1982/3/21' );
3. 承上一題,印出陣列$hash_array中每個雜湊鍵為'birthday'的值,如果雜湊鍵不存在,就印出「不存在」來提醒使用者。

註一:其實不單只是這些資料可以取得參照,還有其他部份也可以使用參照來操作,不過我們並不在此討論。

TOP

15. 關於資料庫的基本操作

如果讀者練習足夠認真的話,也許你已經對Perl可以慢慢的上手了。當然,你可能也準備用Perl來寫公司的一些小系統,或是個人日常生活使用的一些小程式。而大部份的時候,你都會希望能把存下一些資料,至少總不會每次要執行程式時,又要重新輸入這些相關的資訊。當然,很多資料也總是隨著程式不斷進行時被記錄下來的。這時候你可以利用不少不同的方式來儲存資料,例如你可以寫入檔案中。最簡單的方式應該也是如此,我們總是可以舉出這樣的例子。
引用:
sub savePhone {
    my ($name, $phone) = shift;
    open PHONE, ">>phone";
    print PHONE "$name\t$phone";
    close PHONE;
}
這是一個很小,也很簡單,作為通訊錄的副常式。或者它根本算不上是一個通訊錄,只是紀錄姓名跟電話的相對應資料。而我們的作法也不過就是把得到的資料寫入檔案中,而這個檔案的名稱叫做"phone"。於是我們把姓名跟電話新增到檔案的最後,而且就只作這個動作,然後就把檔案關閉。如果我們持續新增朋友的電話,那麼檔案也就會不斷增長,也就長的像一個通訊錄了。
這樣的通訊錄有甚麼特色呢?特色就是它會長長的一串,如果你交友廣闊,也許這樣的檔案可以讓你印出好幾頁。當然,如果你要搜尋也不是太容易,要排序也是要自己處理。所以歸納出一個特色,這樣的通訊錄純粹只是作為「紀錄」,實在無法拿來運用。不相信的話,你可以想像一下,如果你現在想紀錄的不只姓名跟電話,你還想把眾多朋友的地址,Email全部記下來,那麼你會發現用檔案真是非常辛苦。何況如果你想在使用檔案儲存這些資料的同時還可以方便的搜尋,刪除,排序等等,那麼你大概很快就會放棄這樣的想法。
當然,如果你只是想要簡單的記下一些資料,那麼單純的使用檔案也會讓你自己輕鬆一點,不過如果你打算開始利用Perl幫你處理一些稍微複雜一點的資料時,那麼你應該好好考慮使用資料庫的形式。

15.1 DBM

一種非常簡單的資料庫形式,而且在你安裝Perl之後,你也就同時擁有這樣的資料庫系統,也就是「DBM檔案」。不過雖然同樣都是「DBM檔案」,運作的方式卻根據所在的環境而不盡然完全相同。所以如果對於DBM的運作方式有興趣的人也許就得自己去翻翻其他資料了。
不過就像本章大多數的部份,我們會盡量讓大家在比較沒有負擔的狀況下使用資料庫。畢竟在這個時候,很多技巧與程式的語法似乎都是使用比自己動手開始寫要重要許多。

15.1.1 與DBM連繫

「DBM檔案」的資料庫非常有趣,它是利用特殊的雜湊來存取資料庫,或說達成資料庫的形式。所以它會利用雜湊跟所謂的DBM進行緊密的結合。這從我們開始操作DBM就可以看出來,所以我們先來開啟一個DBM檔案,試試怎麼使用DBM。
引用:
dbmopen (%HASH, "dbmfile", 0666) or
    die "檔案打不開!";
看起來是不是有點面熟?其實跟開檔案的方式確實有點接近。不過我們所指定的並不是檔案代號,而是一個雜湊,用來聯繫DBM檔案,這樣的方式可以讓你在操作雜湊時就等於在操作DBM檔案一般,也就是讓你用簡單的方式對雜湊進行改變時,Perl會直接幫你反應到檔案中。
其實當我們使用了dbmopen時,Perl也會自動幫我們建立起相對的資料庫檔案,可是我們在寫程式所對應的還是只有雜湊,這樣想像起來好像非常的輕鬆。即使Perl已經幫我們在系統中建立了兩個實體檔案,我們卻並不需要理會,繼續使用方便的雜湊吧。
當然,這個雜湊就像我們平常所看到的一樣,所以你也要遵守雜湊的命名規則。不過很多時候,程式設計師總會有一些慣用法,就像檔案代號大多使用全部大寫的方式,我們也習慣使用全部大寫的雜湊代號來代表繫結DBM檔案的雜湊。而且請各位務必記住,所謂的繫結是指在目前的程式當中,一但我們利用dbmopen這個指令時,Perl會讓我們以雜湊的方式來對DBM檔案運作,可是一但我們關閉這個繫結或是離開程式後,這樣的關係就不存在了。所以檔案內當然也不會有相關的雜湊名稱被記錄下來。
而關閉的方式則是只要使用像這樣的指令:
引用:
dbmclose(%HASH);
沒錯,還是跟你在關閉一個檔案一樣的方式。而且如果你沒有手動關閉這個DBM檔案,Perl也會在程式結束時自動將它關閉,不過你應該知道,這不會是一個好習慣的。

15.1.2 DBM檔案的操作

在開啟一個DBM檔案資料庫之後,我們就可以對它進行存取,也就是進行一般的操作。例如你可以修改資料庫的內容,新增資料,搜尋你要的資料等等。至少值得慶幸的是,所有的事情都可以利用雜湊來完成,那是大家都還算熟悉的東西。其實我們如果看個例子,應該很容易就可以上手了。
引用:
dbmopen (%HASH, "dbmfile", 0666) or
    die "檔案打不開!";
print $HASH{'John'} if (exists $HASH{'Hohn'});
$HASH{'Mary'} = '0227331122';

@sort_keys = sort {$a cmp $b} (keys %HASH);
for (@sort_keys) {
    print "$_: $HASH{$_}\n";
}

delete $HASH{'Paul'};
dbmclose(%HASH);
沒錯吧,除了第一行需要讓你的雜湊跟DBM檔案建立繫結以及最後一行關閉DBM檔案之外,你幾乎看不出來你正在對一個DBM資料庫進行操作。因此如果你打算要用DBM資料庫,那麼幾乎是可以非常容易的上手。不過其實使用DBM資料庫還是有一些比較深入的技巧,例如你可能要鎖定資料庫,以避免因為超過一個以上的行程在存取資料庫而產生問題。不過一開始使用,你暫時還不需要讓自己煩惱這麼多 (否則只怕光擔心這些問題就讓人不敢使用了)。而這相關的問題,你可以在某些進階的Perl相關書籍中找到合適的作法與解答。

15.1.3 多重資料

如果你還記得雜湊的特性(希望你會記得,不然該怎麼使用DBM資料庫呢),那麼你應該記得雜湊其實是由一對的鍵值所構成的。也就是說,雜湊中每個獨特的鍵都會被對應到一個值上。而我們剛剛也利用了這個特性,紀錄了姓名與電話的對應關係。但是這時候卻有問題了,如果我希望記下的不只電話,或說除了家裡電話之外,我還希望記下好友的行動電話號碼,那麼在雜湊中好像就不適用了。
可是如果一次只能紀錄一個鍵跟一個值,那麼DBM資料庫還真是不實用,畢竟大多數的時候,我們總會需要使用一大堆的資料欄位。因此最好可以讓DBM資料庫可以讓一個鍵對應到多個欄位,這樣子我們就可以紀錄行動電話,或是生日,地址等等其他資料了。
可是既然雜湊的特性沒辦法改變,那麼要怎麼樣可以把多個欄位擠在一個雜湊值中呢?很直覺的,我們當然可以直接把所有的資料「連」成一個超長的字串。所以你可能得到這樣的資料:
引用:
$personal_data = "02-27631122\t0931213987\t1974.12.3"
接著你利用split來把字串切成一個小陣列,讓你可以獨立使用每一個部份,就像這樣的方式:
引用:
@data = split /\t/, @personal_data;
這樣好像很容易,你可以利用簡單的方式來儲存多個欄位的資料。可是如果有其他更有效率的方式也許會更好,畢竟把所有東西全部胡亂的擠成一團似乎不是甚麼太好的方式。所以pack跟unpack這樣的函式似乎可以幫我們解決這類的問題,而且它們也正是為了這個理由而存在的。當然,有一部份的理由是在於把資料打包成為系統或網路的傳輸時使用。
pack的運作方式主要在於格式化我們的資料,也就是當我們在進行打包的時候,我們必須先定義出打包的方式。例如你要打包成位元組,整數等等不同的資料格式。所以Perl會根據我們所定義出來的格式,把資料打包成整齊的字串,以方便我們進行儲存或傳輸的工作。至於我們常用的格式字元包括c(字元),s(整數),l(長整數),a(字串),h(以低位元組開始的十六進位字串),H(以高位元組開始的十六進位字串),f(浮點數),x(一個null的位元組)等等。當然,各式各樣的格式字元種類繁雜,各位如果有興趣可以利用perldoc 來檢查手上版本所有可用的格式字元集。我們先用簡單的例子來看看pack的用法。
引用:
my $data1 = length(pack("s l", 23, 6874192));
my $data2 = length(pack("s l", 463, 66250921));
print "$data1\t$data2";
結果我們發現兩個長度都是一樣的,沒錯,因為Perl依照我們的格式把資料打包起來了。也就是整數以兩個位元組,長整數以四個位元組來儲存。當我們取得被打包過的資料後,我們可以用unpack把已經打包起來的資料解開,以便取得原來的資料格式,簡單的寫法就像這樣:
引用:
my $pack = pack("s l", 23, 6874192);
my ($short, $long) = unpack("s l", $pack);
print "$short\t$long";
另外,你可以在格式字元後使用重複次數的方式,例如使用"cccc"來表示重複使用c四次,不過一般來說,我們都不會這麼用,因為使用c4可以達到同樣的效果,而且當然更簡潔易讀。只是當你使用數字來代表重複的次數時,有一個要注意的部份,因為有些格式字元的數字並不是用來表示重複的用法,例如你使用a6其實是表示長度為6的字串。當然,如果你的字串長度不足,Perl是會幫你補進NULL的。
其實使用pack/unpack這一組打包資料的函式的另一項好處就是你可以利用固定的長度來儲存某些特定的資料爛位。例如你訂好某些資料的欄位,接下來當你把打包過的資料存到檔案裡面,那麼你就可以保證每一比資料的長度都是相等的。以後當你要查某一筆資料的時候,你可以直接用seek的方式,迅速的找到資料所在的位置。
回頭來看我們的通訊錄吧!我們現在希望儲存不只一種欄位,所以使用pack來把各種需要的欄位打包在一起,當成雜湊的值存入DBM資料庫中。所以我們先使用pack來儲存電話跟行動電話兩個欄位吧,首先要先訂出需要的長度。假設他們各是長度為12的字串,所以我們應該這麼作:
引用:
my $packed = pack("a12 a12", $tel, $cellphone);
$HASH{'John'} = $packed;

my $data = $HASH{'Mary'};
my ($mary_tel, $mary_cell) = unpack("a12 a12", $data);
接下來,你可以試著用DBM作簡單的資料管理,而且可以使用多個欄位。

15.2 DB_File

另外,使用Perl,你也可以簡單的使用雜湊的方式來存取Berkeley DB,而所需的模組DB_File目前已經內附在Perl的核心當中,因此你裝完Perl之後,如果你的系統中已經有Berkeley DB的話,你就可以在程式中直接使用DB_File來存取Berkeley DB。
使用DB_File其實非常容易,和使用dbmopen非常類似,你只需要把資料庫檔案和雜湊建立起繫結,那麼你就可以直接使用雜湊來控制資料庫的檔案,不過實際的使用上,還是有著很大的差別。最簡單的使用方式,其實只需要這麼作:
引用:
$filename = "test";
tie %hash, "DB_File", $filename;
接下來,你就可以把建立起繫結(tie)的雜湊直接使用,就像一般雜湊一般。於是我們就直接使用%hash來存取,就像這樣:
引用:
$hash{'John'} = '27365124';
$hash{'Mary'} = '26421382';
沒錯,完全就只是雜湊的操作,感覺被騙了嗎?其實在操作上,最簡單的方式也就是這樣,跟你直接使用dbmopen沒有相差太遠。不過就使用上而言,DB_File還是有些比較方便的地方,例如你可以在建立繫結的時候就指定檔案的權限。另外,你也可以控制相同雜湊鍵的處理方式。就像這樣的寫法:
引用:
$DB_BTREE->{'flags'} = R_DUP;          # 允許鍵值重複
$tie = tie %h, "DB_File", "test", O_RDWR, 0644, $DB_BTREE or die $!;
接下來,你就可以利用 $tie 來進行一些操作。你可以使用
引用:
$tie->del($key);
$tie->put($key, $value);
$tie->get($key, $value);
等等方式來對資料庫進行方便的存取。

可是你慢慢會發現,這樣的資料庫雖然方便,可是有時候卻未必能夠滿足我們的需求。於是我們也許可以轉向求助於關聯式資料庫,雖然它的資料庫形式複雜不少,但是以功能來說,卻能夠讓你在處理大量資料時還能夠隨心所欲。

15.3 DBI

接下來,我們假設你已經知道的需求,你對於簡單的DB_File,也就是Berkeley DB所提供的簡易資料庫再也無法滿足。你需要更強大的資料庫功能,最好是能應付各種複雜狀況的「關聯式資料庫」。你的資料需要大量的表格來儲存,表格和表格間也許還存在著交纏的連結與相關性。總之,至少在這時候,你應該要有「關聯式資料庫」的概念,當然也要可以自己獨立掌握資料庫的操作,因為我們並沒有打算在這裡教導大家怎摩使用資料庫。當然,目前在開放源碼社群經常使用的MySQL也是屬於「關聯式資料庫」,你也可以在網路上找到非常完善的相關文件與使用手冊等等。所以如果你對於這部份還不太熟悉,那麼可以先閱讀相關的資料後再回頭來看這一節,利用Perl連結到各種的關聯式資料庫。
不過在正式上路之前,我們還是必須來看看在Perl的使用上,整個DBI的概念。所謂的DBI,其實是Database Interface,所以其實對於DBI來說,它只是一個讓使用者可以降低成本的方式去控制各種資料庫,也就因此,它的使用必須搭配所謂的DBD,也就是Database driver。

其實使用者所接觸到的部份幾乎只有DBI的部份,只不過在使用前必須根據使用者實際搭配的資料庫安裝DBD。當然,我們在這裡也就只針對DBI的部份介紹。可是因為DBI的使用其實是利用Perl物件導向的程式寫作方式來進行,而我們並沒有介紹物件導向的程式寫法,所以對於大多數的人來說也許有些困擾,不過一開始就讓初學者學習怎麼使用Perl的物件導向寫法似乎太過躁進。不過即使還沒開始自己動手寫物件導向程式前,當然也可以安心的使用已經現成的各式各樣物件導向模組。而且目前在CPAN上有不少模組都已經是以物件導向的方式開發,因此使用者也可以利用學習DBI的機會,順便也學學物件導向模組的使用
引用:
一般來說,我們在開始使用一個物件時,都會先利用建構元來產生一個物件。有些程式語言也許是使用new
這個「方法(method)」來達到進行這樣的工作,在Perl的物件導向語法中,雖然也有很多模組使用new
來達成建構一個物件的目的,不過Perl本身的物件導向語法並不強制規定建構方式的關鍵字。所以你可以使用

$dbh = DBI->connect(...);

來建構出一個DBI物件,然後使用$dbh來進行各式各樣該物件所提供的方法。
一開始,你顯然要先在你的程式使用DBI這麼模組,接著就是連接上你的資料庫,就像這樣:
引用:
use DBI;

my $dbh = DBI->connect("dbig:dbname=foo", "user", "passwd");
其中的Pg其實代表的是資料庫的驅動程式部份(Pg 指的是PostgreSQL的驅動程式),其實比較精準的用法應該是這樣:
引用:
$dbh = DBI->connect($data_source, $username, $auth, \%attr);
其中的$data_source也就是描寫相關的資料庫驅動以及所要連結的資料庫等資訊,當然,如果需要,你還必須把資料庫所在的主機位置也寫在這裡,就像這樣:
引用:
$dbh = DBI->connect("dbig:dbname=foo;host=db.host", "user", "password");
另外,由於DBI可以處理相關失敗時的錯誤狀況,如果連接資料庫失敗,它會傳回錯誤訊息:$DBI::errstr。所以我們在進行資料庫連結時,可以使用這樣的方式:
引用:
$dbh = DBI->connect("dbig:dbname=foo", $user, $passwd) or die $DBI::errstr;
有些時候,你可能不知道你的資料庫是不是已經安裝了專門給DBI使用的驅動程式,這時候你可以利用這樣的方式來取得目前機器上可以使用的所有驅動程式的陣列:
引用:
@available = DBI->available_drivers;
連接上資料庫之後,最簡單的方式就是直接執行你的SQL命令,所以我們要用最簡單的方式應該是這麼寫的:
引用:
my $sql = "DELETE FROM foo";
my $return = $dbh->do($sql);          # 傳回受到改變的資料筆數
這樣的方式是讓你執行SQL語法的最簡單方式,不過它的主要限制在於並沒有辦法傳回你從資料庫中選取的資料。因此大多數的時候,我們都會使用其他的方式來進行資料庫的select動作。一般的方式大概都會是這個樣子:
引用:
my $sql = "SELECT * FROM foo";
my $sth = $dbh->prepare($sql);
$sth->execute;
while (my @result = $sth->fetchrow_array) {
    print $result[0];
    # @result 回依序取得每筆資料的各欄位值
}
這樣是非常常見的寫法,主要就是用來從資料庫取出某些特定的資料。這是因為我們並不能直接使用do這個方法來執行select的語法,因為那並不會得到我們真正想要的內容。所以我們還是先寫好我們要取值的SQL語法,也就是"select * from foo"這個敘述。接下來,我們要告訴$dbh,處理我們所要的這個SQL敘述,並且建構出一個敘述的控制器(statement handler),因為我們在進行select的時候,大多是透過執行某個select語法,接著一筆一筆取回執行的結果。而敘述控制器剛好就是為了這樣的需求而存在的。因此在利用prepare產生出控制器之後,我們就可以要求控制器執行我們的SQL敘述。緊接著利用while這個迴圈逐筆的把取得的資料當到其他的變數供我們使用。
而取回的方式最簡單的就是利用陣列的方式,也就是利用fetchrow_array這個方法來一筆一筆,所以每當fetchrow_array執行後有取得結果,就會讓while敘述成真,因此我們就可以從陣列@result中取得該筆的值。而陣列的元素則是依照資料庫select出來的結果來排序。
所以很多人並不喜歡這樣的用法,因為當你的資料表格欄位足夠多時,你會發現這種用法真的讓人很頭痛。你看看下面的例子吧:
引用:
my $sql = "SELECT a, b, c, d, e, f, g, h, i, j FROM data_table";
my $sth = $dbh->prepare($sql);
$sth->execute;
while (my @result = $sth->fetchrow_array) {
    ....
}
然後你希望取出欄位e跟i來作運算,於是你就要從0開始算,算出e是索引為4,i則是索引為8的欄位。這種事情真的是太辛苦了,當然,你的欄位數目也許還會更多,那時候你可能會想要找一片堅固一點的牆了。所以我們還是寧願使用雜湊的方式來取得資料庫送出來的值,也就是DBI提供的fetchrow_hashref。至少使用雜湊參照的方式能夠讓使用者在取值時更為直覺。如果我們改寫剛剛的取值方式,應該可以這麼使用:
引用:
while (my $hash_ref = $sth->fetchrow_hashref) {
    print $hash_ref->{'e'};        # 你可以直接叫用欄位名稱
    print $hash_ref->{'i'};        # 這樣顯然愉快了
}
另外,有時候你還會需要更方便的形式來取出資料,其中常用的包括了fetchall_arrayref跟fetchall_hashref。其中的用法會是類似這樣的形式:

於是,你在也不需要手動計算某個欄位應該位於陣列的第幾個元素了,希望這樣能夠讓你心情愉快的寫程式。可是你又發現,很多時候,你只是打算先把全部的資料從資料庫擷取出來之後,再一次進行運算,還是乾脆包成一個陣列,丟給副常式去處理,那麼你可能會寫成這樣的方式:
引用:
$arrayref = $sth->fetchall_arrayref;
或是
$hashref = $sth->fetchall_hashref($key);
這兩種方法都會一次傳回使用者所選定的條件,只是傳回值儲存的方式有所差異。其中特別有趣的是fetchall_hashref這個方法,使用者可以選定資料庫鍵值欄位,並依此來得到相關的其他欄位值。假如我們剛剛的眾多欄位中,欄位'a'是資料表的Primary Key,那麼我們就可以使用這種方式,取出特殊鍵值的那一筆資料:
引用:
$hashref = $sth->fetchall_hashref('a');
print $hashref->{foo}->{c};
這一行很顯然的,我們使用了fetchall_hashref這個物件的方法,重要的是我們指定了主要鍵的欄位'a'。於是敘述控制器到資料庫選出了我們要的所有資料,並且以主要鍵作為雜湊的鍵,而它的值則是其他個欄位鍵,值所形成的雜湊參照。因此我們要取得某個主要鍵值為'foo'這筆資料中,欄位'c'的值,就可是使用第二行的方式取得。

以上我們提到的大多是一般在進行一次的DBI操作時會使用到的方法,可是整個DBI的操作卻是相當複雜,提供的功能也非常強大,要詳細討論的話,都可以寫出一本書來。不過即使你沒打算買回DBI的書仔細鑽研,在你可以使用簡單的DBI操作之後,我們還是建議你看看DBI的官方文件,你可以直接使用perldoc DBI來閱讀。

你以為我忘了另外一個重要的部份了嗎?當然沒有。接下來我們要來看看在你對於資料庫的操作結束時,你應該要作的工作就是把它釋放,也就是利用disconnect的方法來釋出它所佔用的資源。雖然Perl會在程式結束時自動釋出還沒佔用的相關資源,可是我們還是強烈建議你,你應該在使用完DBI的資源後,手動將它關閉。因為你的程式也許會在系統運作一段時間,而且你卻只有某一段程式使用了這些資源,更有甚者,你也許一次開啟了多個資料庫連結,那麼適時的釋放這些資源顯然是程式寫作的好習慣。
引用:
$dbh->disconnect;
15.4 DBIx::Password

相對於DBI的複雜程度,DBIx::Paswsword只能算是協助使用DBI的一個小小工具。它也沒有提供類似DBI那樣魔術般的強大功能,簡單的說,它只是讓你用比較偷懶的方式來建構出一個資料庫控制物件,也就是所謂的database handler。別忘了,資料庫是相當重要的,我們經常要幫資料庫設定出繁雜的密碼,以避免遭人破解。可是這些密碼到底有多複雜呢?很多時候,連管理人員也總是搞不清楚這些密碼,再加上如果你的伺服器上還有不只一個資料庫,或是你的程式要連接多部資料庫,而且它們還分屬於不同的種類,那麼要寫個程式倒也相當辛苦。
針對這個需要考驗程式設計師記憶力的問題,DBIx::Password提出了解決的方式。也就是只要設定一次,那麼設定會被寫入Password.pm這個檔案之中。當你第一次安裝DBIx::Psassword這個模組時,它會問你一堆問題,其實也就是問你目前使用中的資料庫設定。而所有的資料將會像這樣的存在資料庫中:
引用:
my $virtual1 = {
    'dbix' => {
        'database' => 'db_name',  # 資料庫名稱
        'password' => 'passwd',          # 使用者密碼
        'attributes' => {},          # 連接資料庫的其他參數
        'port' => '',
        'username' => 'user',          # 使用者名稱
        'host' => 'localhost',          # 主機
        'driver' => 'mysql',          # 資料庫類型
        'connect' => 'DBI:mysql:database=db_name;host=localhost'
    },
};
那麼以後當你要連接資料庫時,就只需要使用虛擬的名稱了,原來建構資料庫控制器的方式也由DBI轉移倒DBIx。所以使用者再也不需要記一長串的資料庫連接需要的參數,因為所有的東西現在都由DBIx::Password負責了。讓生活快樂一點的寫法就變成了:
引用:
my $dbh = DBIx::Password->connect($user);
剛剛我們舉的例子中,就可以把$user設為"dbix",那麼DBIx::Password就會幫忙我們使用DBI進行資料庫的連結。而且在DBIx::Password中,你當然可以設定多組的參數,因為它只是利用雜湊存起你所有資料庫的相關資料。

雖然DBIx::Password還提供其他的一些操作方式,不過其實我們幾乎有超過百分之九十的機會都是使用connect這個物件方法,所以我們應該暫時可以先下課了。

習題:
1. 利用自己熟悉的資料庫系統(例如 MySQL 或 Postgres),建立一個資料庫,並且利用DBI連上資料庫,取得Database Handler。
2. 試著建立以下的一個資料表格,並且利用Perl輸入資料如下:
引用:
資料表格:
name: varchar(24)
cellphone: varchar(12)
company: vrchar(24)
title: varchar(12)
資料內容
[ name: 王小明
  cellphone: 0911111111
  company: 甲上資訊
  title: 專案經理 ]
[ name: 李小華
  cellphone: 0922222222
  company: 乙下軟體
  title: 業務經理 ]
3. 從資料庫中取出所有資料,並且利用fetchrow_array的方式逐筆印出資料。
4. 呈上題,改利用fetchrow_hashref進行同樣的工作。

TOP

16. 用Perl實作網站程式

很多人開始接觸Perl都是因為把他拿來作為寫CGI(Common Gateway Interface)程式的工具,當然,也因此不少人都把Perl定位在「寫網站程式」。雖然事實絕非如此,不過用Perl來寫CGI也確實是非常方便的。尤其在不少前輩的努力之下,讓我們現在得以更方便的建立網路相關的程式。雖然目前已經有其他非常方便的腳本語言(scripting language)也可以讓人非常輕鬆的寫出CGI程式,例如像PHP或ASP一開始就是以最方便的嵌入HTML來進行互動式網頁作為主要目的(目前PHP已經把觸角延伸到其他方面,例如PHP-GTK),可是Perl所依賴的不單單只是方便的CGI寫成模式,更重要的是程式語言本身所能達到的效果。
很多人在準備開始寫CGI的時候都會遇到類似的問題:「我應該學PHP或是Perl?」其實如果你大多數的時候只希望把Perl拿來寫網站,而且你手上的東西又非常的急迫,那麼PHP也許可以很快的讓你達到目的。雖然很多人可能不以為然,不過我倒是以為,可以把學Perl當成純粹的學習一種程式語言,而CGI只是一種實作Perl GUI(圖形使用界面,Graphical User Interface)的方式。何況越來越多人把瀏覽器當成是GUI以及Client/Server架構最容易的達成方式,其中當然也因為使用瀏覽器作為用戶端程式可以降低使用平台的困擾,而Perl正是驗證了這種詮釋。
利用Perl來寫CGI程式的另外一個最大的疑慮大概是Perl的效率問題,這也許要從網站結構跟原理說起。一般來說,整個網站的原理並不算太困難,也就是用戶端發出一個需求,伺服器端收到之後,根據用戶的需求發出回應,然後關閉兩者的連線。而如果想要達到動態網站的目的,也就是根據使用者的需求,由伺服器端在收到需求之後,根據伺服器的設定把資料傳給後端的程式,接著程式依照需求產生出結果之後再傳回給網站伺服器。接著,網站伺服器就根據正常的流程把資料傳回給用戶端。我們可以從圖一看到比較清楚的流程。
在正常的狀況下,Perl每次收到由網站伺服器傳來的需求時,就會重新開啟一個程序(process),然後開始根據需求來產生結果。可是問題在於Perl在初始化的過程必須耗費相當的時間跟記憶體,因此當Perl完成初始化,然後產生出適當的結果並且回傳給網站伺服器。這樣的過程其實是相當漫長的,尤其當你的伺服器負載過大,或是硬體本身的效率不佳的時候,就會壤人感到非常的不耐。而這也經常是Perl作為CGI程式最讓人疑慮的部份。
當然,這部份也已經有解決的方案了。現在使用者可以利用FastCGI,或是在Apache中搭配mod_perl使用,這樣一來就可以讓原來的程式被保留在記憶體中,而不必因為每次有使用者發出瀏覽的需求就必須重新啟動Perl,造成因為Perl初始化的延遲問題。不過如果想要有更好的效能來使用mod_perl,那麼足夠的記憶體就變成非常重要的。所幸硬體的價格不斷下降,讓這樣的資源使用不至於發生太大的問題。
如果要使用Perl來增進網站的速度,是需要進行一些設定上的改變,當然有時候能夠跟程式配合是更理想的。不過這對於剛開始準備使用Perl來作為網站程式的工具而言顯然是有些困難的,所以我們並沒有要在這一章中介紹更多相關於mod_perl的使用。待各位對於使用Perl來建構出網站這樣的工作熟悉之後,可以繼續選擇相關書籍或文件進行研究。

16.1 CGI

現在使用Perl作為網路應用程式的主要程式語言時,幾乎所有人第一個會遇到的就是CGI這個模組。CGI模組提供的功能非常的強大,不管是輸出至網頁上或是透過CGI取得使用者輸入的參數,另外也可以利用CGI動態產生HTML的各種表單選項。對於動態網頁的支援,CGI目前已經可以算是非常完善。甚至有時候會讓人感到相當意外,因為有些時候你會忽然發現,原來這些東西CGI.pm也可以達到。
現在我們就來看一下,如果要使用Perl開始寫CGI程式,那麼應該怎麼下手呢?毫無疑問,你總得先載入CGI這麼模組,所以就像我們所熟知的方式,先在你的程式加入這一行吧:
引用:
use CGI;
接下來,我們應該建立一個新的CGI物件,方法也不太難,也就是利用new這個物件的操作方法。所以只要這樣子就可以建立起CGI物件:
引用:
my $q = CGI->new;
如果你想要把任何內容輸出到網頁上,你大概都要先送出HTML的標頭檔到網頁上,也就是所謂的header。最簡單的方式當然就只要這麼寫:
引用:
print $q->header;
不過其實header有很多的參數,例如你應該要可以設定內容的編碼或輸出的內容型態等等。所以header這個函式也允許你使用相關的參數來改變header的屬性,例如你可以設定網頁輸出的內容編碼為UTF-8:
引用:
print $q->header(-charset => 'utf-8');
而且你可以一次指定多種屬性,就像這樣的方式:
引用:
print $q->header(-charset => 'utf-8',
                 -type => 'text/html');
當然,HTML的標頭檔還有各式各樣的屬性,使用者也都可以利用header來設定,而且這樣子的方式似乎也比起手動輸入各種header的設定值要簡潔不少,因此這個函式對於經常使用CGI的人來說還是非常方便的。而另一個重要的功能則是CGI的param,也就是利用CGI傳回來的參數。很多時候,我們都會利用這樣的方式來取得由HTML表單中傳回來的欄位值:
引用:
如果你的HTML裡面寫的是這個樣子:

<input type="text" name="user">

那麼你的Perl就可以這麼使用:

$q->param('name');
也就是藉由HTML裡面的表單欄位名稱作為param的參數來取得使用者輸入的值,這樣就可以讓程式設計師很方便的取得使用輸入的結果。舉個簡單的例子吧!如果我們想製作一個使用者登入的程式,那麼畫面上大多不外乎就是使用者名稱密碼兩個欄位。所以我們通常的作法就是利用取得的使用者名稱去資料庫搜尋相對應的密碼資料,如果沒有相關的資料就表示沒有這個使用者,或是使用者輸入的使用者的名稱有誤。如果可以找到相對應的使用者,那麼我們就比對資料庫中擷取出來的密碼與使用者輸入的是否一樣。而程式的寫法大概就像是這樣:
引用:
#!/usr/bin/perl -w

use strict;
use CGI;
use DBI;

my $q = CGI->new;
print $q->header;
my $user = $q->param('user');
my $dbh = ('DBD:mysql:database=foo', 'user', 'passwd');
my $sql = "SELECT password FROM table WHERE user = '$user'";
my $sth = $dbh->prepare($sql);
$sth->execute;
unless (my @pwd_check = $sth->fetchrow_array) {
    print "沒有這位使用者";
else {
    unless ($pwd_check[0] eq $q->param('passwd')) {
        print "密碼錯誤";
    else {
        ......
    }
}
有些時候,你會希望可以一次取得所有由HTML表單傳過來的欄位名稱,這時候CGI的另一個函式就可以派上用場了。也就是可以使用params這個函式,它會傳回一個陣列,這個陣列就包含了所有由HTML傳來的表單欄位。因此你的程式中可以這樣寫:
引用:
my @params = $q->params();
我們一開始就提到了關於利用CGI這麼模組送出動態header的方式,其實它不只能夠送出檔頭,你還可以動態的產生其他的網頁元素,例如你可以使用這樣的方式來印出字級為h1的文字內容:
引用:
print h1('這是h1級的字');

或是使用

print start_h1,"這是h1級的字,end_h1;

結果都可以產生

<h1>這是h1級的字</h1>

這樣的HTML內容
類似的用法還有包括下面幾種:
引用:
start_table()  # 送出<table>這個標籤
end_table()  # 送出</table>這個標籤
start_ul()  # 送出<ul>
end_ul()  # 送出</ul>
當然,你還可能不直接送出header,因為你想要在處理完某些狀況之後,把網頁的位址送到其他地方。這時候你就應該使用redirect這個函式。它的用法也是非常的簡單,你只需要告訴它你想轉往的其他網址就可以了。
引用:
$q->redirect('http://url.you.want/');
至於HTML的元素,也有很多時候你可以利用CGI這麼模組動態產生,例如你可以使用img來產生出<img src="">這樣的HTML標籤。其他諸如ul,comment等等也都能夠輕易的被產生。不過另外一組非常完整的則是HTML表單的產生方式,也就是你可以利用CGI來產生大多數的表單欄位。
一個非常基本的例子,就是利用CGI模組來產生一個text的表單欄位。
引用:
print $q->textfield( -name=>'field',
                     -default=>'default value');
另外的一些欄位也都可以用類似的方式產生,就像接下來的例子:
引用:
print $query->textarea( -name => 'textarea',
                        -default => '會放在欄位的預設值',
                        -rows => 5,
                        -columns => 10);
print $query->password_field( -name => 'password',
                              -value => '這裡也是預設值',
print $query->checkbox_group( -name=>'checkbox',
                              -values=>['value1','value2','value3','value4'],
                              -default=>['value1','value3'],
                              -linebreak=>'true',
                        -labels=>\%labels);
當然,還有各式各樣的表單欄位,使用的方式也都大同小異,事實上,CGI模組的文件中有詳細的描述。不過有一個非常重要的卻是不可忽略的,也就是錯誤訊息。當使用者使用CGI進行一些運作時,有時候會有一些錯誤,這時候CGI模組會透過cgi_error來傳回錯誤的訊息,所以你可以在程式中加上這個函式,讓CGI發生錯誤時能送出錯誤訊息。就像這樣的方式:
引用:
if ($q->cgi_error) {
   print "無法處理CGI程式";
   print $q->cgi_error;
}
接下來,我們再來講一個CGI模組的重要功能,也就是cookie的存取。許多時候,網站設計者為了減少使用者重複輸入資料的困擾,或是取得使用者瀏覽的紀錄,常常會藉由用戶端瀏覽器的cookie功能來紀錄一些相關性的資料。而CGI也可以針對這些cookie進行存取。如果我們想要取得已經存在用戶端瀏覽器上的cookie資料,我們只需要這麼作:
引用:
use CGI;
my $q = CGI->new;
my $cookie = $q->cookie('cookie_name');
接下來,我們當然也可能需要寫入cookie到使用者端,那麼我們可以利用CGI的檔頭來完成這項工作,也就是HTML的header。而完整的用法就是先把你所要寫入的cookie值,屬性都先設定好,然後直接用header來把這些內容送給使用者的瀏覽器中。
引用:
$cookie = $q->cookie( -name=>'cookie_name',
                      -value=>'value',
                      -expires=>'+1h',
                      -domain=>'.my.domain',
                      -secure=>1);
    print $q->header( -cookie=>$cookie);
很顯然的,CGI的功能還不止於此,雖然我們已經介紹了大部份使用CGI時常用的功能。不過如果你還有進一步的需求,應該務必使用perldoc CGI來詳細閱讀CGI的相關文件。

16.2 Template

Template雖然不單單用於網路應用程式中,可是卻有許多人在寫網站相關的程式時總會大量的使用,因為對於能夠讓程式設計師單獨的處理程式而不需要擔心網頁的設計對於許多視設計為畏途的程式設計師而言,實在是非常的重要。
Template其實完整的名稱是Template Toolkit,因為目前的Template Toolkit的版本是2.13,所以一般人又習慣稱呼目前的為TT2。至於正接受Perl基金會發展的則是Template::Toolkit的第三版,也就是俗稱的TT3。當然,既然可以接受Perl基金會的贊助開發新版Template::Toolkit,可見這個模組對於Perl社群的重要性了。
首先我們先來談談template系統的概念,讓大家能更深刻的感受使用template系統對程式設計師在網站程式的重要性。我們在剛剛可以看到CGI這個模組的運作,所以我們可以透過CHI的使用,輸出絕大多數的HTML標籤,也就是完成一個HTML頁面的輸出。當然,還有一種可能就是直接使用print指令,一個一個的把HTML標籤手動印出。可是不管是上面兩種方式選擇哪一種都會遇到相同的問題,也就是要怎麼跟網頁設計者一起合作來完成一個美觀,功能又強的網站呢?有一種可能的方式也許是由設計者做好一般的HTML頁面,然後你將這個頁面當作一般的文字檔案將它逐行讀入。然後自行置換掉要動態產生的部份。如此一來,只要網頁設計者能在網頁中加上讓你的程式可以認得的關鍵字,那麼你就可以不理會畫面的改變,而且還能夠讓程式正確的執行。這樣的概念正是孕育出模板系統的主要想法。
Perl的模板系統其實不只一種,可是Template Toolkit卻是深受許多人喜愛的,因為它不但可以讓使用者把模板與程式碼分隔,也可以讓程式設計師在模板中加上各式各樣的簡單控制。雖然說簡單,可是功能卻也一點也不含糊。因為使用者可以在裡面使用迴圈,可以取得由程式傳來的各種變數,當然也可以在模板中自己設定變數。當然,最後如果所有的方法都用盡,還不能達成你的要求,你也可以在模板裡面加上Perl的程式碼。
基本的使用TT2,你應該要有一個Perl的程式碼,跟相關的模板。而如果你只需要單純的變數替換,那麼使用的方式非常的簡單。你可以這麼寫:
引用:
use Template;

my $config = {
    INCLUDE_PATH => '/template/path',
    EVAL_PERL    => 1,
};

my $template = Template->new($config);

my $replace = "要放入模板的變數";
my $vars = {
    var  => $replace,
};

my $temp_file = 'template.html';
my $output;
$template->process($temp_file, $vars, $output)
    || die $template->error();

print $output;
這時候,你就必須有一個符合Perl程式碼中指定的模板檔案,已就是template.html。而在這個模板中,大多數都是一般的HTML標籤。而需要被置換的變數則被定義在$vars中。我們可以先來看看這個templte可能的形式。
引用:
<html>
    <head>
  <title>這是TT2示範</title>
    </head>
    <body>
  這裡可以替換變數 [% var %]
    </body>
</html>
好了,現在你可以把這個HTML拿去給網頁設計的人,讓他負責美化頁面,只要他在設計完頁面後,能把關鍵的標籤[% var %]留在適當的位置就可以了。不過你可不能輕鬆,讓我們來研究一下Template是怎麼運作的。首先,在我們新建立一個Template的物件時,我們必須設定好相關的內容,在這裡的例子中,我們只有設定了兩個參數,一個是INCLUDE_PATH,也就是你的template檔案所放置的位置,這裡的內容可以是一個串列。也就是說,你可以指定不只一個路徑。另一個我們設定的是EVAL_PERL這個選項是設定是否讓你的Template執行Perl的區塊。當然,選項還有好幾項,例如你可以設定POST_CHOMP,這個選項跟chomp函式有一些類似,它可以幫你去除使用者參數的空白字元。另外,還有PRE_PROCESS的選項則是設定所有的模板在被載入之後,都必須預先執行某個程序,例如先把檔頭輸出到模板中等等。另外,你還可以修改Template預設的標籤設定,例如我們剛剛看到的[% var %],就是利用Template的預設標籤[% %]把它表現出來。而Template允許你在建立Template物件時使用START_TAG跟END_TAG來改變這樣的預設標籤。如果你用了這樣的方式:
引用:
my $template = Template->new({
                   START_TAG => quotemeta('<?'),
                   END_TAG   => quotemeta('?>'),
             });

那麼剛剛在模板裡的變數就應該改寫成

<? var ?>

這樣似乎跟PHP有一點像了。
不過Template的作者也知道很多人大概很習慣PHP或是ASP的標籤,所以在Template也提供了另一個設定選項,也就是TAG_STYLE。你可以設定成php(<? ... ?>)或是asp(<% ... %>),不過其實我個人以為[% ... %]的原創形式還算順手,所以倒是沒換過任何其他標籤風格。Template的設定選項非常多樣化,不過只要瞭解以上的這些項目就大概都能應付百分之八十的情況了。如果還需要其他的資訊,則可以參閱Template::Manual::Config。
接下來,我們來看看使用上有甚麼需要注意的。在程式中,基本上你如果有甚麼特別需要注意的部份,那大概就是變數的傳遞了。如果你要傳一個純量變數,跟我們剛剛的範例一樣,那麼就只是把變數指定為雜湊變數的一個值。就像我們剛剛的用法一樣,你就只要指定:
引用:
my $vars = {
   var => $replace
};
可是很多時候,我們也許會傳送一整個陣列或是雜湊,那麼這時候你最方便的方式就是傳送這些變數的參照,寫法也許就像是這樣:
引用:
my @grades = (86, 54, 78, 66, 53, 92, 81);
my $vars = {
    $var => $replace,
    $var2 => \@grades,
};
這時候,你的模板內容顯然也需要改寫,把印出陣列的這部份加入你的模板中,一般來說,我們可以使用Template提供的FOREACH迴圈。所以你可以在你的template加上類似的一塊:
引用:
[% FOREACH grade = grades %]
成績:[% grade %]
[% END %]
當然,除了陣列,我們還可以使用雜湊。使用的方式卻也不太一樣,雖然你還是傳遞雜湊參照,可是在模板中的使用卻是直接利用雜湊鍵。所以你的Perl程式碼跟模板中分別是這麼寫的:
引用:
my %hash = ( height => 178,
          weight => 67,
       age => 28 );
my $vars = { var => \%hash };

於是你必須在模板中做出相對應的修改:

[% var.height %]
[% var.weight %]
[% var.age %]
我們剛剛看到在模板中放了FOREACH這個Template提供的迴圈,其實模板中還有許多可供利用的特殊功能,例如一個非常方便的就是[% INCLUDE %]。很多時候,人們都喜歡在網頁的某個部份加上一些制式的內容,最常見的當然就是版權說明了。所以我們可以把這些版權說明的內容放到某一個檔案中,例如就叫做copyright.tt2吧!所以我們有了一個template檔案,內容其實就是一些文字敘述:
引用:
copyright.tt2:
Copyright Hsin-Chang Chien 2004 - 2005
好了,現在我們有不少其他的模板檔案,希望每一頁都能加上這一段內容。那麼我們只需要在這些檔案的適當位置加上這樣的一行()所謂的適當位置就是你希望看到這些內容的位置:
引用:
[% INCLUDE copyright.tt2 %]
這樣的寫法可以讓你一次省去相當多的麻煩,尤其當你有可能更動這些檔案的內容時。例如我現在想把版權的內容進行調整,那麼你只需要修改copyright.tt2一個檔案,而不需要把所有的檔案一個一個叫出來修改。不過其實INCLUDE可以應付比較複雜的模板系統,而像版權聲明這種純文字的檔案,還有更簡潔的載入方法,也就是INSERT,如果你要被載入的檔案是一個純文字檔,不需要Template幫你進行任何的處理,那麼就考慮使用[% INSERT %]吧!
另外,Template還支援另一種常用的重複敘述,也就是WHILE。它的語法相當簡單,也就是使用這樣的區塊把要執行的內容標示出來:
引用:
[% WHILE condition %]
   ....
[% END %]
而IF敘述的基本用法也是和Perl語法幾乎一樣只是他是使用大寫字母,而且用Template的標籤區隔出來,所以你可以輕鬆的使用:
引用:
[% IF condition %]
   ....
[% END %]

或是

[% IF condition %]
   ....
[% ELSE %]
   ....
[% END %]

而ELSIF也是被允許的,用法也是類似:

[% IF condition %]
   ....
[% ELSIF condition2 %]
   ....
[% ELSE %]
   ....
[% END %]

當然,你還有UNLESS可以使用,用法也是相同:

[% UNLESS condition %]
   ....
[% END %]
至於如果你真的想在模板裡面寫Perl程式,也只需要這麼作:
引用:
[% PERL %]
   # perl 程式碼
   ....
[% END %]
只是我個人並不建議你常常需要這麼使用,否則你有可能其實是挑錯工具了。因為Perl有其他模板系統也許比較符合你的習慣跟需求。而我們接下來就要介紹另一種在Perl社群中最近非常風行的另一套網路應用程式的搭配系統,也就是Mason。至於Template,它還有很多奧妙,你可以試著參考相關的官方文件。

16.3 Mason

網頁設計跟程式碼怎麼切割,這個想法會根據寫程式的人的習慣而有很大的差距。很多人喜歡利用像Template::Toolkit這樣的工具來讓網頁的版面跟程式碼分離的越乾淨越好。當然也有人認為像php形式的內嵌式作法可以讓網站的雛型在很短的時間就產生出來,因此Mason也就以類似的作法誕生了。因此在這一,兩年來,Mason已經成為非常重要的模組,尤其在作為網站的工具時。根據job.perl.org(一個專門張貼Perl相關工作機會的網站)上的資訊,Mason已經是許多國外企業在徵求網站相關程式開發人員時需求度很高的技術了。而且許多大型網站現在也都使用了Mason來產生他們的網頁內容,例如亞馬遜書店(訪客無法瀏覽此圖片或連結,請先 註冊登入會員 就是一例。
Mason基本的操作原理是在你的Apache中加上一個控制器(Handler),讓使用者的要求全部送給Mason處理,這樣一來,Mason就可以把各式各樣的使用者需求都預先處理好,然後送出合適的內容。使用Mason,你除了裝上HTML::Mason這個模組之外,你的Apache還必須支援mod_perl,在一切準備就緒之後,你可以在你的Apache中像這樣的進行設定:
引用:
PerlModule HTML::Mason::ApacheHandler

<Location />
    SetHandler perl-script
    PerlHandler HTML::Mason::ApacheHandler
</Location>
在Mason中,你可以使用百分比(%)符號作為Perl程式碼的引導符號,或是<% ... $>。如果是一行的開始是由%引導,那麼表示這一行是Perl程式碼。或是利用<% ... %>來設定一個區塊的perl程式碼。所以你的網頁可以像這樣子:
引用:
<%perl>
my $num = 1;
my $sum = 0;
while ($num <= 10) {
    $sum+=$num;
}
</%perl>
總和是: <% $sum %>
那麼結果就會在網頁上呈現出計算後的總和,因此他已經把網頁的HTML跟perl程式碼作了緊密的結合。尤其如果我們使用%作為程式行的起始,就更能表達出其中不可切割的關係了:
引用:
% $foo = 70;
% if ($foo >= 60) {
你的成績及格了
% } else {
你被當了
% }
我們常使用這樣的方式來把條件判斷穿插在HTML裡面,所以你的內容已經是夾雜各種語法的一個綜合體了。而且不像Template使用自己定義的特殊語法,你在Mason中使用的大多是標準的HTML跟Perl語法(雖然他們總是夾雜在一起)。所以你當然可以這樣寫:
引用:
% my @array = (67, 43, 98, 72, 87);
% for my $grade (@array) {
你的成績是: <% $grade %>
% }
接下來比較特殊的是一些Maon專用的元件,利用這些元件,你可以很容易的處理一些資料。例如使用者傳來的需求就是其中一個很好的例子。在Mason中最基本的兩個全域變數(每次使用者發出各種要求時,就會產生的兩個變數)分別是$r跟$m。其中$r是Apache傳來的需求內容,至於$m則是負責Mason自己的API。所以你可以藉由$r來取得由Apache傳來的資料,例如:
引用:
$r->uri
$r->content_type
不過我們暫時先不管$m這個負責處理Mason API的變數,因為我們還有更有趣的東西要玩。也就是在Mason頁面中常常會被使用的<%args>...</%args>區塊。這個區塊可以用來取得由使用者藉由POST/GET傳來的參數,一個很簡單的例子當然就是像這樣:
引用:
http://my.site/mason.html?arg1=value1&arg2=value2

於是我們就在mason.html裡面加上args
區塊,利用ARGS取得這些變數之後,我們就可以在頁面中自由使用了。

<%args>
$arg1
$arg2
</%args>
所以剛剛累加的程式,我們可以由使用者輸入想要累加的數字,這時候,我們只要把while的結束條件利用使用者輸入的變數來替換就可以了。
引用:
<%perl>
my $sum = 0;
while ($end > 0) {
    $sum+=$end;
}
</%perl>
總和: <% $sum %>

<%args>
$end
</%args>
看來應該不是太困難,只是有點雜亂。如果我們每次都想要在頁面開始之前,就先用perl進行一堆運算,判斷的時候,可以把<%perl> ... </%perl>這一大段的區塊搬離開應該屬於HTML的位置嗎?其實在Mason也替你想到了這個問題,所以在Mason中你可以使用<%init> ... </%init>這個區塊,也就是進行初始化的工作。我們把剛剛的那一段頁面的程式碼重新排列組合一下。
引用:
總和: <% $sum %>

<%init>
my $sum = 0;
while ($end > 0) {
    $sum+=$end;
}
</%init>

<%args>
$end
</%args>
這樣看起來顯然乾淨多了,不過記得我們在使用Template::Toolkit的時候有一個很不錯的概念,也就是[% INSERT %]/[% INCLUDE %]的方式,在Mason中也有類似,也就是利用<& ... &>的方式來載入你的自訂元件。用個簡單的例子來看:
引用:
<HTML>
<HEAD><TITLE>標題</TITLE></HEAD>
<BODY>
<TABLE>
% for my $grade (@grades) {
<TR><TD><% $grade %></TD></TR>
% }
</TABLE>
<HR>
copyright 2004-2005 Hsin-chan Chien
</BODY>
</HTML>

<%args>
@grades
</%args>
這時候,我們可以把前、後的HTML分別放到header跟footer兩個地方,然後利用<& ... &>來載入,所以這個內容就會被改寫為:
引用:
<& header &>
<TABLE>
% for my $grade (@grades) {
<TR><TD><% $grade %></TD></TR>
% }
</TABLE>
<& footer &>

<%args>
@grades
</%args>
這對於一整個網站維持所有網頁中部份元素的統一是一種非常方便而且有用的方式。而且任何在獨立的元素中被修改的部份也會在所有的頁面一次更新,這絕對比起你一個一個檔案修改要來得經濟實惠許多。尤其當你所進行的是一個非常龐大的網站時,更能瞭解這種用法的重要性。

不可否認,我們花了這麼多的頁面來講這三個目前在Perl社群中最被常用來進行網站程式的工具模組,卻只能對每一個部份做非常入門的介紹。畢竟這三個模組都是非常複雜而且功能強大的。另外的特點就是他們都可以詳細到各自出版一本完整的使用手冊。不過對於一開始想要嘗試使用這幾個模組的人來說,事實上也能用陽春的功能幫你進行許多繁複的工作了。別忘了,大多數的Perl模組或語法,你只要瞭解他們的百分之二十,就可以處理百分之八十的日常工作。

習題:
1. 以下是一個HTML頁面的原始碼,試著寫出action中指定的print.pl,並且印出所有欄位中,使用者填入的值。
引用:
<HTML>
    <HEAD>
        <TITLE>習題</TITLE>
    </HEAD>
    <BODY>
        <FORM ACTION="print.pl" METHOD="POST">
      姓名:<INPUT TYPE="text" NAME="name"><BR/>
      地址:<INPUT TYPE="text" NAME="address"><BR/>
      電話:<INPUT TYPE="text" NAME="tel"><BR/>
      <INPUT TYPE="submit">
  </FORM>
    </BODY>
</HTML>
2. 承上題,試著修改剛剛的print.pl,並且利用Template模組搭配以下的模板來進行輸出。
引用:
<TABLE>
    <TR><TD>姓名:</TD><TD>[% name %]</TD></TR>
    <TR><TD>地址:</TD><TD>[% address %]</TD></TR>
    <TR><TD>電話:</TD><TD>[% tel %]</TD></TR>
</TABLE>
3. 承上題,將利用Template輸出的部份改為HTML::Mason。

TOP

17. Perl與系統管理

Perl之所以能夠一直在腳本程式中佔有一席之地,有一些部份其實也是因為在系統管理中,Perl還能發揮著不錯的效用,而且在使用上也是非常方便。它能夠像shell script一樣,拿了就直接用,而不需要定義一堆變數,物件,物件的方法之後才開始寫程式碼。你可以找到相關的模組,然後非常迅速的完成你想達到的目的。所以它保留了shell script的方便性,卻又比shell script擁有更多的資源。當然優點也在某些程度上被當為缺點,例如有些人認為Perl程式碼非常的不夠嚴謹,因為相對於Java或Python,它顯然太過自由了。
其實Perl對於Linux/*BSD的方便性幾乎是不言可喻,很多時候在這些系統中都會預設安裝了Perl,因為Perl能夠相當程度的處理系統中的雜事。但是不只如此,如果你是一個系統管理員,Perl能幫你作的事情也許比你想像中的還要多。尤其當你需要對很多系統的資料進行搜尋,比對的時候,Perl就更能顯示它的重要性。何況很多時候,我們可以透過日誌檔(log)的分析來進行系統的監控跟效率的評比,而這也正是Perl的其中一項專長。這也就對於很多系統管理的統計部份(例如MRTG跟awstats)是藉由perl來達成能做出很好的解釋了。
這一章的大綱是根據Autrijus Tang在對一群準Linux系統管理員上課前,我們一起討論出來的結果,而內容則有許多是直接使用他上課中使用的範例。這些範例在原作中以開放文本的方式釋出,各位可以直接使用在測試或其他正式的產品中。

17.1 Perl在系統管理上的優勢
其實就像我們前面所提的,Perl在系統管理上有著非常重要的應用跟地位,對於許多Unix-like之類的作業系統管理來說,Perl經常是他們的好幫手。可是Perl到底有甚麼特別的優勢呢?除了我們剛剛提到的文字比對,處理的優勢外,Perl其實還有不少能夠吸引人的地方。其中最特殊幾點大概包括:

1. Perl的黏性特強:Perl被稱為是一種黏膠程式語言,他能夠用最省力的方式把各種東西綁在一起。作為一個系統管理員總是會大量接觸各式各樣的工具,因此需要整合這些工具的機會就非常的大,所以perl的優勢在這個時候就能夠容易的顯現出來。
2. Perl資源豐富:我們之前多次提到的CPAN就是最豐富的資料庫,不單單是網路程式,資料庫處理,其實就像系統管理相關的部份,也有為數不少的模組。這些模組不但是許多系統管理員實際用來管理的工具,當然也是他們的經驗。所以你看這些模組,不單單是找尋可供應用的工具,也可以藉此挖掘這些人管理系統的方式。
3. Perl的作業環境:除了Unix-like的各種作業系統能夠輕易的安裝,執行Perl之外,微軟的Windows或是OS/2,以及蘋果公司的Mac OS也都可以讓Perl正常的運作。因此很多時候,當系統管理者同時必須管理一種以上的作業平台時,也能夠輕易的使用相同的工具。
4. Perl幾乎是一種標準:這裡所謂的標準,其實是因為目前已經很多的系統管理工具都是使用Perl所開發出來的,所以如果系統管理人員如果可以擁有Perl的能力,那麼自己維護或修改這些系統的可能性就可以大大增加。

17.2 Perl的單行執行模式
很多系統管理員使用Perl當然是因為Perl的順手跟方便,就像我們說的,你總不會希望找一個檔案,或置換檔案的某些字串前還要先定義一大堆變數,或是先弄一個物件,然後拿來繼承,再利用被繼承的物件寫出置換字串的物件方法。然後又沒有好的正規表示式,然後你可能得用substr一個一個去找出來。最慘的可能是你寫完這樣的程式已經是下班時間,所以你還得加班把需要的結果搞定。而且你知道,程式寫出來不一定可以那麼順利,尤其你寫了那麼長的程式,有bugs也在所難免,然後......。於是,下場應該是可以預料的。
雖然很多人批評Perl非常不嚴謹,可是換個角度看,應該是說Perl允許你「隨手」寫出可以解決手邊工作的工具。而且Perl的「隨手」還不是普通的隨手。因為Perl提供單行執行模式,所以你可能可以看到這樣的一行程式:
引用:
perl -MEncode -pi -e '$_ = encode_utf8(decode(big5 => $_))' *
執行完這一行之後,你該目錄下的Big5檔案就會全部都變成UTF8了。這實在非常神奇,不是嗎?其實不只這樣,你還可以利用一行的程式把檔案中的某些字串一次換掉。就像這樣:
引用:
perl -pi.bak -e "s,foo,bar," *
這樣一來,所有檔案中的foo就全部被換成bar了。希望你沒有繼續想像如果你手動替換或是正在用其他結構嚴謹的程式語言在進行中。當然,如果你現在還在這裡,我們就利用那些堅持一定要有足夠完整的程式架構才能執行的傢伙還在奮鬥的時間,來看看怎麼執行單行的Perl吧。
首先,最基本的就是"-e"這個參數了,你可以使用perl -e來執行一行程式。不過請注意,我說的是一行程式,而不是一個敘述句,所以你當然可以這麼寫:
引用:
perl -e "$foo = 2; print $foo"
接下來,雖然只是一行的Perl程式,可是你還是可以使用Perl的其他各式各樣模組,如果沒有辦法使用模組,那單行的執行模式大概也沒有人願意使用了吧!所以我們使用了"-M"的選項來指定所要使用的模組,就像前面的第一個例子中,我們用了"-MEncode"來指定使用Encode這個模組。
接下來,我們要讓這一行程式能對所有我們指定的檔案工作,所以我們使用了"-p"這個選項這個選項允許使用者以迴圈的方式來執行這一行程式,在這裡也就讓我們可以使用*這個萬用字元來指定所有的檔案。接下來,我們看到另一個選項是"-i",這是讓perl會自動幫你進行備份,免得你對檔案進行操作之後,結果非常不滿意,卻還得手動改回來。所以當我們指定了"-i.bak"就是讓所有被修改的檔案在修改前就先備份一個副檔名為.bak的檔案。不過這是小寫i,如果你用了大寫的I,那意義可就大不相同了,"-I"讓你可以指定@INC的內容。

17.3 管理檔案

系統管理一開始大概都要面對一大堆的檔案吧,而基本的檔案管理其實還沒有太過複雜,只要你的shell夠熟,幾乎也可以應付很多狀況。例如你可以輕鬆的利用find找到你要的檔案,甚至禳他們排序,就像這樣:
引用:
find /home/hcchien/svn/ *.txt -print | sort

如果你用Find::File::Rule來寫,可能會像這樣:

#!/usr/bin/perl -w

use File::Find::Rule;

my $rule =  File::Find::Rule->new;
my @files = $rule->file->name( '*.txt' )->in('/home/hcchien/');

print "$_$/" for @files;
Perl的寫法不但比較長,比較複雜,而且速度比起你在shell底下真是慢了好幾倍。暫時看起來,Perl在這樣簡單的狀況下實在不特別好用。可是別忘了,像這樣簡單的狀況,每個人都習慣隨手就用shell解決,可是如果情況稍微複雜一點呢?比如我想找出檔案狀態是可執行的.txt檔案,那麼你應該怎麼做呢?接下來,我想把這些檔案的可執行模式取消,然後也許再修改某些內容......。如果只是單一內容,shell確實非常輕巧,快速,可是一但我們要把一堆操作集合在一起時,你就會發現perl有甚麼過人之處了。
引用:
#!/usr/bin/perl -w

use File::Find::Rule;

my $rule =  File::Find::Rule->new;
$rule->file;
$rule->executable;
my @files = $rule->name( '*.txt' )->in('/home/hcchien/');

for my $file (@files) {
    open READ, $file;
    s/foo/bar/g while (<READ>);
}
當然,你還可以對這些檔案做其他的操作,例如每個檔案都插入一列新的資料等等。這時候,有另外一個模組就顯得非常有用了,這麼模組就是IO::All。還記得我們之前怎麼讀入一個檔案的內容吧?IO::All現在可以讓你非常簡單的控制檔案,我們來比較看看:
引用:
用傳統的作法,我們可以這麼寫:

open FILE, "<foo.txt";
$buf.=$_ while (<FILE>);

倒也相當簡潔,不過現在使用IO::All,就只要這麼做:

my $buf < io('foo.txt');
不過我們並不打算在這裡深切的介紹IO::All這個模組,因為我們還有其他更重要的事情要做。

17.4 郵件管理

接下來讓我們來看看作為伺服器的一個重要工作,也就是關於Mail的管理。也因為對於郵件的管理需求其實非常的大,所以其實相關的管理工具也不在少數。例如可以過濾廣告信件,發送大量信件或是寄發一般的通知信件等等。不過由於某些工作的特殊性質,使得Perl成為這些工作中非常能夠勝任重要工具,這些工作中當然有不少是字串內容的分析,而最具代表性的也就是廣告信的過濾了。

17.4.1 Mail::Audit + Mail::SpamAssassin

這是一個非常具有殺手級實力的一個Perl模組,如果你已經是一個現任的網路管理人員,整天聽到你所管理的郵件伺服器中不斷傳來廣告郵件,而且你還不知道這個模組,那麼應該先去CPAN上搜尋Mail::SpamAssassin這個模組。然後你還可以搭配Mail::Audit這個模組。這裡的例子是許多人使用Mail::Audit跟Mail::SpamAssassin時經常會作為過濾信件第一關的檢查工具:
引用:
use Mail::Audit;
use Mail::SpamAssassin;
my $m = Mail::Audit->new(
    emergency   => "~/emergency_mbox",
    nomime      => 1,
);
my $sa = Mail::SpamAssassin->new;
$m->pipe("listgate cle") if $m->from =~ /svk-devel/;    # 送到pipe
$m->accept("~/perl")     if $m->from =~ /perl/;         # 接進特定信箱
$m->reject("no rbl")     if $m->rblcheck;               # 拒絕黑名單
$m->ignore               if $m->subject =~ /sex/i;      # 忽略信件
my $status = $sa->check($m);                            # 檢查垃圾信
if ($status->is_spam) {
    $status->rewrite_mail;                              # 加上檔頭
    $m->accept("~/Mail/spam");                          # 收進垃圾桶
}
$m->noexit(1); $m->accept("~/Mail/%Y%m"); $m->noexit(0);# 按月彙整
$m->accept;                                             # 其餘接收
其實如果利用Mail::Audit,還可以直接套用Mail::Audit::MAPS。因為這樣就可以直接把一些已經被列為黑名單的寄件者先排除在外了,而且使用上也沒有甚麼太大的差別。其實你只需要多做一個判斷:
引用:
if ($mail->rblcheck) {
    ......
}
另外,Mail::Audit還有一個常用的外掛程式則是Mail::Audit::KillDups。他可以幫你刪除一些ID重複的信件,其實這也是可能的廣告信來源之一,所以你可以又事先過濾掉一些垃圾郵件。其實不只這些,Mail::Audit的外掛程式種類之多,實在讓人覺得有趣,我自己另一個常用的是Mail::Audi:GP,不過這就未必適合所有人了。
至於Mail::SpamAssassin,除了搭配Mail::Audit之外,其實也有命令列程式。透過命令列程式,你可以設定自己的黑名單(blacklist)跟白名單(whitelist),也可以透過手動訓練的方式,來增進SpamAssassin的準確率。

17.4.2 Mail::Sendmail 與 Mail::Bulkmail

另外,perl也可以用非常方便的方式來傳送mail。雖然你在系統管理時,如果需要寄發mail,可以方便的使用sendmail來傳送。可是其實有些時候你會在系統中定時執行一些程式,或許是進行系統的意外檢查,或許是做定時的工作。那麼你可能會希望這些工作如果發生意外,你可以很快的收到電子郵件通知。這時候Mail::Sendmail就變得很有用了。
引用:
use Mail::Sendmail;

%mail = ( To      => 'yourmail@hostname.com',
          From    => 'mail@server.com',
          Message => "救命啊!Apache不動了!!"
        );

sendmail(%mail) or die $Mail::Sendmail::error;
各位大概都聽過「自相矛盾」的成語故事吧?而且千萬別以為那是書上拿來騙小孩的故事,因為真實的就發生在perl的模組中。我們之前介紹了目前幾乎被公認最好阻擋廣告信的模組:SpamAssassin,可是現在我們也要介紹另一個被認為發廣告信的強力模組,也就是Mail::Bulkmail。
引用:
use Mail::Bulkmail;
my $bulk = Mail::Bulkmail->new(
    LIST          => "~/listfile",      # 地址清單
    From          => 'admin@bulkmail.com',  # 寄件人
    Subject       => "System Information",  # 標題
    Message       => '~/announcement.txt'   # 內文檔名
    message_from_file => 1                  # 從檔案讀取內文
);
$bulk->bulkmail() or die Mail::Bulkmail->error;
在這裡,你只需要把要一次傳送的一大堆郵件地址逐行放進一個純文字檔。Mail::Bulkmail就會幫你讀出這些地址,而且發送郵件。至於內文,你雖然可以直接寫在程式中,可是更有彈性的方式也是可以利用文字檔來讀入郵件內容。這樣一來,如果你的郵件是每週定時發送,那麼你只需要修改傳送名單跟郵件內容的檔案就可以輕鬆的讓程式替你完成其他工作了。

17.4.3 POP3Client 及 IMAPClient

有些時候,你可能有一些帳號是專門用來處理一些特定的工作,那麼其實這些送到該帳號的信件未必需要由一個特定的人去收,而可以利用程式去進行處理。這個時候,你可以使用Mail:OP3Client這個模組來讀取這些郵件。我們先用一個簡單的例子來看看這個模組的用法:
引用:
use Mail:OP3Client;
my $pop = Mail::POP3Client->new(
    USER     => "me",
    PASSWORD => "mypassword",
    HOST     => "pop3.example.com",
);
foreach ( 1 .. $pop->Count-1 ) {
    $pop->Head( $_ ) =~ /^From:\s+somebody\@example.com/ or next;
    open my $fh, '>', "mail-$_.txt" or die $!;
    $pop->RetrieveToFile($fh, $_);
}
這個程式可以讓某個人寄來的信都被備份到一個特定的檔案中,其實主要的就是透過郵件檔頭中的寄件者進行比對。所以如果你可以針對主題進行比對,就可以對郵件進行分類。當然,如果你想出其他更好用的郵件過濾演算法,也許可以從這裡開始進行實做(雖然我們並不建議你這麼做)。其實一但可以直接取出郵件中的每一封郵件,我們可以做的應用就非常的廣泛了。而有了POP3Client,好像也少不了IMAPClient。我們還是先來看另一個例子吧:
引用:
use Mail::IMAPClient;

my $imap = Mail::IMAPClient->new;      
$imap = Mail::IMAPClient->new(  
                  Server => $host,
                        User    => $id,
                        Password=> $pass,
        )       or die "無法連上主機:$host as $id: $@";
使用IMAPClient,也因為郵件伺服器種類的特性差異,讓它提供更多的操作方式給使用者。最基本的比如IMAPClient的Connected跟Unconnected,另外可以透過Range來選定某一個特定範圍的郵件。例如:
引用:
$imap->Range($imap->messages);
其中$imap->messages會取得目前所在資料夾的所有郵件,然後透過$imap->Range()來把這些郵件設定成要操作的郵件範圍。不過你也可以在$imap->Range()中使用逗點分隔來指定某幾封特定的信件。另外,如果你考慮搬移信件,你可以使用move來進行,其實非常方便,就像這樣:
引用:
my $newUid = $imap->move($newFolder, $oldUid)
          or die "Could not move: $@\n";
$imap->expunge;
另外,還有一些常用的方法例如delete_messages和restore_messages就分別是用來刪除郵件跟回覆被刪除的郵件。或是你可以用search來搜尋郵件。其實IMAPClient非常的強大,確實需要根據自己的需求去研究相關的文件才能確實掌握。

17.5 日誌檔

作為一個系統管理,能確實掌握每日的紀錄檔確實是非常重要的工作。不過如果你進入/etc/log資料夾,就會發現裡面的檔案其實是相當多的。也就是說,如果你身為一個系統管理員,每天要注意這些檔案中有沒有異常,如果你還在使用傳統的工人智慧,那麼每天進行這樣的工作就要浪費許多時間。因此要希望能夠在這部份節省更多的時間來從事其他的管理工作,我們可以使用一些工作來簡化這些日常的程序,Parse::Syslog就是其中之一。
我們先來看看一個簡單的例子,來瞭解Parse::Syslog怎麼幫我們處理這些日誌檔案。
引用:
use Parse::Syslog;
my $syslog = Parse::Syslog->new('/var/log/syslog');

while (my $entry = $syslog->next) {
    $entry->{program} =~ /sudo$/ or next;
    print localtime($entry->{timestamp})."\n",
        "$entry->{text}\n\n";
}
其實程式並不困難,首先我們設定要處理的檔案,在這裡我們針對"/var/log/syslog"這個檔案來檢查。接著Parse::Syslog傳回一個物件,也就是$syslog。接下來我們使用next這個物件操作方法逐行處理這個檔案,在檔案結束之前,$syslog->next都會傳回真值,因此讓我們可以一行一行來進行比對的工作。我們試圖找出日誌檔中關於有使用者使用sudo這個指令,所以我們使用了正規表示式。
接下來,Parse::Syslog可以幫你取得事件發生的時間,所以當我們比對成功之後,就可以印出使用者使用sudo這個指令的時間。
所以如果我們一次把所有要監視的檔案內容利用Parse::Syslog設定好,那麼其實以後的日子就會輕鬆愉快許多了。當然,如果你擷取出這魔一堆檔案,那麼要把這些讓人需要特別注意的紀錄進行處理,才能方便管理者閱讀或檢查,這時候可以使用另一個模組來進行。
Log:ispatch是不錯的選擇。你只要新建立一個相關的物件,並且指定要寫入的檔名,那麼可以依照紀錄的等級來將相關的訊息寫入檔案中。
引用:
use Log:ispatch;
my $log = Log:ispatch->new;               # 建立紀錄物件
# 新增紀錄檔
$log->add( Log::Dispatch::File->new(
    name => 'file',                         # 物件名稱
    min_level => 'debug',                   # 紀錄門檻
    filename => '/var/log/test.log',        # 紀錄檔名
) );

接下來,我們把想要寫入的訊息像這樣的加入檔案中:

$log->log( level => 'alert', message => 'Strange data in incoming request' );
這裡有一個有趣的部份,也就是紀錄門檻。他可以讓使用者依照不同的等級來分門別類,而門檻的類別分別為:
引用:
除錯 (debug)
消息 (info)
提示 (notice)
警告 (warning)
錯誤 (error)
關鍵 (critical)
警鈴 (alert)
緊急事件 (emergency)
既然有了這些分別,你就可以進行更多的動作來確保系統正常的運作。例如你可以在系統遇到異常現象時發出信件給自己,就像這樣:
引用:
$log->add( Log::Dispatch::Email::MailSend->new(
    name => 'email',                        # 物件名稱
    min_level => 'emergency',               # 紀錄門檻
    to => [ 'admin@example.com' ],          # 收件地址
    subject => 'HELP!!!',                   # 郵件標題
) );
於是如果你的日誌得到了一些緊急事件的訊息時,就會自動發出電子郵件給你。如此一來你可以在第一時間就知道系統異常,並且進行檢修。當然,如果你是超級負責的系統管理員,你也許希望在你沒有網路的時候也能得到相關的緊急警告,這時候手機簡訊也許是另一種通知的好方法。
引用:
use Net::SMS;
my $df = `df -h`;                  # 取得硬碟配置資訊
$df =~ /-\d/ or exit;             # 若沒有數字是負的就直接離開
my $sms = Net::SMS->new;                # 簡訊物件
$sms->accountId("123-456-789-12345");   # 帳號
$sms->accountPassword("mypassword");    # 密碼
$sms->sourceAddr("0928-000-000");       # 來源
$sms->destAddr("0928-999-999");         # 目的
$sms->msgData("HARD DISK FULL:\n$df");  # 訊息
$sms->submit;                           # 送出簡訊
$sms->success or die $sms->errorDesc;   # 偵測錯誤
我們使用Net::SMS可以直接發送簡訊,在這個例子中,我們使用shell指令去檢查磁碟空間。並且當發現磁碟空間不足時就發出手機簡訊來警告系統管理人員。雖然你寫了這些程式之後,未必就可以獲得老闆的賞識而加薪,不過我想讓老闆少點機會找你麻煩應該還算是一項大利多吧!

17.6 報表

身為系統管理人員,尤其當你是在企業內部進行系統管理時,最容易遭到忽略。因為當大家系統非常順暢時,幾乎沒人會歸功於系統管理員的認真。於是要怎麼拿出實際的內容來說服其他的人,這也算是系統管理人員的重大業務之一了吧!很多人使用MRTG(註一)來監測網路流量,使用awstats(註二)來看網站的各式數據。
可惜的是大多數的人對這些報表並不太有興趣,除非你管理的是一個(或一堆)網站,而且公司的業務也就是經營這些網站。既然如此,那麼一個系統管理員有些時候其實還是要呈現出自己優良的管理績效,在沒有現成的套裝工具下,要怎麼樣快速的建立出漂亮的報表其實也是不小的學問。
如果你需要的是文字的報表,那麼我們前一章使用的Template就非常適合,你可以利用Template::Toolkit畫一個HTML的報表。
引用:
#!/usr/bin/perl -w

use strict;
use Template;
use IO::All;
use Mail::MboxParser;

my $dir = io('/var/mail');                    # 準備逐個檢查信箱
my %all;
while (my $io = $dir->next) {                    # 一個一個看信箱的信件數目
    if ($io->is_file) {                                # 只檢查檔案
        eval {                                    # 避免因為某些原因中斷
            my $mb = Mail::MboxParser->new("$io", newline => '#DELIMITER');
            %all{$io} = $mb->nmsgs;            # 把結果放入雜湊
        }
    }
}

my $config = {
             INCLUDE_PATH => '/search/path',
             POST_CHOMP   => 1,
         };

my $template = Template->new($config);          # 建立新的模板物件
my $vars = { messages  => \%all };
my $input = 'report.html';
my $output;

$template->process($input, $vars, $output)  # 處理模板內容
      || die $template->error();
print $output;
這時候,你可以取得一個含有所有信箱郵件個數的一個雜湊變數(我們使用了Mail::MboxParser)。如此一來,你只要弄一個漂亮的模板,就可以讓系統動態把資料填入,隨時可以監控目前大家信箱內的郵件數目了。不過這樣其實還沒結束,因為很多老闆或主管對於文字的接受度總是比較低,所以如果你有漂亮的報表,那麼給他們的印象應該也會隨之提高。這時候,你也許可以認真考慮使用GD::Graph這個模組。這個模組可以讓你輕鬆的畫出漂亮的統計圖表,你可以根據資料的屬性以及需求的差異,畫出例如圓餅圖,曲線圖,柱狀圖等等,如果你還想更絢麗,GD::Graph還可以畫出立體的3D圖形。
我們用另一個例子來看看怎麼使用GD::Graph吧:
引用:
use GD::Graph::bars3d;
my $graph = GD::Graph::bars3d->new(800, 600);       # 新增柱狀圖
my @files = </var/log/maillog.*.bz2>;

my $image = $graph->plot([                      # 訂出橫座標,縱座標內容
    [map /(\d+)\./g, @files],
    [map -s, @files],
]) or die $graph->error;

open my $fh, '>', '3.png' or die $!;
print $fh $image->png;      # 儲存影像
我們取得了每次的電子郵件日誌檔的,然後根據這些檔案的大小進行統計。這時候,我們只需要訂出橫座標跟縱座標的內容,交給GD去畫就好了,你當然也可以定時的要求程式幫你畫出某些統計圖表。很有用吧!你可以定時交出漂亮的工作報表,而且還是由電腦自動產生。

利用Perl來協助系統管理其實還算是非常方便的,何況已經有許多的系統管理員早就在做這些工作,也因此我們有很多方便的工具可以使用。這完全讓我們省下許多時間,尤其當更多的系統管理員每天都花許多時間在這些繁瑣而且又沒有變化的工作上。

習題:
1. 找出maillog中被reject(退信)的資料,也就是找到日誌檔中以reject標明的內容。例如:
引用:
    Jun  3 00:00:46 dns2 postfix/smtpd[71431]: D988D6A: reject: RCPT
    from smtp2.wanadoo.fr[193.252.22.29]: 450 <
    fnatterdobkl@hcchien.org>: User unknown in local recipient
    table; from=<> to=<fnatterdobkl@hcchien.org>
    proto=ESMTP helo=<mwinf0203.wanadoo.fr>
2. 承上題,統計當月每天的退信數字,並且畫成長條圖。

註一:Multi Router Traffic Grapher (訪客無法瀏覽此圖片或連結,請先 註冊登入會員 )
註二:訪客無法瀏覽此圖片或連結,請先 註冊登入會員

TOP

附錄A. 習題解答

第一章:
1. 試著找出你電腦上的Perl版本為何。

解答:當然,你得先確定你的電腦上確實裝了Perl。如果你在Unix/Linux/*BSD或是Mac OS X上,打開你的終端機(terminal),進入shell,接著打入perl -v,其中第一行中就可以看到你電腦上的Perl版本了。詳細的內容可以參閱第一章內容。

2. 利用perldoc perl找出所有的perl文件內容

解答:當你能看到perl -v的內容之後,你的電腦應該已經安裝Perl。接下來,你可以在shell中打入perldoc perl。於是你可以看到所有的文件,像這樣:



       Overview

           perl                Perl overview (this section)
           perlintro           Perl introduction for beginners
           perltoc             Perl documentation table of contents

     ...........

           perluts             Perl notes for UTS
           perlvmesa           Perl notes for VM/ESA
           perlvms             Perl notes for VMS
           perlvos             Perl notes for Stratus VOS
           perlwin32           Perl notes for Windows


至於如果你想看其中的任何一份文件,只要使用perldoc這個指令即可,例如可以使用perldoc perlsyn來看關於Perl語法的相關文件。

3. 利用Perl寫出第一個程式,印出你的名字

解答:你只需要使用print就可以解決這個問題,所以像是這樣:



print "簡信昌";
當然,你還可以用單引號,至少在這裡的用法是一樣的:
print '簡信昌';


第二章:
1. 使用換行字元,將你的名字以每個字一行的方式印出。

解答:最簡單的方式,你可以這麼寫


print "簡\n信\n昌\n";
另外,你當然可以逐行印出:
print "簡\n";
print "信\n";
print "昌\n";


2. 印出'\n', \t'字串。

解答:你可以單純的使用單引號


print '\n \t";
或是使用雙引號,然後加上跳脫字元:
print "\\n \\t";


3. 讓使用者輸入姓名,然後印出包含使用者姓名的招呼語(例如:hello xxx)。

解答:這裡主要是要能夠讓使用者輸入,所以我們應該使用<STDIN>


#!/usr/bin/perl

use strict;

chomp(my $input = <STDIN>);
print "Hello $input \n";


第三章:
1. 試著把串列 (24, 33, 65, 42, 58, 24, 87) 放入陣列中,並讓使用者輸入索引值 (0...6),然後印出陣列中相對應的值。

解答:在這裡,我們並不先對輸入值做判斷,也就是假設使用者都會乖乖的輸入0...6的數字。


#!/usr/bin/perl

use strict;

my @array = (24, 33, 65, 42, 58, 24, 87);
chomp(my $input = <STDIN>);     # 使用者輸入
print $array[$input];


2. 把剛剛的陣列進行排序,並且印出排序後的結果。

解答:這部份其實只需要使用一個排序的函式sort。


print sort @array;


3. 取出陣列中大於40的所有值。

解答:至於這一個部份,我們則是可以使用gerp這個函式直接完成:


print grep {$_ > 40} @array;
你當然還可以把過濾出來的值再進行排序,就像這樣:
print sort grep {$_>40} @array;


4. 將所有陣列中的值除以 10 後印出。

解答:至於要把一個陣列中的所有值同時進行某種轉換,對應,就可以使用map


print map {$_/10} @array;
同樣的,你還是可以試著將結果排序


第四章:
1. 算出1+3+5+...+99的值。

解答:我們可以使用for迴圈或是while迴圈來進行。


$!/usr/bin/perl

use strict;
my $sum = 0;
for (my $i = 0; $i < 100; $i+2) {
    $sum+=$_;
}

print $sum;

如果使用while,那麼程式碼應該像是這樣

#!/usr/bin/perl

use strict;

my ($sum,$i);
while ($i < 100) {
    $sum+=$i;
    $i++;
}
print $sum;


2. 如果我們從1加到n,那麼在累加結果不超過100,n的最大值應該是多少?

解答:這時候,我們用while迴圈似乎就比較方便了


#!/usr/bin/perl

use strict;

my ($sum, $i);
while ($sum <= 100) {
    $sum+=$i;
    $i++;
}

print $i;


3. 讓使用者輸入一個數字,如果輸入的數字小於50,則算出他的階乘,否則就印出數字太大的警告。

解答:這裡有兩個重點,一個是if判斷式,另一個則是計算階乘的迴圈。


#!/usr/bin/perl

use strict;

chomp(my $input = <STDIN>);
if ($input < 50) {
    my $total = 1;     # 這跟算總和不同
    for (my $i = 1; $i <= $input; $i++) {
      $total*=$i;    # 進行階乘
    }
    print $total;
} else {
    print "數字太大了";
}


第五章:
1. 將下列資料建立一個雜湊:
John => 1982.1.5
Paul => 1978.11.3
Lee => 1976.3.2
Mary => 1980.6.23

解答:我們可以很簡單的使用串列,或是=>來建立雜湊:


my %hash = (   John => "1982.1.5",
            Paul => "1978.11.3",
            Lee  => "1976.3.2",
            Mary => "1980.6.23" );
至於如果使用串列,則非常單純的只要:
my %hash = qw/John 1982.1.5 Paul 1978.11.3 Lee 1976.3.2 Mary 1980.6.23/;


2. 印出1980年以後出生的人跟他們的生日。

解答:我們逐個取出雜湊的鍵值,然後比較資料。


my %hash = qw/John 1982.1.5 Paul 1978.11.3 Lee 1976.3.2 Mary 1980.6.23/;
while ( ($key, $value) = each %hash) {
    print "$key, $value" if ($value gt "1980");
}


3. 新增兩筆資料到雜湊中:
Kayle => 1984.6.12
Ray => 1978.5.29

解答:要新增雜湊中的內容很簡單,只需要單純的指定鍵跟對應的值就可以了。


$hash{Kayle} = '1984.6.12';
$hash{Ray} = '1978.5.29';


4. 檢查在不修改程式碼的情況下,能否達成第二題的題目需求

解答:由於我們使用while迴圈,它會自動檢查雜湊中所有的內容,因此即使我們新增了兩筆資料,對於迴圈的運作並不會有所影響。

第六章:
1. 下面有一段程式,包含了一個陣列,以及一個副常式diff。其中diff這個副常式的功能在於算出陣列中最大與最小數值之間的差距。請試著將這個副常式補上。


#!/usr/bin/perl -w

use strict;

my @array = (23, 54, 12, 64, 23);
my $ret = diff(@array);
print "$ret\n";          # 印出 52 (64 - 12)
my @array2 = (42, 33, 71, 19, 52, 3);
my $ret2 = diff(@array2);
print "$ret2\n";  # 印出 68 (71 - 3)


解答:我們需要進行的工作包括讀取透過副常式傳來的陣列內容,並且取得陣列中的最大值與最小值,再進行兩個值的計算。


sub diff {
    my @param = @_;
    my ($max, $min) = ($param[0], $param[0]);
    for (@param) {
      $max = $_ if $_ > $max;                # 求最大值
  $min = $_ if $_ < $min;                # 求最小值
    }
    $max - $min;
}


2. 把第四章計算階乘的程式改寫為副常式型態,利用參數傳入所要求得的階乘數。

解答:我們先來看看第四章中關於階乘的這段程式碼。


my $total = 1;
for (my $i = 1; $i <= $input; $i++) {
    $total*=$i;
}
print $total;

接下來我們將它改為副常式型態:
sub times {
    my $input = shift;                          # 取得使用者傳入的參數
    my $total = 1;
    for (my $i = 1; $i <= $input; $i++) {  # 計算階乘
      $total*=$i;
    }
    return $total;
}


第七章:
1. 讓使用者輸入字串,並且比對是否有Perl字樣,然後印出比對結果。

解答:這裡只需要使用最單純的樣式比對來判斷比對的結果。


#!/usr/bin/perl

use strict;

chomp(my $input = <STDIN>);
if ($input =~ /Perl/) {
    print "比對成功\n";
} else {
    print "比對失敗\n";
}


2. 比對當使用者輸入的字串包含foo兩次以上時(foofoo 或是 foofoofoo 或是 foofoofoofoo...),印出比對成功字樣。

解答:使用群組比對的方式似乎可以簡單的達到這個要求,所以設定樣式為foo。請注意,你不能將樣式設定為foofoo,否則如果foo的出現次數是單次(例如三次)的話,那就無法正確比對了。所以我們的寫法可以像這樣:


#!/usr/bin/perl

use strict;

chomp(my $input = <STDIN>);
if ($input =~ /(foo){2,}/) {          # 必須出現兩次以上
    print "比對成功\n";
} else {
    print "比對失敗\n";
}


第八章:
1. 延續第七章的第一題,比對出perl在字串結尾的成功結果。

解答:和第七章的第一個問題不同的是在於我們必須使用定位點的概念。所以我們只寫出需要進行字串結尾的樣式。


$input =~ /perl$/;


2. 繼續比對使用者輸入的字串,並且確定是否有輸入數字。

解答:這個問題主要的部份在於可以使用字符集或是字符集的簡寫。最簡單的當然是直接使用\d的簡寫形式。


$input =~ /\d/;
而其實也就是可以使用
$input =~/[0-9]/;


3. 利用回溯參照,找出使用者輸入中,引號內(包括雙引號或單引號)的字串。

解答:在這裡,我們特別要求不管使用者使用單引號或雙引號時都可以可以找出引號中的字串。因此在比對時,就必須使用字符集,也就是['"]必須同時被納入。但是一但使用字符集來進行比對,為了避免產生錯誤的對稱,例如"單引號''",我們就必須使用回溯參照,以確定我們比對的是對稱的引號。另外,則是要注意使用記憶變數來取得我們比對出來的內容。所以比對的樣式應該可以這麼寫:


$input =~ /['"](.+?)\1/;
print $1;                  # 比對出來的內容

我們還要注意括號裡的內容,首先是一個萬用符號.,接下來是重複符號+,這是指至少出現一次的重複符號。接下來是為了避免比對超過第一次對稱的引號範圍,所以我們用了不貪多的修飾符號'?'。而這裡面正是我們所要取得的全部內容,所以就用了記憶變數的符號。

4. 找出使用者輸入的第一個由p開頭,l結尾的英文字。

解答:這裡我們要確定的有幾個部份,也就是我們要比對的是一個「字(word)」,因此p跟l分別是這個字的兩個端點,我們也就可以利用\b來畫出這個字的界線。當然,記憶變數還是需要的,因為我們不但要確定是否比對成功,因為我們還想取得比對成功的字串內容。所以我們就把比對樣式寫成這樣:


$input =~ /\b(p\w*l)\b/;
print $1;


第九章:
1. 陸續算出 (1...1) 的總和,(1...2) 的總和,...到 (1...10) 的總和。但是當得到總和大於50時就結束。

解答:這個題目主要有兩個部份,第一個是關於計算加總的部份,一般我們也許常用for迴圈來進行加總的部份,當然你也可以使用while迴圈或其他方式。接下來,你要考慮計算出來的總和,讓他不超過50。這個情況下,可以使用last來做迴圈的餓外控制。


#!/usr/bin/perl -w

use strict;

my ($base, $sum) = (0, 0);
for $base (1...10) {
    $sum = sum($base);
    last if ($sum > 50);
    print "$base => $sum\n";
}

sub sum {
    my $index = shift;
    my $summary;
    for (1...$index) {
        $summary += $_;
    }
    return $summary;
}


2. 把下面的程式轉為三元運算符形式:


#!/usr/bin/perl -w

use strict;

chomp(my $input = <STDIN>);
if ($input < 60) {
    print "不及格";
} else {
    print "及格";
}


解答:這部份其實只要考慮if敘述句內的部份。所以我們先找出關鍵的部份,也就是if條件跟else的內容。接下來就只需要一一對照轉換就可以了。


原來寫法:     if ($input < 60) { print "及格" } else { print "不及格" }
三元算符寫法:  ($input < 60) ? print "及格" : print "不及格";


第十章:
1. 試著將下面的資料利用perl寫入檔案中:


Paul, 26933211
Mary, 21334566
John, 23456789


解答:這裡主要的重點就是開啟檔案,寫入內容,至於資料,我們可以使用雜湊來處裡。


#!/usr/bin/perl -w

use strict;

my %tel = ("aul", 26933211, "Mary", 21334566, "John", 23456789);
open FILE, ">telephone";
for (keys %tel) {
    print FILE "$_ => $tel{$_}\n";
}
close FILE;

然後你可以去看檔案"telephone"的內容,也就是:

John => 23456789
Mary => 21334566
Paul => 26933211


2. 在檔案中新增下列資料:


Peter, 27216543
Ruby, 27820022


解答:剛剛我們在開啟檔案時使用了">"來表示寫入一個新檔。接下來,我們只是要在現有的檔案中加入新的內容,因此我們應該改用">>"的方式,以避免原來的檔案被清空。


#!/usr/bin/perl -w

use strict;

my %tel = ("eter", 27216543, "Ruby", 27820022);
open FILE, ">>telephone";
for (keys %tel) {
    print FILE "$_ => $tel{$_}\n";
}
close FILE;

這樣我們就可以很容易的看出其中的不同了。

3. 從剛剛已經存入資料的檔案讀出檔案內容,並且印出結果。

解答:不同於剛剛寫入檔案,我們現在需要的是把檔案內容讀出。


#!/usr/bin/perl -w

use strict;

open FILE, "telephone";
while (<FILE>) {
    print $_;
}
close FILE;


第十一章:
1. 列出目前所在位置的所有檔案/資料夾名稱。

解答:我們可以用簡單的角括號方式來取得目前目錄下的所有內容。


#!/usr/bin/perl -w

use strict;

my @files = <*>;
print "$_\n" for @files;


2. 承一,只列出資料夾名稱。

解答:在這裡,我們只需要修改剛剛的程式,在列印前判斷我們取得的是檔案或資料夾。


my @files = <*>;
for (@files) {
    print "$_\n" if (-d $_);
}


3. 利用perl,把目錄下所有附檔名為.pl的檔案修改權限為可執行。

解答:首先我們還是使用角括號,但是我們這次要取出的只有所有附檔名為.pl的檔案。接下來,再以chmod來修改權限。


my @files = <*.pl>;
chmod 0755, @files;


第十二章:
1. 讓使用者輸入字串,取得字串後算出該字串的長度,然後印出。

解答:這裡主要還是要使用length這個函式,來取得字串長度。


#!/usr/bin/perl -w

use strict;

chomp(my $str = <STDIN>);
print length($str);


2. 利用sprintf做出貨幣輸出的表示法,例如:136700以$136,700,26400以$26,400表示。
3. 利用雜湊%hash = (john, 24, mary, 28, david, 22, paul, 28)進行排序,先依照雜湊的值排序,如果兩個元素的值相等,則依照鍵值進行字串排序。

第十三章:
1. 試著在你的Unix-like上的機器裝起CPANPLUS這個模組。

解答:你可以直接透過CPAN來安裝CPANPLUS,或是到訪客無法瀏覽此圖片或連結,請先 註冊登入會員 下載CPANPLUS的原始碼,解開之後直接安裝。成功安裝之後,你可以在shell底下使用CPANPLUS,就像這樣:


[hcchien@Apple]% cpanp
CPANPLUS::Shell:efault -- CPAN exploration and modules installation (v0.03)
*** Please report bugs to <cpanplus-bugs@lists.sourceforge.net>.
*** Using CPANPLUS::Backend v0.049.  
*** ReadLine support available (try 'i Term::ReadLine:erl').

CPAN Terminal>


2. 還記得我們寫過階乘的副常式嗎?試著把它放入套件My.pm中,並且寫出一個程式呼叫,然後使用這個副常式。

解答:其實如果知道Package的包裝方式跟使用方式,這個問題可以很容易的解決。


sub times {
    my $input = shift;                          # 取得使用者傳入的參數
    my $total = 1;
    for (my $i = 1; $i <= $input; $i++) {  # 計算階乘
      $total*=$i;
    }
    return $total;
}


第十四章:
1. 下面程式中,%hash是一個雜湊變數,$hash_ref則是這個雜湊變數的參照。試著利用$hash_ref找出參照的所有鍵值。


%hash = ( name => 'John',
          age => 24,
    cellphone => '0911111111' );
$hash_ref = \%hash;


解答:其實你只要解開雜湊參照,就可以簡單的使用keys函式來取得參照的所有鍵。


#!/usr/bin/perl -w

use strict;

my %hash = ( name => 'John',
             age => 24,
             cellphone => '0911111111' );
my $hash_ref = \%hash;

my @keys = keys %{$hash_ref};
print $_ for @keys;


2. 以下有一個雜湊,試著將第一題中的雜湊跟這個雜湊(@hash_array)放入同一陣列中。


%hash1 = ( name => 'Paul',
            age => 21,
     cellphone => '0922222222',
     birthday => '1982/3/21' );


解答:由於陣列中的元素都是純量,所以我們需要的是把兩個雜湊的參照放進陣列@hash_array中。


my @hash_array = ( { name => 'John',
                     age => 24,
                    cellphone => '0911111111' },
                   { name => 'Paul',
                            age => 21,
                  cellphone => '0922222222',
                  birthday => '1982/3/21' } );


3. 承上一題,印出陣列$hash_array中每個雜湊鍵為'birthday'的值,如果雜湊鍵不存在,就印出「不存在」來提醒使用者。

解答:在這裡,我們應該先從陣列中依序取出雜湊的參照,然後解開參照,判斷參照鍵'birthday'是否存在。如果存在就可以取出其中的雜湊值。


#!/usr/bin/perl -w

use strict;

my @array = ( { name => 'John',
                age => 24,
               cellphone => '0911111111' },
        { name => 'Paul',
                       age => 21,
             cellphone => '0922222222',
             birthday => '1982/3/21' } );

for (@array) {
    if (exists ${$_}{birthday}) {  # 解開參照,並且判斷雜湊鍵是否存在
  print ${$_}{birthday};
    } else {
  print "the key doesn't exist";
    }
}

其實你可以用更簡潔的方式來解開參照,也就是$_->{birthday}


第十五章:
1. 利用自己熟悉的資料庫系統(例如 MySQL 或 Postgres),建立一個資料庫,並且利用DBI連上資料庫,取得Database Handler。

解答:假設我們在MySQL建了一個資料庫叫做'perlbook'所以我們要連接上資料庫,就只要使用DBI。


my $dbh = DBI->connect('dbi:mysql:dababase=perlbook', 'user', 'password');


2. 試著建立以下的一個資料表格,並且利用Perl輸入資料如下:


資料表格:
name: varchar(24)
cellphone: varchar(12)
company: vrchar(24)
title: varchar(12)
資料內容
[ name: 王小明
  cellphone: 0911111111
  company: 甲上資訊
  title: 專案經理 ]
[ name: 李小華
  cellphone: 0922222222
  company: 乙下軟體
  title: 業務經理 ]


解答:建立資料表格,我們可以透過各種方式,例如MySQL的用戶端程式,或現成的管理程式。當然也可以利用DBI的方式來建立新的資料表格。


#!/usr/bin/perl -w

use strict;
use DBI;

my $dbh = DBI->connect('dbi:mysql:database=perlbook', 'user', 'password');

my $create = <<"END";
CREATE TABLE address (
    name varchar(24),
    cellphone varchar(12),
    company vrchar(24),
    title varchar(12)
);
END

$dbh->do($create) or die "can't create";    # 先把資料表格建起來

my $sql;
$sql = "INSERT INTO address VALUES ('王小明', '0911111111', '甲上資訊', '專案經理')";
$dbh->do($sql);
$sql = "INSERT INTO address VALUES ('李小華', '0922222222', '乙下軟體', '業務經理')";
$dbh->do($sql);


3. 從資料庫中取出所有資料,並且利用fetchrow_array的方式逐筆印出資料。

解答:和新增資料不同,一般我們要從資料庫抓資料出來,都會先使用prepare,然後execute之後才取得資料內容。所以寫法和剛剛會有不少的差別。


#!/usr/bin/perl -w

use strict;
use DBI;

my $dbh = DBI->connect('dbi:mysql:database=perlbook', 'user', 'password');

my $sql = "select * from address";
my $sth = $dbh->prepare($sql);
$sth->execute;                                  # 先取得所有的內容
while (my @result = $sth->fetchrow_array) {  # 逐筆取出
    print "姓名:$result[0]\n";
    print "電話:$result[1]\n";
    print "公司:$result[2]\n";
    print "職稱:$result[3]\n";
}

$dbh->disconnect;


4. 呈上題,改利用fetchrow_hashref進行同樣的工作。

解答:在這裡,我們只需要修改while迴圈內的程式碼。將原來使用fetchrow_array的部份改成使用fetchrow_hashref就可以了。當然,因為fetchrow_hashref拿到的是一個雜湊參照,所以我們得先解開參照,然後取得其中的值。



while (my $result = $sth->fetchrow_hashref) {  # 逐筆取出
    print "姓名:$result->{name}\n";
    print "電話:$result->{cellphone}\n";
    print "公司:$result->{company}\n";   
    print "職稱:$result->{title}\n";
}


第十六章:
1. 以下是一個HTML頁面的原始碼,試著寫出action中指定的print.pl,並且印出所有欄位中,使用者填入的值。


<HTML>
    <HEAD>
        <TITLE>習題</TITLE>
    </HEAD>
    <BODY>
        <FORM ACTION="print.pl" METHOD="OST">
      姓名:<INPUT TYPE="text" NAME="name"><BR/>
      地址:<INPUT TYPE="text" NAME="address"><BR/>
      電話:<INPUT TYPE="text" NAME="tel"><BR/>
      <INPUT TYPE="submit">
  </FORM>
    </BODY>
</HTML>


解答:基本上,這個題目我們想要的就是取得使用者輸入的內容,所以利用CGI模組就可以簡單的做到這件事。


#!/usr/bin/perl -w

use strict;
use CGI;

my $q = CGI->new;
print "姓名:".$q->param('name')."\n";
print "地址:".$q->param('adress')."\n";
print "電話:".$q->param('tel')."\n";


2. 承上題,試著修改剛剛的print.pl,並且利用Template模組搭配以下的模板來進行輸出。


<TABLE>
    <TR><TD>姓名:</TD><TD>[% name %]</TD></TR>
    <TR><TD>地址:</TD><TD>[% address %]</TD></TR>
    <TR><TD>電話:</TD><TD>[% tel %]</TD></TR>
</TABLE>


解答:這裡的主要工作就是把Template的物件建起來,這樣一來,我們就可以使用Template::Toolkit來建立漂亮的模板。我們假設把上面的模板存成template.html。


#!/usr/bin/perl -w

use strict;
use Template;
use CGI;

my $q = CGI->new;
my $config = {
    INCLUDE_PATH => './',
    EVAL_PERL    => 1,
};

my $template = Template->new($config);
my $vars = {
    name  => $q->param('name'),
    address => $q->param('address'),
    tel => $q->param('tel')
};

my $temp_file = 'template.html';
my $output;
$template->process($temp_file, $vars, $output)
    || die $template->error();

print $output;


3. 承上題,將利用Template輸出的部份改為HTML::Mason。

解答:我們假定各位的HTML::Mason都設定完成,也就是其實目前都可以執行HTML::Mason的相關程式。因此我們接下來需要的只是處理這一頁的Mason程式。


<TABLE>
    <TR><TD>姓名:</TD><TD><% $name %></TD></TR>
    <TR><TD>地址:</TD><TD><% $address %></TD></TR>
    <TR><TD>電話:</TD><TD><% $tel %></TD></TR>
</TABLE>

<%args>
$name
$address
$tel
</%args>


第十七章:
1. 找出maillog中被reject(退信)的資料,也就是找到日誌檔中以reject標明的內容。例如:


Jun  3 00:00:46 dns2 postfix/smtpd[71431]: D988D6A: reject: RCPT from
smtp2.wanadoo.fr[193.252.22.29]: 450 <fnatterdobkl@hcchien.org>:
User unknown in local recipient table; from=<>
to=<fnatterdobkl@hcchien.org> proto=ESMTP helo=
<mwinf0203.wanadoo.fr>


解答:對於系統的日誌檔而言,其實最有利的大多還是格式的固定(規則)化。所以我們可以比較容易的處理這些日誌檔,進而用比較輕鬆的方式取得我們需要的資料。在這裡,我們發現郵件伺服器的日誌檔格式是以': '來作為區隔。所以如果我們把每一筆資料(一列)視為一個字串,利用split來將字串切開為包含各欄位的陣列的話,我們就發現陣列的第三個元素就可以用來判斷是否為退信的資料,因此這樣就顯得容易多了。讓我們來試試看:


#!/usr/bin/perl -w

use strict;

my $file = "/var/log/mail.log";
open LOG, $file;
while (<LOG>) {
    my @columns = split /: /, $_;
    print $_ if ($columns[2] eq 'reject');
}
close LOG;



2. 承上題,統計當月每天的退信數字,並且畫成長條圖。

TOP

 19 12
發新話題

本站所有圖文均屬網友發表,僅代表作者的觀點與本站無關,如有侵權請通知版主會盡快刪除。