Perl IO:隨機讀寫檔案

駿馬金龍發表於2019-03-01

隨機讀寫

如果一個檔案控制程式碼是指向一個實體檔案的,那麼就可以對它進行隨機資料的訪問(包括隨機讀、寫),隨機訪問表示可以讀取檔案中的任何一部分資料或者向檔案中的任何一個位置處寫入資料。實現這種隨機讀寫的功能依賴於一個檔案讀寫位置指標(file pointer)

當一個檔案控制程式碼關聯到了一個實體檔案後,就可以操作這個檔案控制程式碼,比如通過這個檔案控制程式碼去移動檔案的讀寫指標,當這個指標指向第100個位元組位置處時,就表示從100個位元組處開始讀資料,或者從100個位元組處開始寫資料。

可以通過seek()函式來設定讀寫指標的位置,通過tell()函式來獲取檔案讀寫指標的位置。如果願意的話,IO::Seekable模組同樣提供了一些等價的物件導向的操作方法,不過它們只是對seek、tell的一些封裝,用法和工作機制是完全一樣的。

需要注意的是,雖說檔案讀寫指標不是屬於檔案控制程式碼的,但通過檔案控制程式碼去操作讀寫指標的時候,可以認為指標是屬於控制程式碼的。例如,同一個檔案的多個檔案控制程式碼的讀寫指標是獨立的,如果檔案A上同時開啟了A1和A2兩個檔案控制程式碼,那麼在A1上設定的讀寫指標不會影響A2控制程式碼的讀寫指標。

seek跳轉檔案指標位置

通過seek()函式,可以讓檔案的指標隨意轉到哪個位置。注意還有一個sysseek()函式,它們不同,seek()是工作在buffer上的,sysseek()是底層無buffer的。

seek FILEHANDLE, POSITION, WHENCE
複製程式碼

seek()有三個引數:

  • 第一個引數是檔案控制程式碼
  • 第二個引數是正負整數或0,它的意義由第三個引數決定
  • 第三個引數是flag,用來表明相對與哪個位置進行跳轉的,值可以是0、1和2。如果匯入了Fcntl模組的seek標籤(即use Fcntl qw(:seek)),則可以使用0、1、2對應的常量SEEK_SET、SEEK_CUR、SEEK_END來替代0、1、2

第三個引數的值決定第二個引數的意義。如下:

seek                           意義
-----------------------------------------------------------------
seek FH, $pos, 0             以相對於檔案開頭的位置跳轉$pos個位元組,
seek FH, $pos, SEEK_SET      即直接按照絕對位置的方式設定指標位置。
                             pos不能是負數,否則表示跳轉到檔案頭的
                             前面,這會使得seek失敗而返回0。例如
                             `seek FH, 0, 0`表示跳轉到開頭(第一個
                             位元組前),`seek FH, 10, 0`表示跳轉到第
                             10位元組前

seek FH, $pos, 1             以相對於當前指標的位置向前(pos為正數)、
seek FH, $pos, SEEK_CUR      向後(pos為負數)跳轉pos個位元組,pos=0表
                             示保持原地不動。例如`seek FH, 60, 1`
                             表示指標向右(向前)移動60個位元組。如果移
                             動到超出檔案的位置並從這裡寫入資料,將
                             以空位元組`\0`填充直到那個位置

seek FH, $pos, 2             以相對於檔案尾部的位置跳轉$pos個位元組。
seek FH, $pos, SEEK_END      如果pos為負數,表示向檔案頭方向移動pos
                             個位元組,如果pos為0則表示保持在尾部不動,
                             如果pos大於0且要寫入,則會以空位元組`\0`
                             填充直到那個位置。例如`seek FH, -60, 2`
                             表示從檔案尾部向檔案頭部移動60個位元組
複製程式碼

seek在成功跳轉成功時返回true,否則返回0值,例如想要跳轉到檔案頭的前面,這時返回0且將指標放置到檔案的結尾。

比如用seek來建立一個大檔案:

open BIGFILE, ">", "bigfile.txt";
seek BIGFILE, 100*1024, 0;  # 100K
syswrite BIGFILE, 'endendend'  # 100k + 9bytes
close BIGFILE
複製程式碼

跳轉超出檔案尾部後,如果要真的讓檔案擴充,需要在結尾的地方寫入一點資料,否則不會填充。這就相當於用類似於下面的dd命令建立一個稀疏大檔案一樣。

dd if=/dev/zero of=bigfile seek=100 count=1 bs=1K
複製程式碼

tell()函式獲取檔案指標位置

tell FILEHANDLE
複製程式碼

tell函式獲取給定檔案控制程式碼當前檔案指標的位置。

唯一需要注意的一點是,如果檔案控制程式碼指向的檔案描述符不是一個實體檔案,比如套接字控制程式碼,tell將返回-1。注意不是返回undef,儘管我們可能更期待它返回undef來判斷。

$pos = tell MYHANDLE;
print "POS is", $pos > -1 ? $pos : "not file", "\n";
複製程式碼

IO::Seekable

IO::Seekable模組提供了seek和tell的封裝方法。例如:

$fh->seek($pos, 0);         # SEEK_SET
$fh->seek($pos, SEEK_CUR);
$pos = $fh->tell();
複製程式碼

seek在EOF處讀

就像實現tail -f一樣監控每秒寫入到檔案尾部的資料並輸出。如果使用seek來實現這個功能的話,參考如下:

#!/usr/bin/perl
use strict;
use warnings;

die "give me a file" unless(@ARGV and -f $ARGV[0])
open my $taillog, $ARGV[0];

while(1){
    while(<$tailog>){print "$.: $_";}
    seek $taillog, 0, 1;
    sleep 1;
}
複製程式碼

上面的程式中,先讀取出檔案中的資料,然後將檔案的指標保持在原地以便下次迴圈繼續從這裡開始讀取,睡一秒後繼續,這個邏輯並不難。

當然,對於上面簡單的tail -f來說,根本沒使用seek的必要,但是這提供了一種連續從尾部讀取資料的思路。

seek在EOF處寫

典型的是寫日誌檔案,要不斷地向檔案尾部追加一行行日誌資料。但是,多個程式可能會互相覆蓋資料,因為不同程式的寫真正是互相獨立的,誰也不知道誰的指標在哪裡。如果使用的是追加式寫入方式,則多程式間不會出現資料覆蓋的問題,因為每次append資料之前都會將指標放到檔案的最結尾處。但是多個程式的append無法保證每行資料寫入的順序。

如果要保證某程式某次兩行資料的寫入是緊連在一起的,那麼需要使用鎖的方式,例如使用flock檔案鎖。

下面是一個簡單的日誌寫入程式示例:

#!/usr/bin/perl
use strict;
use warnings;
use Fcntl qw(:flock :seek);

sub logtofile {
    die "give me two args" if @_ < 1;
    my $logfile = shift;
    my @msg = @_;

    open LOGFILE, ">>", $logfile or die "open failed: $!";

    flock LOGFILE, LOCK_EX;
    seek LOGFILE, 0, SEEK_END;
    print LOGFILE @msg;
    close LOGFILE;
}

logtofile "/tmp/mylog.log", "msgA\n", "msgB\n", "msgC\n";
複製程式碼

truncate截斷檔案

如果要截斷檔案為某個空間大小,直接使用truncate()函式即可(shell下也有truncate命令來截斷檔案)。

它的第一個引數是檔案控制程式碼,第二個引數是截斷後的檔案大小,單位位元組。注意,truncate是從當前指標位置開始向後截斷的,其指標前面(左邊)的資料不會動但是會計算到截斷後的大小。如果指定的截斷大小超過檔案大小,則會使用空位元組\0填充到給定大小(這個行為預設沒有定義)。

因為要截斷,這個檔案控制程式碼的模式必須是可寫的,且如果是使用">"模式,將首先被截斷為空檔案。所以,應該使用+<>>+>>這類模式。為了保證截斷效果,如果使用的是後兩種open模式,應該在每次截斷前使用"seek"將指標指到檔案的頭部。

例如,截斷檔案為100位元組大小。

open FILE, ">>", "bigfile";
seek FILE, 0, 0;
truncate FILE, 100;
close FILE;
複製程式碼

按行截斷檔案

truncate只能按位元組截斷檔案,不過有時候我們想按照行數來截斷檔案。

例如,想要保留前10行資料。實現的邏輯很簡單,先按行讀取10行(判斷行號或使用一個行號計數器),然後記錄下當前的指標位置,最後使用truncate截斷到這個位置。

#!/usr/bin/perl

use strict;
use warnings;

die "give me a file" unless @ARGV;
die "give me a line num" unless (defined($ARGV[1]) and $ARGV[1] >= 0);

my $file = $ARGV[0];
my $trunc_to = int($ARGV[1]);

# 讀取到前X行
open READ, $file or die "open failed: $!";
while(<READ>){
    last if $. == $trunc_to;
}

my $trunc_size = tell READ;
exit if $. < $trunc_to;    # total line less than $trunc_to
close READ;

# truncate
open WRITE, "+<", $file or die "open failed: $!";
truncate WRITE, $trunc_size or die "truncate failed: $!";
close WRITE;
複製程式碼

相關文章