« 9. 再談控制結構 | Perl 學習手札目录 | 11. 檔案系統 »

10. 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進行系統操作時,我們就可以檢查出來,而且印出來的結果會像是這樣:


TTY=ttyp0 ; PWD=/var/log ; USER=root ; COMMAND=/bin/rm -rf httpd-error.log TTY=ttyp0 ; PWD=/var/log ; USER=root ; COMMAND=/bin/rm -rf httpd-access.log TTY=ttyp0 ; PWD=/var/log ; USER=root ; COMMAND=/bin/rm -rf 192.168.1.1_access_log 192.168.1.1_error_log TTY=ttyp0 ; PWD=/usr/home ; USER=root ; COMMAND=/bin/rm -rf interchange/ TTY=ttyp0 ; PWD=/usr/home ; USER=root ; COMMAND=/bin/rm -rf gugod/ TTY=ttyp0 ; PWD=/usr/home ; USER=root ; COMMAND=/bin/rm -rf mysql/ TTY=ttyp0 ; PWD=/ ; USER=root ; COMMAND=/bin/rm kernel.old TTY=ttyp0 ; PWD=/ ; USER=root ; COMMAND=/bin/rm -rf modules.old/ TTY=ttyp0 ; PWD=/ ; USER=root ; COMMAND=/bin/rm -rf opt/

如果你是負責管理一些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就是這類型的工具。