Effective Perl-chapter5

用perl处理文件非常容易,perl能借助文件句柄接口处理几乎所有形式的数据。通过文件句柄我们能完成大部分重要的任务。文件句柄还可以保存为普通的标量变量,以便将来选择要操作的对象

不要忽略文件测试操作符

所有文件测试默认情况下使用变量$_

#获取文件大小
my ($size) = (stat $filename)[7];
#对于这类的任务,perl的文件测试操作符就是专为简化常见任务而设计的
my $size = -s $filename;

复用测试结果
如果想找出所有人为当前运行程序的用户,并且权限为可执行的文件,可以在grep中联合使用多个文件测试

my @results = grep {-o and -x} glob '*';
#实际上,文件测试操作在幕后调用的是stat函数,每次运行文件测试,都会重新调用一次stat,上例中perl对$_调用了两次stat

#因此,如果对同一个文件做多次文件测试操作,可以使用虚拟文件句柄 _
my @results = grep {-o and -x _} glob '*';

栈式文件测试
从perl 5.10 开始,已经可以使用栈式文件测试了。即对同一个文件或文件句柄,可以同时进行多项属性测试

use 5.010;
if (-r -w $file) {
        print "file is readable and writeable\n";
}

#老式写法
if (-w $file and -r $file) {
        print "file is readable and writeable\n";
}

#对于上一节的例子
my @results = grep {-o -x } glob '*';

始终以三项参数的形式调用open

#读取文件
open my ($fh) ,'<',$read_file or die ... ;

#覆盖模式
open my ($fh) ,'>',$read_file or die ... ;

#追加模式
open my ($fh) ,'>>',$read_file or die ... ;

采取不同方式读取数据流

一般我们用行输入操作符<>读取数据流,如果是标量上下文,就返回一行,如果是列表上下文,就返回数据流中所有的数据
总体而言,一次读取一行的方式在时间和内存开销上效率是最高的,而while (<>)这种隐式的写法,在速度上和相对应的显式写法时一样的

open my ($fh) , '<' , $file or die;
while (<$fh>) {
        ... ;
}

#显式写法
while (defined (my $line = <$fh>)) {
        ... ;
}

也可以在foreach循环中使用类似的语法读取整个文件内容到内存

foreach (<$fh>) {
        ... ;
}

一次全部读入的方式自然要比一次一行的方式耗费内存,一次读入也有其优势

print sort <$fh>;        #打印排序后的每一行

如果需要同时访问多行内容,一次全部读入的方式就不可或却

#如找到包含apple的行时,会将该行连同相联的上下两行一同打印出来
my @f = <$fh>;
foreach (0 .. $#f) {
        if ($f[$_] =~ /\bapple\b/) {
                my $lo = ($_ > 0) ? $_ - 1 : $_;
                my $hi = ($_ < $#f) ? $_ + 1 : $_;
                print map {"$_: $f[$_]"} $lo .. $hi;
        }
}

#当然也可以使用一次一行的方式
my @f;
@f[0 .. 2] = ("\n") x 3;
for ( ; ; ) {
        @f[0 .. 2] = (@f[1,2],scalar(<$fh>) );
        last if not defined $f[1];
        if ($f[1] =~ /\bapple\b/) {
                print map {($_ + $. - 1) . " :$f[$_]" } 0 .. 2;
        }
}

文件slurp
有时候我们的需求很简单,只是想尽可能快地一次性读取所有内容,考虑将每行的分隔符都去掉后读入

my $contents = do {
        local $/;
        open my ($fh1), '<',$file1 or die;
        <$fh1>;
};

也可使用File::Slurp模块替我们完成,只需一条函数,便可把全部内容读入标量或者按行保存到数组值中

use File::Slurp;
my $text = read_file ('filename');
my @lines = read_file ('filename');

处理字符串的文件句柄

从perl 5.6开始,我们可以在字符串上打开一个文件句柄

从字符串读
对于多行字符串,不用拿正则表达式切分各行。只要在该字串标量的引用上打开一个文件句柄,然后像以往那样从该句柄读取数据即可

my $string = << 'multiline';
a
b
c
d
multiline

open my ($str_fh) , '<' ,\$string
my @results = grep /^[ae]$/, <$str_fh>;

写入字符串

my $string = q{};
open my ($str_fh), '>',\$string;
print $str_fh "this is the last line\n";

用File::Spec或Path::Class处理文件路径

用File::Spec提高可移植性
要构建新的文件路径,需要磁盘卷(有时不需要)、目录以及文件名这些元素

my $volume = 'C:';      #卷
my $file = 'perl.exe';    #文件名
#由rootdir()开头、逐个列出文件所在的目录层次,然后用catdir连接,目录间隔字符则会根据当前系统决定
use File::Spec;
my $dir = catdir (rootdir(), qw (strawberry perl bin) );  #目录名

#得到文件路径三个部分,用catpath组合起来
my $full_path = catpath ($volume,$dir,$file);

#在linux系统中catpath会忽略磁盘卷的参数,可以用undef代替
my $full_path = catpath (undef,$dir,$file);

尽可能选用Path::Class
基于File::Spec封装而来的Path::Class模块,为常见的路径操作提供了更为便捷的方法

在windows上,直接将windows下的文件路径给file函数即可,它会理解并做好一切

use Path::Class qw(file dir);
my $file = file ('C:/strawberry/perl/bin/perl.exe');
#该文件并不需要真实存在,$file对象不会对路径做任何真实性验证,它只是按照文件系统规范构造一条路径而已

如果不是在windows系统上运行程序,但还需要以windows上的路径工作,可以选用foreign_file代替

my $file = foreign_file ('Win32', 'C:/strawberry/perl/bin/perl.exe');

转换为其他系统的路径,则可以使用as_foreign方法

my unix_path = $file -> as_foreign ('Unix');

将数据留于磁盘以节约内存

现在数据集往往异常庞大,所要处理的数据总量很容易就会超过程序允许的的内存大小
有一些对策是用以减少不必要的内存开销,接下来我们逐一介绍

逐行读取文件
其实没必要一次性加载所有文件内容到内存的,我们可以将整个文件读入一个数组

open my ($fh) , '<', $file or die;
my @lines = <$fh>;  

然而,你实际上并不同时需要所有数据,那么对当前行处理即可

open my ($fh) , '<', $file or die;
while (<$fh>) {
        ... ;
}

将大的哈希表保存到DBM文件
有这样一种常见的情况,有时候我们需要根据某些关键字查找对应关联的一堆数据,而当这样的关键字不计其数时,每次查找数据都要加载内存循环一遍。因此,我们可以在查询之前,先把数据加载到DBM文件,这样所做的查询操作就从内存搬到了硬盘上,大大节约了内存

#运行起来好像数据都杂内存,但实际它们却是保存在外部的数据库文件,我们仅仅是将该文件绑定进而通过哈希的方式进行访问而已
use Fcntl;         #引入O_RDWR, O_CREAT常量
my ($lookup_file,$data_file) = @ARGV;
my $lookup = build_lookup($lookup_file);

open my ($data_fh) , '<', $data_file or die;
while (<$data_fh>) {
        chomp;
        my @row = split;
        if (exists $lookup->{ $row[0]}) {
                print "@row\n";
        }
}

sub build_lookup {
        my ($file) = @_;
        open my ($lookup_fh), '<', $file or die;
        
        require SDBM_File        #等价于use SDBM_File,不同的地方于require Package在运行时间加载
        tie (my %lookup, 'SDBM_File', 'lookup.$$', ORDWR | O_CREAT,0666) or die "can't tie SDBM file 'filename' : $! ";
        
        while ($lookup_fh) {
                chomp;
                my ($key,$value) = split;
                $lookup{$key} = $value;
        }
        return \%lookup;
}

tie()此函数把一个变量和一个类绑定在一起,而该类提供了该变量的实现。它让你创建一个看起来象普通变量的变量,但是在 变量的伪装后面,它实际上是一个羽翼丰满的 Perl 对象
你想打破变量和 类之间的关联,你可以 untie (松绑)那个变量

把文件当作数组来读取
如果觉得基于关键字查找的方式不够灵活,可以试试Tie::File模块,他将文件每一行当作数组元素来处理,但并不会全部加载到内存。我们可以在文件内采用导航处理,就好像操作普通数组一样;也可以在任何时候访问文件中的任何一行

use Tie::File;
tie my @fortunes, 'Tie::File', $fortune_file or die "unable to tie $fortune_file";

foreach (1 .. 10) {
        print $fortunes[rand @fortunes];
}

使用临时文件和临时目录
如果没有预先准备好的文件,任何时候我们都可以自己写一个临时文件应急。File::Temp模块会自动创建一个名字唯一的临时文件,并在使用之后自动清除。这种方式适合一次性使用的情况,比如对某个文件创建新的版本,可以先写一个临时文件,等全部内容更新完毕后,再重命名覆盖原来的版本

use File::Temp qw(tempfile);

my ($fh,$file_name) = tempfile();
while(<>) {
        print {$fh} uc $_;
}
$fh ->close;
rename $file_name => $final_name;

File::Temp还可以创建临时目录,存放一堆临时文件

use File::Temp qw(tempdir);
use File::Spec::Functions;
use LWP::Simple qw(getstore);

my ($temp_dir) = tempdir(CLEANUP => 1);

my %searches = (
        google => 'http://google.com/#h1',
        yahho => 'http://search.yahho.com',
        mircosoft => 'http://www.bing.com',
);

foreach my $research (keys %searches) {
        getstore ($searches{$search},
        catfile($temp_dir,$search)
        )
}

你可能感兴趣的:(Effective Perl-chapter5)