perlfaq5 - 檔案和格式
版本 5.20210520
本節探討 I/O 和「f」議題:檔案處理、清除緩衝區、格式和頁尾。
(由 brian d foy 提供)
您可能想閱讀 Mark Jason Dominus 在 http://perl.plover.com/FAQs/Buffering.html 上發表的「緩衝區的痛苦」一文。
Perl 通常會緩衝輸出,因此不會為每一點輸出執行系統呼叫。透過儲存輸出,它會減少昂貴的系統呼叫。例如,在這一點點程式碼中,您希望為處理的每一行印出一個點到螢幕上,以觀察程式進度。Perl 會緩衝輸出,因此您會長時間等待,才會一次看到 50 個點的列,而不是看到每一行的點
# long wait, then row of dots all at once
while( <> ) {
print ".";
print "\n" unless ++$count % 50;
#... expensive line processing operations
}
為了解決這個問題,您必須取消緩衝輸出檔案句柄,在本例中為 STDOUT
。您可以將特殊變數 $|
設定為 true 值(助記符:讓您的檔案句柄「熱騰騰」)
$|++;
# dot shown immediately
while( <> ) {
print ".";
print "\n" unless ++$count % 50;
#... expensive line processing operations
}
$|
是每個檔案句柄特殊變數之一,因此每個檔案句柄都有自己的值副本。例如,如果您要合併標準輸出和標準錯誤,您必須取消緩衝每個檔案句柄(儘管 STDERR 可能預設取消緩衝)
{
my $previous_default = select(STDOUT); # save previous default
$|++; # autoflush STDOUT
select(STDERR);
$|++; # autoflush STDERR, to be sure
select($previous_default); # restore previous default
}
# now should alternate . and +
while( 1 ) {
sleep 1;
print STDOUT ".";
print STDERR "+";
print STDOUT "\n" unless ++$count % 25;
}
除了 $|
特殊變數之外,您也可以使用 binmode
為您的檔案句柄提供 :unix
層,這是取消緩衝的
binmode( STDOUT, ":unix" );
while( 1 ) {
sleep 1;
print ".";
print "\n" unless ++$count % 50;
}
如需有關輸出層的更多資訊,請參閱 perlfunc 中 binmode
和 open 的條目,以及 PerlIO 模組文件。
如果您使用 IO::Handle 或其子類別之一,您可以呼叫 autoflush
方法來變更檔案句柄的設定
use IO::Handle;
open my( $io_fh ), ">", "output.txt";
$io_fh->autoflush(1);
IO::Handle 物件也有 flush
方法。您可以隨時清除緩衝區,而不需要自動緩衝
$io_fh->flush;
(由 brian d foy 提供)
插入、變更或刪除文字檔案中列的基本概念包括讀取和列印檔案至您要變更的點,進行變更,然後讀取和列印檔案的其餘部分。Perl 不提供列的隨機存取(特別是因為記錄輸入分隔符 $/
是可變的),儘管 Tie::File 等模組可以偽造它。
執行這些任務的 Perl 程式會採用開啟檔案、列印其列,然後關閉檔案的基本形式
open my $in, '<', $file or die "Can't read old file: $!";
open my $out, '>', "$file.new" or die "Can't write new file: $!";
while( <$in> ) {
print $out $_;
}
close $out;
在那個基本形式中,加入您需要插入、變更或刪除列的部分。
若要將行前置於開頭,請在列印現有行的迴圈之前列印這些行。
open my $in, '<', $file or die "Can't read old file: $!";
open my $out, '>', "$file.new" or die "Can't write new file: $!";
print $out "# Add this line to the top\n"; # <--- HERE'S THE MAGIC
while( <$in> ) {
print $out $_;
}
close $out;
若要變更現有行,請在 while
迴圈內插入用於修改行的程式碼。在此情況下,程式碼會找出所有小寫的「perl」並將其轉換成大寫。這會發生在每一行,所以請確定你真的要在每一行都這麼做!
open my $in, '<', $file or die "Can't read old file: $!";
open my $out, '>', "$file.new" or die "Can't write new file: $!";
print $out "# Add this line to the top\n";
while( <$in> ) {
s/\b(perl)\b/Perl/g;
print $out $_;
}
close $out;
若要僅變更特定行,輸入行號 $.
會很有用。首先讀取並列印出你想要變更的行。接著,讀取你想要變更的單一行,變更它,並將其列印出來。之後,讀取其餘行並將其列印出來
while( <$in> ) { # print the lines before the change
print $out $_;
last if $. == 4; # line number before change
}
my $line = <$in>;
$line =~ s/\b(perl)\b/Perl/g;
print $out $line;
while( <$in> ) { # print the rest of the lines
print $out $_;
}
若要略過行,請使用迴圈控制。此範例中的 next
會略過註解行,而 last
會在遇到 __END__
或 __DATA__
時停止所有處理。
while( <$in> ) {
next if /^\s+#/; # skip comment lines
last if /^__(END|DATA)__$/; # stop at end of code marker
print $out $_;
}
執行相同類型的動作來刪除特定行,方法是使用 next
略過你不想顯示在輸出中的行。此範例會略過每五行的第五行
while( <$in> ) {
next unless $. % 5;
print $out $_;
}
如果因為某些奇怪的原因,你真的想要一次看到整個檔案,而不是逐行處理,你可以將其吸入(只要你能將整個檔案塞進記憶體中!)
open my $in, '<', $file or die "Can't read old file: $!"
open my $out, '>', "$file.new" or die "Can't write new file: $!";
my $content = do { local $/; <$in> }; # slurp!
# do your magic here
print $out $content;
像 Path::Tiny 和 Tie::File 等模組也可以協助處理。不過,如果你能的話,請避免一次讀取整個檔案。Perl 在處理程序完成之前不會將該記憶體還給作業系統。
你也可以使用 Perl 一行指令來修改檔案。以下指令會將 inFile.txt 中的所有「Fred」變更為「Barney」,並以新內容覆寫檔案。使用 -p
開關,Perl 會將 while
迴圈包覆在你使用 -e
指定的程式碼,而 -i
會開啟原址編輯。目前行在 $_
中。使用 -p
,Perl 會在迴圈結束時自動列印 $_
的值。請參閱 perlrun 以取得更多詳細資料。
perl -pi -e 's/Fred/Barney/' inFile.txt
若要備份 inFile.txt
,請給 -i
一個檔案副檔名以進行新增
perl -pi.bak -e 's/Fred/Barney/' inFile.txt
若要僅變更第五行,你可以新增一個測試來檢查 $.
(輸入行號),然後僅在測試通過時執行操作
perl -pi -e 's/Fred/Barney/ if $. == 5' inFile.txt
若要新增行到某一行之前,您可以在 Perl 印出 $_
之前新增一行(或多行)
perl -pi -e 'print "Put before third line\n" if $. == 3' inFile.txt
您甚至可以新增一行到檔案開頭,因為目前的列印行會在迴圈結束時印出
perl -pi -e 'print "Put before first line\n" if $. == 1' inFile.txt
若要插入一行到檔案中已存在的一行之後,請使用 -n
參數。它就像 -p
,只是不會在迴圈結束時印出 $_
,因此您必須自己印出。在這種情況下,請先印出 $_
,然後印出您要新增的行。
perl -ni -e 'print; print "Put after fifth line\n" if $. == 5' inFile.txt
若要刪除行,請只印出您要保留的行。
perl -ni -e 'print if /d/' inFile.txt
(由 brian d foy 提供)
從概念上來說,計算檔案中行數最簡單的方法就是讀取並計算它們
my $count = 0;
while( <$fh> ) { $count++; }
您不必自己計算,因為 Perl 已透過 $.
變數完成這項工作,這是從最後讀取的檔案處理常式中目前的列號
1 while( <$fh> );
my $count = $.;
如果您想使用 $.
,您可以將它簡化為簡單的一行指令,如下所示
% perl -lne '} print $.; {' file
% perl -lne 'END { print $. }' file
不過,這些指令可能相當沒有效率。如果這些指令對您來說不夠快,您可能只會讀取資料塊並計算換行符號的數量
my $lines = 0;
open my($fh), '<:raw', $filename or die "Can't open $filename: $!";
while( sysread $fh, $buffer, 4096 ) {
$lines += ( $buffer =~ tr/\n// );
}
close $fh;
但是,如果行尾不是換行符號,這項方法便無法運作。您可以將 tr///
變更為 s///
,以便計算輸入記錄分隔符號 $/
出現的次數
my $lines = 0;
open my($fh), '<:raw', $filename or die "Can't open $filename: $!";
while( sysread $fh, $buffer, 4096 ) {
$lines += ( $buffer =~ s|$/||g; );
}
close $fh;
如果您不介意使用殼層,wc
指令通常是最快的,即使有額外的跨程序處理負擔。請確保您有一個未污染的檔案名稱
#!perl -T
$ENV{PATH} = undef;
my $lines;
if( $filename =~ /^([0-9a-z_.]+)\z/ ) {
$lines = `/usr/bin/wc -l $1`
chomp $lines;
}
(由 brian d foy 提供)
最簡單的概念性解決方案是計算檔案中的行數,然後從開頭開始,將行數(減去最後 N 行)印出到新的檔案中。
最常遇到的問題是如何在不對檔案進行多次掃描的情況下刪除最後 N 行,或如何不大量複製的情況下進行刪除。當您的檔案中有數百萬行時,這個簡單的概念將會變成嚴峻的現實。
一個方法是使用 File::ReadBackwards,它從檔案的結尾開始。該模組提供一個物件,將真實的檔案句柄封裝起來,讓您能輕鬆地在檔案中移動。一旦您移動到需要的點,就可以取得實際的檔案句柄,並像平常一樣使用它。在這個情況下,您可以在想要保留的最後一行的結尾取得檔案位置,並將檔案截斷到該點
use File::ReadBackwards;
my $filename = 'test.txt';
my $Lines_to_truncate = 2;
my $bw = File::ReadBackwards->new( $filename )
or die "Could not read backwards in [$filename]: $!";
my $lines_from_end = 0;
until( $bw->eof or $lines_from_end == $Lines_to_truncate ) {
print "Got: ", $bw->readline;
$lines_from_end++;
}
truncate( $filename, $bw->tell );
File::ReadBackwards 模組還有一個優點,就是將輸入記錄分隔符號設定為正規表示式。
您也可以使用 Tie::File 模組,它讓您能透過繫結陣列來存取各行。您可以使用一般的陣列運算來修改您的檔案,包括設定最後一個索引和使用 splice
。
-i
選項? -i
設定 Perl 的 $^I
變數的值,而這又會影響 <>
的行為;請參閱 perlrun 以取得更多詳細資料。透過直接修改適當的變數,您可以在較大的程式中獲得相同的行為。例如
# ...
{
local($^I, @ARGV) = ('.orig', glob("*.c"));
while (<>) {
if ($. == 1) {
print "This line should appear at the top of each file\n";
}
s/\b(p)earl\b/${1}erl/i; # Correct typos, preserving case
print;
close ARGV if eof; # Reset $.
}
}
# $^I and @ARGV return to their old values here
這個區塊會修改目前目錄中的所有 .c
檔案,並將每個檔案的原始資料備份到新的 .c.orig
檔案中。
(由 brian d foy 提供)
使用 File::Copy 模組。它隨附在 Perl 中,可以在檔案系統之間進行真正的複製,而且它以可攜式的方式執行其魔法。
use File::Copy;
copy( $original, $new_copy ) or die "Copy failed: $!";
如果您無法使用 File::Copy,您必須自己執行這項工作:開啟原始檔案,開啟目標檔案,然後在讀取原始檔案時列印到目標檔案。您還必須記得將權限、擁有者和群組複製到新檔案中。
如果您不需要知道檔案名稱,您可以使用 open()
,並在檔案名稱的位置使用 undef
。在 Perl 5.8 或更新版本中,open()
函式會建立一個匿名的暫存檔
open my $tmp, '+>', undef or die $!;
否則,您可以使用 File::Temp 模組。
use File::Temp qw/ tempfile tempdir /;
my $dir = tempdir( CLEANUP => 1 );
($fh, $filename) = tempfile( DIR => $dir );
# or if you don't need to know the filename
my $fh = tempfile( DIR => $dir );
File::Temp 自 Perl 5.6.1 以來就是標準模組。如果您安裝的 Perl 版本不夠新,請使用 IO::File 模組中的 new_tmpfile
類別方法取得可讀寫的檔案控制代碼。如果您不需要知道檔案名稱,請使用它
use IO::File;
my $fh = IO::File->new_tmpfile()
or die "Unable to make new temporary file: $!";
如果您堅持手動建立暫存檔案,請使用程序 ID 和/或目前的時值。如果您需要在一個程序中建立多個暫存檔案,請使用計數器
BEGIN {
use Fcntl;
use File::Spec;
my $temp_dir = File::Spec->tmpdir();
my $file_base = sprintf "%d-%d-0000", $$, time;
my $base_name = File::Spec->catfile($temp_dir, $file_base);
sub temp_file {
my $fh;
my $count = 0;
until( defined(fileno($fh)) || $count++ > 100 ) {
$base_name =~ s/-(\d+)$/"-" . (1 + $1)/e;
# O_EXCL is required for security reasons.
sysopen $fh, $base_name, O_WRONLY|O_EXCL|O_CREAT;
}
if( defined fileno($fh) ) {
return ($fh, $base_name);
}
else {
return ();
}
}
}
最有效率的方法是使用 pack() 和 unpack()。在取得許多字串時,這比使用 substr() 快。但如果只有幾個,則較慢。
以下是將一些固定格式的輸入行拆解並重新組合的範例程式碼片段,此處的輸入行來自 Berkeley 風格的 ps 輸出
# sample input line:
# 15158 p5 T 0:00 perl /home/tchrist/scripts/now-what
my $PS_T = 'A6 A4 A7 A5 A*';
open my $ps, '-|', 'ps';
print scalar <$ps>;
my @fields = qw( pid tt stat time command );
while (<$ps>) {
my %process;
@process{@fields} = unpack($PS_T, $_);
for my $field ( @fields ) {
print "$field: <$process{$field}>\n";
}
print 'line=', pack($PS_T, @process{@fields} ), "\n";
}
我們使用雜湊切片輕鬆處理每一列的欄位。將金鑰儲存在陣列中可輕鬆地將它們當成一個群組操作,或使用 for
迴圈處理它們。它也可以避免使用全域變數和符號參照來污染程式。
自 perl5.6 起,如果您傳遞未初始化的純量變數給 open(),它會自動將檔案和目錄控制代碼視為參照。然後您可以像其他純量一樣傳遞這些參照,並在命名控制代碼的位置使用它們。
open my $fh, $file_name;
open local $fh, $file_name;
print $fh "Hello World!\n";
process_file( $fh );
如果您願意,可以將這些檔案控制代碼儲存在陣列或雜湊中。如果您直接存取它們,它們就不是單純的純量,您需要將檔案控制代碼參照放在大括號中,才能讓 print
稍微協助一下。只有當檔案控制代碼參照是單純的純量時,Perl 才能自行找出。
my @fhs = ( $fh1, $fh2, $fh3 );
for( $i = 0; $i <= $#fhs; $i++ ) {
print {$fhs[$i]} "just another Perl answer, \n";
}
在 perl5.6 之前,您必須處理各種類型球體慣用語,您可以在較舊的程式碼中看到它們。
open FILE, "> $filename";
process_typeglob( *FILE );
process_reference( \*FILE );
sub process_typeglob { local *FH = shift; print FH "Typeglob!" }
sub process_reference { local $fh = shift; print $fh "Reference!" }
如果您想要建立許多匿名控制代碼,您應該查看 Symbol 或 IO::Handle 模組。
間接檔案控制代碼是在預期檔案控制代碼的位置使用符號以外的內容。以下是取得間接檔案控制代碼的方法
$fh = SOME_FH; # bareword is strict-subs hostile
$fh = "SOME_FH"; # strict-refs hostile; same package only
$fh = *SOME_FH; # typeglob
$fh = \*SOME_FH; # ref to typeglob (bless-able)
$fh = *SOME_FH{IO}; # blessed IO::Handle from *SOME_FH typeglob
或者,你可以使用 IO::* 模組中的 new
方法來建立一個匿名檔案句柄,並將其儲存在一個純量變數中。
use IO::Handle; # 5.004 or higher
my $fh = IO::Handle->new();
然後,你可以像使用一般檔案句柄一樣使用這些檔案句柄。在 Perl 預期檔案句柄的任何地方,都可以改用間接檔案句柄。間接檔案句柄只是一個包含檔案句柄的純量變數。print
、open
、seek
或 <FH>
菱形運算子等函式會接受已命名檔案句柄或包含檔案句柄的純量變數。
($ifh, $ofh, $efh) = (*STDIN, *STDOUT, *STDERR);
print $ofh "Type it: ";
my $got = <$ifh>
print $efh "What was that: $got";
如果你要將檔案句柄傳遞給函式,你可以用兩種方式撰寫函式。
sub accept_fh {
my $fh = shift;
print $fh "Sending to indirect filehandle\n";
}
或者,它可以將類型全域變數本地域化,並直接使用檔案句柄。
sub accept_fh {
local *FH = shift;
print FH "Sending to localized filehandle\n";
}
這兩種樣式都適用於物件或真實檔案句柄的類型全域變數。(在某些情況下,它們也可能適用於字串,但這樣做有風險。)
accept_fh(*STDOUT);
accept_fh($handle);
在上面的範例中,我們在使用檔案句柄之前,將其指定給一個純量變數。這是因為只有簡單的純量變數(而不是雜湊或陣列的運算式或下標)才能與內建函式(例如 print
、printf
或菱形運算子)一起使用。將非簡單純量變數用作檔案句柄是非法的,甚至無法編譯。
my @fd = (*STDIN, *STDOUT, *STDERR);
print $fd[1] "Type it: "; # WRONG
my $got = <$fd[0]> # WRONG
print $fd[2] "What was that: $got"; # WRONG
使用 print
和 printf
時,你可以透過使用區塊和運算式(你會在其中放置檔案句柄)來解決這個問題。
print { $fd[1] } "funny stuff\n";
printf { $fd[1] } "Pity the poor %x.\n", 3_735_928_559;
# Pity the poor deadbeef.
該區塊是一個適當的區塊,就像其他區塊一樣,因此你可以在其中放置更複雜的程式碼。這會將訊息傳送到兩個地方之一。
my $ok = -x "/bin/cat";
print { $ok ? $fd[1] : $fd[2] } "cat stat $ok\n";
print { $fd[ 1+ ($ok || 0) ] } "cat stat $ok\n";
將 print
和 printf
視為物件方法呼叫的這種方法不適用於菱形運算子。這是因為它是一個真正的運算子,而不仅仅是一个没有逗号参数的函数。假设你已将类型全域变量存储在你的结构中,如上所述,你可以使用名为 readline
的内置函数来读取记录,就像 <>
所做的那样。鉴于上面显示的 @fd 的初始化,这将起作用,但仅是因为 readline() 需要一个类型全域变量。它不适用于对象或字符串,这可能是一个我们尚未修复的错误。
$got = readline($fd[0]);
请注意,间接文件句柄的不稳定性与它们是字符串、类型全域变量、对象或其他任何内容无关。这是基本运算符的语法。在这里玩对象游戏对你没有任何帮助。
(由 Peter J. Holzer 提供,hjp-usenet2@hjp.at)
從 Perl 5.8.0 開始,可以透過呼叫 open 並傳遞對該字串的參照(而不是檔案名稱)來建立指向字串的檔案句柄。然後可以使用此檔案句柄從字串中讀取或寫入字串。
open(my $fh, '>', \$string) or die "Could not open string for writing";
print $fh "foo\n";
print $fh "bar\n"; # $string now contains "foo\nbar\n"
open(my $fh, '<', \$string) or die "Could not open string for reading";
my $x = <$fh>; # $x now contains "foo\n"
對於較舊版本的 Perl,IO::String 模組提供了類似的功能。
沒有內建的方法可以做到這一點,但 perlform 有幾種技術可以讓勇敢的黑客做到這一點。
(由 brian d foy 提供)
如果您想將內容寫入
字串,您只需將檔案代號<開啟>至字串,Perl 自 Perl 5.6 以來就能做到這一點
open FH, '>', \my $string;
write( FH );
由於您想成為一名優秀的程式設計師,您可能會想使用詞彙檔案代號,即使格式設計為與裸字檔案代號搭配使用,因為預設格式名稱會採用檔案代號名稱。不過,您可以使用一些 Perl 特殊的檔案代號變數來控制這一點:$^
,用於命名頁首格式,以及 $~
,用於顯示列格式。您必須變更預設檔案代號才能設定這些變數
open my($fh), '>', \my $string;
{ # set per-filehandle variables
my $old_fh = select( $fh );
$~ = 'ANIMAL';
$^ = 'ANIMAL_TOP';
select( $old_fh );
}
format ANIMAL_TOP =
ID Type Name
.
format ANIMAL =
@## @<<< @<<<<<<<<<<<<<<
$id, $type, $name
.
雖然寫入功能可以用於詞彙或套件變數,但您使用的任何變數都必須在格式中設定範圍。這很可能表示您會想將一些套件變數設為區域變數
{
local( $id, $type, $name ) = qw( 12 cat Buster );
write( $fh );
}
print $string;
您還可以對 formline
和累加器變數 $^A
執行一些技巧,但您會失去很多格式的價值,因為 formline
無法處理分頁等作業。當您使用格式時,您最終會重新實作格式。
(由 brian d foy 和 Benjamin Goldberg 提供)
您可以使用 Number::Format 來區分數字中的位數。它會處理地區資訊,讓那些想插入句點(或任何其他他們想使用的符號)的人可以使用。
這個子常式會在您的數字中加上逗號
sub commify {
local $_ = shift;
1 while s/^([-+]?\d+)(\d{3})/$1,$2/;
return $_;
}
Benjamin Goldberg 的這個正規表示式會在數字中加上逗號
s/(^[-+]?\d+?(?=(?>(?:\d{3})+)(?!\d))|\G\d{3}(?=\d))/$1,/g;
加上註解後會比較容易理解
s/(
^[-+]? # beginning of number.
\d+? # first digits before first comma
(?= # followed by, (but not included in the match) :
(?>(?:\d{3})+) # some positive multiple of three digits.
(?!\d) # an *exact* multiple, not x * 3 + 1 or whatever.
)
| # or:
\G\d{3} # after the last group, get three digits
(?=\d) # but they have to have more digits after them.
)/$1,/xg;
使用 <>(glob()
)運算子,文件記載於 perlfunc 中。早於 Perl 5.6 的 Perl 版本需要您安裝一個可以理解波浪號的殼層程式。較新的 Perl 版本內建了這個功能。 File::KGlob 模組(可從 CPAN 取得)提供更具可攜性的 glob 功能。
在 Perl 中,您可以直接使用這個
$filename =~ s{
^ ~ # find a leading tilde
( # save this in $1
[^/] # a non-slash character
* # repeated 0 or more times (0 means me)
)
}{
$1
? (getpwnam($1))[7]
: ( $ENV{HOME} || $ENV{LOGDIR} )
}ex;
因為您使用了類似這樣的指令,它會先將檔案截斷,然後再讓您讀寫存取
open my $fh, '+>', '/path/name'; # WRONG (almost always)
糟糕。您應該改用這個指令,如果檔案不存在,它會失敗
open my $fh, '+<', '/path/name'; # open for update
使用「>」總是會覆寫或建立。使用「<」則永遠不會執行這兩個動作。「+」不會改變這一點。
以下是各種檔案開啟範例。所有使用 sysopen
的範例都假設您已從 Fcntl 擷取常數
use Fcntl;
開啟檔案以進行讀取
open my $fh, '<', $path or die $!;
sysopen my $fh, $path, O_RDONLY or die $!;
開啟檔案以進行寫入,必要時建立新檔案或截斷舊檔案
open my $fh, '>', $path or die $!;
sysopen my $fh, $path, O_WRONLY|O_TRUNC|O_CREAT or die $!;
sysopen my $fh, $path, O_WRONLY|O_TRUNC|O_CREAT, 0666 or die $!;
開啟檔案以進行寫入,建立新檔案,檔案不得存在
sysopen my $fh, $path, O_WRONLY|O_EXCL|O_CREAT or die $!;
sysopen my $fh, $path, O_WRONLY|O_EXCL|O_CREAT, 0666 or die $!;
開啟檔案以進行附加,必要時建立
open my $fh, '>>', $path or die $!;
sysopen my $fh, $path, O_WRONLY|O_APPEND|O_CREAT or die $!;
sysopen my $fh, $path, O_WRONLY|O_APPEND|O_CREAT, 0666 or die $!;
開啟檔案以進行附加,檔案必須存在
sysopen my $fh, $path, O_WRONLY|O_APPEND or die $!;
開啟檔案以進行更新,檔案必須存在
open my $fh, '+<', $path or die $!;
sysopen my $fh, $path, O_RDWR or die $!;
開啟檔案以進行更新,必要時建立檔案
sysopen my $fh, $path, O_RDWR|O_CREAT or die $!;
sysopen my $fh, $path, O_RDWR|O_CREAT, 0666 or die $!;
開啟檔案以進行更新,檔案不得存在
sysopen my $fh, $path, O_RDWR|O_EXCL|O_CREAT or die $!;
sysopen my $fh, $path, O_RDWR|O_EXCL|O_CREAT, 0666 or die $!;
開啟檔案而不進行封鎖,必要時建立
sysopen my $fh, '/foo/somefile', O_WRONLY|O_NDELAY|O_CREAT
or die "can't open /foo/somefile: $!":
請注意,透過 NFS 建立或刪除檔案並不保證為原子操作。也就是說,兩個程序都可能成功建立或取消連結同一個檔案!因此,O_EXCL 並不像您想像的那麼獨佔。
另請參閱 perlopentut。
<>
算子會執行 glob 操作(請參閱上方)。在早於 v5.6.0 的 Perl 版本中,內部的 glob() 算子會分岔 csh(1) 來執行實際的 glob 展開,但 csh 無法處理超過 127 個項目,因此會傳回錯誤訊息 引數清單太長
。將 tcsh 安裝為 csh 的使用者不會遇到這個問題,但他們的使用者可能會感到驚訝。
要解決這個問題,請升級到 Perl v5.6.0 或更新版本,使用 readdir() 和模式自行執行 glob,或使用不會使用 shell 執行 glob 的模組,例如 File::Glob。
(由 Brian McCauley 提供)
Perl 的 open() 函式的特殊雙引數形式會忽略檔案名稱中的結尾空白,並從某些開頭字元(或結尾的「|」)推斷模式。在較舊版本的 Perl 中,這是 open() 的唯一版本,因此在舊程式碼和書籍中很常見。
除非您有特殊理由使用雙引數形式,否則您應該使用 open() 的三引數形式,它不會將檔案名稱中的任何字元視為特殊字元。
open my $fh, "<", " file "; # filename is " file "
open my $fh, ">", ">file"; # filename is ">file"
如果您的作業系統支援適當的 mv(1) 工具程式或其功能等效項,則此方法可行
rename($old, $new) or system("mv", $old, $new);
使用 File::Copy 模組可能會更具可移植性。您只需將檔案複製到新名稱(檢查回傳值),然後刪除舊檔案即可。這在語意上與 rename()
不同,後者會保留元資訊,例如權限、時間戳記、inode 資訊等。
Perl 內建的 flock() 函式(詳情請參閱 perlfunc)如果存在,將呼叫 flock(2);如果不存在,將呼叫 fcntl(2)(在 perl 版本 5.004 及更新版本中);如果前兩個系統呼叫都不存在,將呼叫 lockf(3)。在某些系統上,它甚至可能會使用不同的原生鎖定形式。以下是 Perl 的 flock() 的一些注意事項
如果三個系統呼叫(或其近似等效項)都不存在,則會產生致命錯誤。
lockf(3) 不提供共享鎖定,且要求檔案句柄已開啟為寫入(或追加,或讀取/寫入)。
某些版本的 flock() 無法鎖定網路上的檔案(例如在 NFS 檔案系統上),因此您需要在建置 Perl 時強制使用 fcntl(2)。但即使如此,充其量也只會令人懷疑。請參閱 perlfunc 的 flock 條目和原始程式碼散佈中的 INSTALL 檔案,以取得有關建置 Perl 以執行此項操作的資訊。
兩個潛在的、不顯而易見的傳統 flock 語意是,它會無限期地等待,直到鎖定被授予,而且它的鎖定只是「建議性的」。這種自由裁量的鎖定較為靈活,但提供的保證較少。這表示使用 flock() 鎖定的檔案可能會被沒有使用 flock() 的程式修改。會在紅燈前停車的車輛彼此相安無事,但不會與不會在紅燈前停車的車輛相安無事。請參閱 perlport 手冊頁、您所在埠口的特定文件或您系統特定的本機手冊頁,以取得詳細資料。如果您撰寫的是可攜式程式,最好假設傳統行為。(如果您不是,您應該像往常一樣,完全自由地為您自己的系統特性(有時稱為「功能」)撰寫程式。對可攜性的奴性堅持不應妨礙您完成工作。)
有關檔案鎖定的更多資訊,如果您有「perlopentut 中的「檔案鎖定」」,也請參閱 /perlopentut#File-Locking(5.6 的新功能)。
不要使用的常見程式碼片段如下:
sleep(3) while -e 'file.lock'; # PLEASE DO NOT USE
open my $lock, '>', 'file.lock'; # THIS BROKEN CODE
這是一個典型的競爭情況:您採取兩個步驟來執行必須一次完成的某件事。這就是電腦硬體提供原子測試和設定指令的原因。理論上,這「應該」有效
sysopen my $fh, "file.lock", O_WRONLY|O_EXCL|O_CREAT
or die "can't open file.lock: $!";
但很遺憾的是,檔案建立(和刪除)在 NFS 上不是原子的,因此這無法正常運作(至少無法在網路上每次都正常運作)。已經提出各種涉及 link() 的方案,但這些方案往往涉及忙碌等待,這也不是很理想。
難道沒有人告訴過您網頁點擊計數器是無用的嗎?它們不會計算點擊次數,它們只是浪費時間,而且只會滿足寫作者的虛榮心。最好選取一個隨機數字;它們更為實際。
無論如何,如果您無法克制自己,以下是您可以執行的操作。
use Fcntl qw(:DEFAULT :flock);
sysopen my $fh, "numfile", O_RDWR|O_CREAT or die "can't open numfile: $!";
flock $fh, LOCK_EX or die "can't flock numfile: $!";
my $num = <$fh> || 0;
seek $fh, 0, 0 or die "can't rewind numfile: $!";
truncate $fh, 0 or die "can't truncate numfile: $!";
(print $fh $num+1, "\n") or die "can't write numfile: $!";
close $fh or die "can't close numfile: $!";
以下是一個更好的網頁點擊計數器
$hits = int( (time() - 850_000_000) / rand(1_000) );
如果數量無法讓你的朋友留下深刻印象,那麼程式碼可能可以。 :-)
如果你在正確執行 flock
的系統上,並且你使用「perldoc -f flock」中的範例附加程式碼,即使你所在的作業系統未正確執行附加模式(如果存在這樣的系統),一切都會沒問題。因此,如果你很樂意將自己限制在執行 flock
的作業系統(這並非真正的限制),那麼你應該這麼做。
如果你知道你只會使用正確執行附加功能的系統(例如非 Win32),則可以從前一個答案中的程式碼中省略 seek
。
如果你知道你只會撰寫在正確執行附加模式的作業系統和檔案系統上執行的程式碼(例如現代 Unix 上的本機檔案系統),並且你將檔案保持在區塊緩衝模式中,而且你每次手動清除緩衝區時寫入的輸出少於一個緩衝區的容量,那麼幾乎可以保證每個緩衝區的載入都會以一個區塊寫入檔案的結尾,而不會與其他人的輸出混雜在一起。你也可以使用 syswrite
函式,它只是一個包裝在你的系統 write(2)
系統呼叫周圍的包裝函式。
系統層級的 write()
作業在完成之前仍有很小的理論機會會被訊號中斷。有些 STDIO 執行可能會呼叫多個系統層級的 write()
,即使緩衝區一開始是空的。有些系統可能會將此機率降低到零,而這在使用 :perlio
取代系統的 STDIO 時並非問題。
如果你只是想修補二進位檔案,在許多情況下,像這樣簡單的方法就有效
perl -i -pe 's{window manager}{window mangler}g' /usr/bin/emacs
但是,如果你有固定大小的記錄,則可以執行更像這樣的方法
my $RECSIZE = 220; # size of record, in bytes
my $recno = 37; # which record to update
open my $fh, '+<', 'somewhere' or die "can't update somewhere: $!";
seek $fh, $recno * $RECSIZE, 0;
read $fh, $record, $RECSIZE == $RECSIZE or die "can't read record $recno: $!";
# munge the record
seek $fh, -$RECSIZE, 1;
print $fh $record;
close $fh;
鎖定和錯誤檢查留給讀者練習。別忘了它們,否則你會很抱歉。
如果你想要擷取檔案上次讀取、寫入或其元資料(擁有者等)變更的時間,請使用 perlfunc 中所記錄的 -A、-M 或 -C 檔案測試作業。這些作業會以浮點數形式擷取檔案的年齡(以程式開始時間為基準)以天為單位。有些平台可能沒有所有這些時間。有關詳細資訊,請參閱 perlport。若要擷取紀元時間以來的「原始」時間(以秒為單位),請呼叫 stat 函式,然後使用 localtime()
、gmtime()
或 POSIX::strftime()
將其轉換為人類可讀的形式。
以下是一個範例
my $write_secs = (stat($file))[9];
printf "file %s updated at %s\n", $file,
scalar localtime($write_secs);
如果您偏好更易讀的內容,請使用 File::stat 模組(5.004 及更新版本中標準發行的一部分)
# error checking left as an exercise for reader.
use File::stat;
use Time::localtime;
my $date_string = ctime(stat($file)->mtime);
print "file $file updated at $date_string\n";
理論上,POSIX::strftime() 方法不受目前區域設定影響,這是一個優點。有關詳細資訊,請參閱 perllocale。
請使用 perlfunc 中的「utime」 中說明的 utime() 函數。以下是一個範例程式,它會將第一個引數的讀取和寫入時間複製到所有其他引數。
if (@ARGV < 2) {
die "usage: cptimes timestamp_file other_files ...\n";
}
my $timestamp = shift;
my($atime, $mtime) = (stat($timestamp))[8,9];
utime $atime, $mtime, @ARGV;
錯誤檢查一如往常,留待讀者練習。
utime 的 perldoc 也有範例,對 已存在的 檔案有與 touch(1) 相同的效果。
某些檔案系統儲存檔案時間時,在預期的精確度層級上會受到限制。例如,FAT 和 HPFS 檔案系統無法以比兩秒更精細的顆粒度建立檔案日期。這是檔案系統的限制,與 utime() 無關。
若要將一個檔案控制代碼連接到多個輸出檔案控制代碼,您可以使用 IO::Tee 或 Tie::FileHandle::Multiplex 模組。
如果您只需要執行一次,您可以個別列印到每個檔案控制代碼。
for my $fh ($fh1, $fh2, $fh3) { print $fh "whatever\n" }
Perl 處理檔案中所有行的慣用方法是逐行處理
open my $input, '<', $file or die "can't open $file: $!";
while (<$input>) {
chomp;
# do something with $_
}
close $input or die "can't close $file: $!";
這比將整個檔案讀取到記憶體中作為陣列,然後一次處理一個元素,要有效率得多,後者通常(如果不是幾乎總是)錯誤的方法。每當您看到有人這麼做時
my @lines = <INPUT>;
您應該仔細思考為什麼需要一次載入所有內容。這並不是一個可擴充的解決方案。
如果您使用 CPAN 中的 File::Map 模組「mmap」檔案,您可以在實際上不將檔案儲存在記憶體中,就能將整個檔案載入字串
use File::Map qw(map_file);
map_file my $string, $filename;
一旦對應,您就可以將 $string
視為任何其他字串。由於您不一定需要載入資料,因此 mmap-ing 可以非常快速,並且可能不會增加您的記憶體使用量。
您可能還會覺得使用標準 Tie::File 模組或 DB_File 模組的 $DB_RECNO
繫結更有趣,這些模組讓您可以將陣列繫結到檔案,以便存取陣列的元素實際上會存取檔案中的對應行。
如果您想要載入整個檔案,您可以使用 Path::Tiny 模組,以一個簡單且有效率的步驟完成
use Path::Tiny;
my $all_of_it = path($filename)->slurp; # entire file in scalar
my @all_lines = path($filename)->lines; # one line per element
或者,你可以像這樣將整個檔案內容讀取到一個純量中
my $var;
{
local $/;
open my $fh, '<', $file or die "can't open $file: $!";
$var = <$fh>;
}
這會暫時取消定義你的記錄分隔符號,並會在區塊退出時自動關閉檔案。如果檔案已經開啟,只需使用這個
my $var = do { local $/; <$fh> };
你也可以使用一個本地的 @ARGV
來消除 open
my $var = do { local( @ARGV, $/ ) = $file; <> };
使用 $/
變數(詳情請參閱 perlvar)。你可以將它設定為 ""
以消除空段落(例如,"abc\n\n\n\ndef"
會被視為兩個段落,而不是三個),或 "\n\n"
以接受空段落。
請注意,空白行中不能有空白。因此 "fred\n \nstuff\n\n"
是同一段落,但 "fred\n\nstuff\n\n"
是兩段。
你可以對大多數檔案句柄使用內建的 getc()
函式,但它無法(輕鬆地)在終端機裝置上執行。對於 STDIN,請使用 CPAN 中的 Term::ReadKey 模組,或使用 "getc" in perlfunc 中的範例程式碼。
如果你的系統支援可攜式作業系統程式介面 (POSIX),你可以使用以下程式碼,你會注意到它也會關閉迴音處理。
#!/usr/bin/perl -w
use strict;
$| = 1;
for (1..4) {
print "gimme: ";
my $got = getone();
print "--> $got\n";
}
exit;
BEGIN {
use POSIX qw(:termios_h);
my ($term, $oterm, $echo, $noecho, $fd_stdin);
my $fd_stdin = fileno(STDIN);
$term = POSIX::Termios->new();
$term->getattr($fd_stdin);
$oterm = $term->getlflag();
$echo = ECHO | ECHOK | ICANON;
$noecho = $oterm & ~$echo;
sub cbreak {
$term->setlflag($noecho);
$term->setcc(VTIME, 1);
$term->setattr($fd_stdin, TCSANOW);
}
sub cooked {
$term->setlflag($oterm);
$term->setcc(VTIME, 0);
$term->setattr($fd_stdin, TCSANOW);
}
sub getone {
my $key = '';
cbreak();
sysread(STDIN, $key, 1);
cooked();
return $key;
}
}
END { cooked() }
CPAN 中的 Term::ReadKey 模組可能更容易使用。最近的版本也支援非可攜式系統。
use Term::ReadKey;
open my $tty, '<', '/dev/tty';
print "Gimme a char: ";
ReadMode "raw";
my $key = ReadKey 0, $tty;
ReadMode "normal";
printf "\nYou said %s, char number %03d\n",
$key, ord $key;
你應該做的第一件事是從 CPAN 取得 Term::ReadKey 擴充套件。正如我們前面提到的,它現在甚至對非可攜式(請閱讀:非開放系統、封閉、專有、非 POSIX、非 Unix 等)系統提供有限的支援。
你還應該查看 comp.unix.* 中的常見問題清單,瞭解這類問題:答案基本上是相同的。這非常依賴於系統。以下是 BSD 系統上可行的解決方案
sub key_ready {
my($rin, $nfd);
vec($rin, fileno(STDIN), 1) = 1;
return $nfd = select($rin,undef,undef,0);
}
如果你想找出有多少字元在等待,還可以使用 FIONREAD ioctl 呼叫來查看。Perl 附帶的 h2ph 工具會嘗試將 C include 檔案轉換為 Perl 程式碼,可以 require
。FIONREAD 最終在 sys/ioctl.ph 檔案中定義為一個函式
require './sys/ioctl.ph';
$size = pack("L", 0);
ioctl(FH, FIONREAD(), $size) or die "Couldn't call ioctl: $!\n";
$size = unpack("L", $size);
如果沒有安裝 h2ph 或它對你不起作用,你可以手動 grep include 檔案
% grep FIONREAD /usr/include/*/*
/usr/include/asm/ioctls.h:#define FIONREAD 0x541B
或者使用冠軍編輯器撰寫一個小型 C 程式
% cat > fionread.c
#include <sys/ioctl.h>
main() {
printf("%#08x\n", FIONREAD);
}
^D
% cc -o fionread fionread.c
% ./fionread
0x4004667f
然後硬編碼它,將移植留給你的繼任者作為練習。
$FIONREAD = 0x4004667f; # XXX: opsys dependent
$size = pack("L", 0);
ioctl(FH, $FIONREAD, $size) or die "Couldn't call ioctl: $!\n";
$size = unpack("L", $size);
FIONREAD 需要一個連接到串流的文件句柄,表示套接字、管道和 tty 裝置有效,但不適用於檔案。
tail -f
? 第一次嘗試
seek($gw_fh, 0, 1);
陳述式 seek($gw_fh, 0, 1)
沒有變更目前位置,但它會清除句柄上的檔案結束條件,因此下一個 <$gw_fh>
會讓 Perl 再次嘗試讀取某些內容。
如果這不起作用(它依賴於 stdio 實作的功能),那麼你需要更類似於此的內容
for (;;) {
for ($curpos = tell($gw_fh); <$gw_fh>; $curpos =tell($gw_fh)) {
# search for some stuff and put it into files
}
# sleep for a while
seek($gw_fh, $curpos, 0); # seek to where we had been
}
如果這仍然不起作用,請查看 IO::Handle 中的 clearerr
方法,它會重設句柄上的錯誤和檔案結束狀態。
CPAN 中還有一個 File::Tail 模組。
如果你查看 "open" in perlfunc,你會看到呼叫 open() 的幾種方法應該可以解決問題。例如
open my $log, '>>', '/foo/logfile';
open STDERR, '>&', $log;
甚至使用文字數字描述符
my $fd = $ENV{MHCONTEXTFD};
open $mhcontext, "<&=$fd"; # like fdopen(3S)
請注意,"<&STDIN" 會製作一份副本,但 "<&=STDIN" 會製作一個別名。這表示如果你關閉一個別名句柄,所有別名都將無法存取。這不適用於複製的句柄。
一如往常,錯誤檢查留給讀者練習。
如果由於某些原因,你有一個檔案描述符而不是檔案句柄(也許你使用了 POSIX::open
),你可以使用 POSIX 模組中的 close()
函數
use POSIX ();
POSIX::close( $fd );
這很少有必要,因為 Perl close()
函數用於 Perl 自己開啟的東西,即使它是數字描述符的 dup,例如上面提到的 MHCONTEXT
。但如果你真的必須這麼做,你可以執行以下操作
require './sys/syscall.ph';
my $rc = syscall(SYS_close(), $fd + 0); # must force numeric
die "can't sysclose $fd: $!" unless $rc == -1;
或者,只需使用 open()
的 fdopen(3S) 功能
{
open my $fh, "<&=$fd" or die "Cannot reopen fd=$fd: $!";
close $fh;
}
糟糕!你剛在那個檔案名稱中放入一個 tab 和一個換頁符!請記住,在雙引號字串("like\this")中,反斜線是一個跳脫字元。這些字元的完整清單在 "Quote and Quote-like Operators" in perlop 中。毫不意外地,你的舊版 DOS 檔案系統上沒有稱為 "c:(tab)emp(formfeed)oo" 或 "c:(tab)emp(formfeed)oo.exe" 的檔案。
請為字串加上單引號,或(建議)使用正斜線。由於自 MS-DOS 2.0 或類似的版本以來,所有 DOS 和 Windows 版本在路徑中都將 /
和 \
視為相同,因此您可以使用不會與 Perl 衝突的版本,或 POSIX shell、ANSI C 和 C++、awk、Tcl、Java 或 Python,僅舉幾例。POSIX 路徑也較具可攜性。
因為即使在非 Unix 埠上,Perl 的 glob 函式也會遵循標準 Unix 全域比對語意。您需要 glob("*")
才能取得所有(非隱藏)檔案。這使得 glob() 甚至可移植到舊有系統。您的埠也可能包含專有全域比對函式。請查看其文件以取得詳細資訊。
-i
會覆寫受保護的檔案?這不是 Perl 的錯誤嗎?這在 http://www.cpan.org/misc/olddoc/FMTEYEWTK.tgz 中「Far More Than You Ever Wanted To Know」系列的 file-dir-perms 文章中得到了詳盡且仔細的說明。
執行摘要:了解您的檔案系統如何運作。檔案的權限說明該檔案中的資料可能發生什麼事。目錄的權限說明該目錄中的檔案清單可能發生什麼事。如果您刪除檔案,您會從目錄中移除其名稱(因此作業取決於目錄的權限,而非檔案的權限)。如果您嘗試寫入檔案,檔案的權限會決定您是否被允許。
除了將檔案載入資料庫或預先建立檔案中列的索引之外,您可以執行下列幾件事。
以下是 Camel Book 中的貯水池抽樣演算法
srand;
rand($.) < 1 && ($line = $_) while <>;
這在空間上比讀取整個檔案有顯著的優勢。您可以在唐納德·E·克努斯所著的《電腦程式設計的藝術》第 2 卷第 3.4.2 節中找到此方法的證明。
您可以使用提供此演算法函式的 File::Random 模組
use File::Random qw/random_line/;
my $line = random_line($filename);
另一種方法是使用 Tie::File 模組,它將整個檔案視為陣列。只需存取隨機陣列元素即可。
(由 brian d foy 提供)
如果您在列印陣列時看到陣列元素之間有空格,您可能是使用雙引號內插陣列
my @animals = qw(camel llama alpaca vicuna);
print "animals are: @animals\n";
這是雙引號造成的,而不是 print
。每當您在雙引號上下文中內插陣列時,Perl 會使用空格(或 $"
中的內容,預設為空格)加入元素
animals are: camel llama alpaca vicuna
這與不使用內插列印陣列不同
my @animals = qw(camel llama alpaca vicuna);
print "animals are: ", @animals, "\n";
現在輸出在元素之間沒有空格,因為 @animals
的元素只會成為 print
清單的一部分
animals are: camelllamaalpacavicuna
當 @array
的每個元素都以換行符號結尾時,您可能會注意到這一點。您希望每行列印一個元素,但請注意,第一行之後的每一行都會縮排
this is a line
this is another line
this is the third line
額外的空格來自陣列的內插。如果您不想在陣列元素之間放置任何內容,請不要在雙引號中使用陣列。您可以將它傳送給 print
,而不用雙引號。
print @lines;
(由 brian d foy 提供)
隨附 Perl 的 File::Find 模組會執行所有困難的工作來橫越目錄結構。它隨附 Perl。您只需使用回呼子程式和您想要橫越的目錄來呼叫 find
子程式
use File::Find;
find( \&wanted, @directories );
sub wanted {
# full path in $File::Find::name
# just filename in $_
... do whatever you want to do ...
}
您可以從 CPAN 下載的 File::Find::Closures 提供許多可立即使用的子程式,您可以將它們與 File::Find 搭配使用。
你可以從 CPAN 下載的 File::Finder,可以幫助你使用更接近於 find
指令列工具語法的東西來建立回呼子程式
use File::Find;
use File::Finder;
my $deep_dirs = File::Finder->depth->type('d')->ls->exec('rmdir','{}');
find( $deep_dirs->as_options, @places );
你可以從 CPAN 下載的 File::Find::Rule 模組,有類似的介面,但也會幫你執行遍歷
use File::Find::Rule;
my @files = File::Find::Rule->file()
->name( '*.pm' )
->in( @INC );
(由 brian d foy 提供)
如果你有一個空的目錄,你可以使用 Perl 內建的 rmdir
。如果目錄不為空(有檔案或子目錄),你必須自己清空它(很多工作)或使用模組來幫助你。
隨 Perl 附帶的 File::Path 模組,有一個 remove_tree
可以幫你處理所有困難的工作
use File::Path qw(remove_tree);
remove_tree( @directories );
File::Path 模組也有舊版的介面,可使用較舊的 rmtree
子程式。
(由 Shlomi Fish 提供)
要在可攜式的 Perl 中執行等同於 cp -R
的動作(即遞迴複製整個目錄樹),你需要自己撰寫程式碼或尋找一個好的 CPAN 模組,例如 File::Copy::Recursive。
版權所有 (c) 1997-2010 Tom Christiansen、Nathan Torkington 和其他作者,如註明。保留所有權利。
此文件是免費的;你可以根據與 Perl 相同的條款重新散布或修改它。
不論其散布方式,這裡的所有程式碼範例都是公有領域。你被允許且鼓勵在你的程式中使用此程式碼和任何衍生品,以供娛樂或營利,視你所見為合適。在程式碼中加入一個簡單的註解,說明此程式碼來自常見問答集,會很客氣,但並非必要。