文件句柄
文件句柄用來對應要操作的文件系統中的文件,這么說不太嚴謹,但比較容易理解。首先為要打開的文件綁定文件句柄(稱為打開文件句柄),然后在后續的操作中都通過文件句柄來操作對應的文件,最后關閉文件句柄。
如不理解文件句柄的概念,可將文件句柄看作Linux中文件描述符的概念(當然,它們是不同的,Perl的文件句柄在層次上對應於Linux中的標准IO流)。例如特殊的STDIN、STDOUT、STDERR就是perl中預定義好的文件句柄,分別表示標准輸入、標准輸出、標准錯誤,要將它們對應到Linux上的話,它們是默認的文件描述符fd=0、fd=1和fd=2的字符串描述形式,而這幾個文件描述符分別對應文件系統中的/dev/stdin、/dev/stdout和/dev/stderr設備文件。也就是說,Linux上的perl中的文件句柄STDIN、STDOUT和STDERR默認關聯的文件是/dev/stdin、/dev/stdout和/dev/stderr。
如果還不理解文件句柄,就把它想象成通道:perl程序中IO操作和磁盤上文件的通道。例如,print語句將數據通過某個通道輸出到對應的文件中。
文件句柄和文件描述符
實際上,文件句柄和文件描述符是有區別的。文件描述符是一個數值,代表操作系統所使用的裸數據流,文件描述符是文件句柄的核心,文件句柄可以看作是文件描述符的更高一層次的封裝,比如提供了和描述符有關數據流的輸入、輸出的buffer緩沖。也就是說,文件句柄比文件描述符多一些額外的功能。
我們可以為任意要操作的文件定義一個文件句柄。通常,使用大寫字母作為文件句柄的名稱。
例如,下面打開一個文件句柄LOG,這個文件句柄對應的文件是/tmp/a.log,操作模式是追加寫入(和shell中的追加重定向是相同的意思)。然后向LOG文件句柄中輸入一段數據,最后關閉文件句柄。
open LOG,">>/tmp/a.log";
print LOG "haha, hello world";
close LOG;
一般來說,打開了文件句柄后,在操作完成后要關閉文件句柄以便節省操作系統"打開文件數量限制"的資源。但perl有時候比較智能,會在某些時候自動幫我們關掉文件句柄。而且,當打開一個文件句柄后,再次去打開這個文件句柄時,perl會先關閉這個文件句柄再打開這個文件句柄,這稱為"文件句柄的reopen",它只是隱式地關閉並重新打開,perl並不認為中間涉及了關閉操作(例如reopen時行號不會重置)。
打開文件句柄
要打開文件句柄,使用open函數。open函數的功能其實很豐富,如有需要,可去官方手冊查看:http://perldoc.perl.org/functions/open.html
打開(open)文件是有目的的:為了讀取?為了寫入?為了追加寫入?這是操作模式。在open文件時,需要指明操作模式。此外,還要給定要關聯的文件路徑,路徑可以是絕對路徑,也可以是相對當前perl程序的相對路徑。
另外注意,文件句柄需唯一,不能出現重名。
例如:
open LOG1,">","/tmp/a.log"; # 以覆蓋寫入的方式打開文件/tmp/a.log
open LOG2,">>","/tmp/a.log"; # 以追加寫入的方式打開文件/tmp/a.log
open LOG3,"<","/tmp/a.log"; # 打開/tmp/a.log文件,以提供輸入源
open LOG4,"/tmp/a.log"; # 等價於上面的輸入,默認的模式就是輸入
另一種寫法是將模式符號和目標文件放在一起:
open LOG1,">/tmp/a.log";
open LOG2,">>/tmp/a.log";
open LOG3,"</tmp/a.log";
中間還可以有空格:
open LOG1,"> /tmp/a.log";
open LOG2,">> /tmp/a.log";
open LOG3,"< /tmp/a.log";
可以將目標文件賦值給一個變量,然后在open函數中使用變量名替換。
my $tmp_file = "/tmp/a.log";
open LOG1,">","$tmp_file";
如果要指明輸入、輸出的文件編碼,則使用上面將"模式和路徑分開"的方式。例如:
open LOG1,">:encoding(UTF-8)","/tmp/a.log"; # 以UTF-8編碼方式寫入數據
open LOG1,">>:encoding(UTF-8)","/tmp/a.log";
open LOG1,"<:encoding(UTF-8)","/tmp/a.log"; # 以UTF-8編碼方式讀入數據
需要注意,perl自身是無法打開外部文件的,它需要請求操作系統內核,讓操作系統來打開文件。所以,打開文件正確、錯誤時,操作系統都會有相應的回饋信息。對於perl來說,open函數的返回值就表示正確、錯誤打開文件。所以,通過以下方式可以判斷是否正確打開:
my $success = open LOG,">","/tmp/a.log";
if(!success){
exit 1;
}
更好、更常用的方式是使用die:
open LOG,">","/tmp/a.log"
or die "open file wrong: $!";
或者使用autodie功能,當捕獲到某些錯誤時,會自動調用die結束程序:
use autodie;
open LOG,">","/tmp/a.log";
無法打開文件的可能原因有很多,比如讀取時文件不存在,比如上級目錄不存在,比如無權限等等。操作系統會向perl報告這些錯誤,使用"$!"可以引用操作系統向perl報告的錯誤,例如die "can't open file: $!";
被觸發時的消息如下:
can't open file: No such file or directory at myperl.plx line 5.
上面的"No such file or directory at myperl.plx line 5."就是$!
收集和整理后的錯誤信息。
同理,關閉文件句柄錯誤也可以捕捉:
close DATA
or die "Couldn't close file properly";
使用文件句柄:讀取文件數據
例如,要從test.log文件中讀取所有數據行,一般的流程如下:
#!/usr/bin/perl
use 5.010;
open LOG,"<","test.log"
or die "open file wrong: $!"
while(<LOG>){
chomp;
say $_;
}
另外,從特殊文件句柄<STDIN>
、<>
、<ARGV>
中讀取數據時,由於它們是預定義好的,所以不需要先open。
使用文件句柄:寫入數據到文件
要向文件中寫入數據,可以使用輸出語句,如print/say和printf,要寫多行的時候,可以使用heredoc的方式。
在使用print/say/printf的時候,在這幾個關鍵字后面接上文件句柄即表示本輸出語句寫入到此文件句柄中。其實,當它們不指定文件句柄的時候,所采用的就是默認的文件句柄STDOUT。
例如,以追加模式寫入一行數據到test.log中。
#!/usr/bin/perl
use 5.010;
open LOG,">>","test.log"
or die "Can't open file: $!";
say LOG "NEW LINE!";
再例如,向標准輸出、標准錯誤中輸出信息:
say STDOUT "NEW LINE!";
say STDERR "NEW LINE!";
選一個默認的輸出文件句柄
注意是選擇默認的輸出文件句柄,不適用於輸入的文件句柄。
默認情況下的默認輸出文件句柄就是STDOUT,但是可以使用select關鍵字自己選一個默認的輸出文件句柄。只是需要注意的是,再將內容輸出到自選的默認輸出文件句柄結束后,應該重新選回STDOUT。
例如,讀取某個文件的內容,追加重定向輸出到另一個文件中:
#!/usr/bin/perl
open LOG,">>","test1.log" or die "Can't open file: $!";
select LOG;
while(<>){
print "Line $. from $ARGV: $_";
}
select STDOUT;
print "restored default filehandler: STDOUT\n";
然后執行該perl程序(程序名:15.plx),並傳遞a.log作為命令行參數。作為運行結果,會將a.log中的數據追加到test1.log文件中,並輸出一行內容到終端屏幕上。
$ perl 15.plx a.log
restored default filehandler: STDOUT
選擇默認的文件句柄后,上面while循環中的print,等價於print LOG ...
。
通常,選擇默認的文件句柄更常用於設置文件句柄是否要緩沖。例如,輸出到下面三個文件句柄(LOG/STDERR/STDOUT)的數據不會緩沖,而是直接輸出到文件句柄。
select LOG; $| = 1; # make unbuffered
select STDERR; $| = 1; # make unbuffered
select STDOUT; $| = 1; # make unbuffered
其中,控制輸出的緩沖變量為$|
,通常在使用管道、套接字的時候,可能不需要甚至不應該對數據進行緩沖,而是直接暴露給其它進程。
一般來說,在超過一個文件句柄需要關閉緩沖時,不會使用這種select XXX; $|=1
的方式,而是導入IO::Handle
模塊,然后使用它的autoflush(1)
函數來實現關閉IO緩沖。
use IO::Handle;
FH1->autoflush(1);
FH2->autoflush(2);
FH3->autoflush(3);
autoflush(1)
的功能等價於:
select( (select(NEWOUT), $| = 1 )[0] );
因為select()
的返回值是當前標准輸出的文件句柄,然后內層的select的結果和$| = 1
的結果構成一個匿名列表,選擇列表的第一個元素即之前標准輸出的文件句柄,將其作為外層select的參數,即表示恢復了之前的文件句柄。(如果目前看不懂,請忽略)
關於IO Buffer
分為兩種IO緩沖模式:block buffer、line buffer。
block buffer是表示先積累一定數據量(比如通常是幾K大小)之后再輸出。line buffer是表示只有讀取到了換行符的時候才輸出。$|=1
或者autoflush(1)都表示從block buffer(默認)切換成line buffer,而不是禁用(或關閉)buffer。
如果真要禁用buffer,可以使用syswrite(),它會直接繞過buffer,也就是禁用buffer。
文件句柄變量
除了使用大寫字母(一般情況下文件句柄都如此命名,稱為裸句柄)的文件句柄,還可以使用變量來命名文件句柄。
例如,使用變量代表的文件句柄:
my $rock_fh;
open $rock_fh,"<","/tmp/a.log"
or die "Can't open file: $!";
或者:
open my $rock_fh,"<","/tmp/a.log"
or die "Can't open file: $!";
使用變量文件句柄時:
while(<$rock_fh>){
chomp;
...
}
不過有時候,使用文件句柄變量會產生歧義。例如下面的語句:
print $rock_fh;
perl並不知道$rock_fh
是要輸出的列表數據還是輸出的目標文件句柄。如果是目標文件句柄,這意味着print將$_
寫入到文件句柄$rock_fh
中。如果是要輸出的列表數據,由於$rock_fh
是一個已定義好的文件句柄,print將輸出它的引用(類似於:GLOB(oxABCDEF12))。
這時可以使用大括號包圍文件句柄變量。
print {$rock_fh};
print {$rock_fh} "hello world";
最后需要注意的是,裸句柄是包變量,在整個文件內都是有效的,而變量方式的文件句柄只在代碼塊范圍內有效,出了自己的作用域范圍就失效(自動關閉)。
目錄句柄
目錄句柄和文件句柄類似,可以打開它,並讀取其中的文件列表。只不過需要使用:
- opendir替換open函數
- 使用readdir來替換readline,readdir返回一個列表
- readdir不會遞歸到子目錄中
- 使用closedir來替代close
- 注意每個Unix的目錄下都包含兩個特殊目錄
.
和..
opendir JAVAHOME,"/usr/local/java"
or die "Can't open dir handler: $!";
foreach $file (readdir JAVAHOME){
print "Filename: $file \n";
}
closedir JAVAHOME;