打开文件句柄:读取数据

Perl中使用一种特殊的菱形操作符<>来读取文件句柄中的数据:<$fh>表示从文件句柄$fh中读取数据。

例如:

open my $fh, "<", "/tmp/a.log" or die "open file failed: $!";
my @lines = <$fh>;
print "$lines[0]";

上面打开文件/tmp/a.log后,使用<$fh>从文件句柄$fh中读取数据保存到数组@lines中,最后输出数组第一个元素,即输出文件中第一行数据。

<>操作符是一种按行读取的迭代器:

  • 如果是在列表上下文读取数据,则一次性读取文件中所有剩下的行,每行数据作为列表的一个元素
  • 如果是在标量上下文读取数据,则只读下一行数据
  • 如果是在空上下文读取数据,则只读下一行数据,并丢弃该行数据

例如:

open my $fh, "<", "/tmp/a.log" or die "open file failed: $!";
my $line1 = <$fh>;  # 读取第一行保存到$line1变量
my $line2 = <$fh>;  # 读取第二行保存到$line1变量
my $line3 = <$fh>;  # 读取第三行保存到$line1变量
<$fh>;              # 读取第四行数据并丢弃
my @lines = <$fh>;  # 读取第五行开始的剩余所有行,保存到数组@lines

更多时候是在while循环中按行读取数据:

while(my $line = <$fh>){...}

while的条件部分是标量上下文,因此上述代码表示:每次读取一行数据,然后执行一次循环体,直到读取完所有行。当达到文件尾部没有更多数据可读时,<>操作符返回undef,while循环条件为假,循环退出。

在此需要注意,<>操作符如果读取到一个数值0或空字符串,它赋值给变量时返回的也表示布尔假,但不会导致while循环退出。这是因为Perl会自动识别并处理while结合<>的情况,它会转变为下面这种安全的循环代码:

while(defined (my $line = <$fh>)){...}

不仅如此,while结合<>操作符时,会将每次所读取的行自动赋值给默认变量$_。但若不在while中,空上下文<>所读取的行将被丢弃:

while(<$fh>){print $_;}

不建议在for或foreach中读取文件数据。如下:

for(<$fh>){
  print $_;
}

因为for或foreach在列表上下文,它会一次性读取文件所有数据放进内存,然后迭代列表的每一个元素(即每一行数据),这意味着可能会一次性消耗很多内存来容纳文件的所有数据。而使用while来循环读取数据,则每次只消耗一行数据的内存来保存每次循环过程中所读取的那一行数据。当然,for遍历文件每一行数据在效率上要稍微高一些些,因为它是一次性读取,其缺点是可能会消耗大量内存。

读取文件内容的常规操作

读取文件内容时,通常采用如下步骤:

while(my $line = <$fh>){
  chomp $line;
  ...
}

# 更简化的代码
while(<$fh>){
  chomp;
  ...
}

# 或者读取的所有数据保存到数组时
my @lines = <$fh>;
chomp @lines;

之所以读取每一行之后要使用chomp处理一次,是因为<$fh>读取的每一行数据会保留每一行的尾部换行符。大多数情况下,尾部换行符不利于数据的处理。比如将读取的行划分为多个字段时,最后一个字段将带有换行符。再例如下面直接输出每一行:

while(<$fh>){
  say $_;
}
# 输出:
#hello world 1
#
#hello world 2
#
#hello world 3
#

输出的每一行内容后都有一个空行。

再例如,直接输出保存了每行数据的数组:

my @lines = <$fh>;
say "@lines";
# 输出:
#hello world 1
# hello world 2
# hello world 3
#

上面代码输出的每一行(除了第一行)前都有一个空格。

因此读取每一行之后的第一步通常是去除尾部换行符,并在需要的时候手动加上尾部换行符。

while(<$fh>){
  chomp;
  say;  # 或者print "$_\n";
}

区分记录和行

前文一直在说,<$fh>会按行读取每一行内容,这种说法不严谨。

严格来说,<$fh>是根据记录分隔符(由特殊变量$/控制记录分隔符)从文件中每次读取一条记录(record),每次读取文件数据时,会逐字符从前先后扫描文件的每一个字符,直到遇到$/所指定的记录分隔符才停止扫描,这部分数据(包括记录分隔符)是本次读取的一条记录。

例如,修改$/的值,使每次读取到空格为止:

open my $fh, "<", "/tmp/a.log";
$/ = " ";
my $word = <$fh>;  # 只读取一个单词

默认情况下,$/的值是换行符,即每次读取在遇到换行符时停止,因此一条记录就是一行数据,此时按记录读取和按行读取是同一个概念。

如果将$/设置为文件中不存在的字符,<$fh>在读取过程中直到文件尾部也找不到记录分隔符,这意味着会一次性读取文件的所有数据。

特殊地:

  • 如果将$/设置为undef,则一次性读取文件所有数据
  • 如果将$/设置为空字符串,则按段落读取,每次读取一段。连续空行将被压缩为单个空行并属于前一段落

例如,/tmp/a.log文件的内容如下:

$ cat /tmp/a.log
line 1
line 2


line 3

line 4

按段落读取/tmp/a.log文件:

open my $fh, "<", "/tmp/a.log" or die "open file failed: $!";
$/ = "";
while(<$fh>){
  print $_;
  say "----------";
}

直接修改$/变量会影响全局,比较安全的做法是在一个独立的作用域内临时设置$/变量(使用local修饰),这样离开作用域后会自动恢复为原来的记录分隔符:

open my $fh, "<", "/tmp/a.log" or die "open file failed: $!";
{
  local $/ = "";
  while(<$fh>){
    print $_;
    say "----------";
  }
}

值得注意的是,chomp函数也是根据$/去除字符串尾部字符的,默认情况下$/的值是换行符,因此chomp默认去除字符串尾部换行符,如果将$/设置为空格,则chomp去除的将是尾部空格。