阅读 116

Perl IO:随机读写文件

随机读写

如果一个文件句柄是指向一个实体文件的,那么就可以对它进行随机数据的访问(包括随机读、写),随机访问表示可以读取文件中的任何一部分数据或者向文件中的任何一个位置处写入数据。实现这种随机读写的功能依赖于一个文件读写位置指针(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;
复制代码
关注下面的标签,发现更多相似文章
评论