多个文件句柄打开同一个文件 - 这是一个好习惯吗?

Multiple filehandles opening the same file - is it a good practice?

我有 grades.tsv 文件,其中包含显示学生姓名、科目和成绩的三列:

Liam    Mathematics 5
Liam    History 6
Liam    Geography   8
Liam    English 8
Aria    Mathematics 8
Aria    History 7
Aria    Geography   6
Isabella    Mathematics 9
Isabella    History 4
Isabella    Geography   7
Isabella    English 5
Isabella    Music   8

我想计算每个学生的平均成绩并将其添加到单独的列中。为此,我使用了两个文件句柄 DATA 和 OUT 打开同一个文件:

use strict;
use warnings;

# Open file with grades for calculation of average grade for each student
open (DATA,"grades.tsv") or die "Cannot open file\n";

my %grade_sums;
my %num_of_subjects;

# Calculate sum of grades and number of subjects for each student
while( <DATA> ) {

   chomp;
   my ($name, $subject, $grade) = split /\t/;

   $grade_sums{$name} += $grade;
   $num_of_subjects{$name} += 1;
}

close DATA;


# Open file with grades again but this time for a purpose of adding a separate column with average grade and printing a result
open (OUT,"grades.tsv") or die "Cannot open file\n";

while ( <OUT> ) {
   chomp;
   my ($name, $subject, $grade) = split /\t/;

   # Calculate average grade
   my $average_grade = $grade_sums{$name} / $num_of_subjects{$name};
   my $outline = join("\t", $name, $subject, $grade, $average_grade);

   # Print a file content with new column
   print "$outline\n";
}

close OUT;

该代码有效,但我不确定它是否适合此任务。这是一个好的做法还是应该首选更好的方法?

重新打开文件就可以了。一种替代方法是 seek 到文件的开头。

use Fcntl qw( SEEK_SET );

seek(DATA, 0, SEEK_SET);

查找效率更高,因为它不必检查权限等。它还保证您获得相同的文件(但不是没有人更改它)。

另一种选择是将整个文件加载到内存中。这就是我通常会做的。


注意

open(FH, $qfn) or die "Cannot open file\n";

最好写成

open(my $FH, '<', $qfn)
   or die("Can't open file \"$qfn\": $!\n");
  • 三参数open避免了一些问题。
  • 在错误消息中包含错误原因是有益的。
  • 在错误消息中包含路径是有益的。
  • 应该避免 DATA,因为 Perl 有时会自动创建一个使用该名称的句柄。
  • 应避免使用全局变量(例如 FH)或词法变量(my $FH)。

学生成绩单示例代码

#!/usr/bin/perl
#
# USAGE:
#   prog.pl
#
# Description:
#   Demonstration code for Whosebug Q59991322
#
# Whosebug: 
#   Question 59991322
#
# Author:
#   Polar Bear    https://whosebug.com/users/12313309/polar-bear
#
# Date: Tue Jan 30 13:37:00 PST 2020
#

use strict;
use warnings;
use feature 'say';

use Data::Dumper;

my $debug = 0;      # debug flag
my %hash;
my $student;
my ($subject,$mark);

map{
    chomp;
    my($name,$subject,$mark) = split "\t",$_;
    $hash{$name}{subjects}{$subject} = $mark;
    $hash{$name}{compute}{Total} += $mark;
    $hash{$name}{compute}{Num_subjects}++;
} <DATA>;

say Dumper(\%hash) if $debug;

foreach $student ( sort keys %hash ) {
    $hash{$student}{compute}{GPA} = $hash{$student}{compute}{Total}/$hash{$student}{compute}{Num_subjects};
    $~ = 'STDOUT_REPORT';
    write;
    print_marks($student);
    $~ = 'STDOUT_REPORT_END';
    write;
}

sub print_marks {
    my $student = shift;

    $~ = 'STDOUT_MARKS';

    while( ($subject,$mark) = each %{$hash{$student}{subjects}} ) {
        write;
    }
}

format STDOUT_REPORT = 
+----------------------------+
| Student: @<<<<<<<<<<       |
$student
+----------------------------+
.

format STDOUT_REPORT_END =
+----------------------------+
| Subjects taken:     @<<    |
$hash{$student}{compute}{Num_subjects}
| Grade average:      @<<    |
$hash{$student}{compute}{GPA}
+----------------------------+

.

format STDOUT_MARKS =
| @<<<<<<<<<<<<<<     @<<    |
$subject, $mark
.

__DATA__
Liam    Mathematics 5
Liam    History 6
Liam    Geography   8
Liam    English 8
Aria    Mathematics 8
Aria    History 7
Aria    Geography   6
Isabella    Mathematics 9
Isabella    History 4
Isabella    Geography   7
Isabella    English 5
Isabella    Music   8

输出

+----------------------------+
| Student: Aria              |
+----------------------------+
| Mathematics         8      |
| History             7      |
| Geography           6      |
+----------------------------+
| Subjects taken:     3      |
| Grade average:      7      |
+----------------------------+

+----------------------------+
| Student: Isabella          |
+----------------------------+
| Music               8      |
| Mathematics         9      |
| History             4      |
| English             5      |
| Geography           7      |
+----------------------------+
| Subjects taken:     5      |
| Grade average:      6.6    |
+----------------------------+

+----------------------------+
| Student: Liam              |
+----------------------------+
| Geography           8      |
| English             8      |
| History             6      |
| Mathematics         5      |
+----------------------------+
| Subjects taken:     4      |
| Grade average:      6.7    |
+----------------------------+

在这种操作中还有一件事需要考虑。如果在写入新数据时搞砸了怎么办?您将如何容忍截断原始文件但无法完全写入新数据的程序?

不要打开相同文件名的写入文件句柄,而是使用临时文件。 File::Temp 是标准库的一部分:

use File::Temp;
my( $temp_fh, $tempfile ) = tempfile();

现在,将所有内容写入 $temp_fh,直到您对能够完成输出感到满意为止。之后,使用 rename 将完成的文件移动到位:

rename $tempfile => $original;

Shawn 也正确地指出这将更改索引节点,从而破坏硬链接。如果你觉得重要,你可以将新文件复制到旧文件中,但我很少看到技术如此先进的情况:)

如果你搞砸了,原始数据还在,你可以重试。注意:这假设这两个文件位于同一分区上,因为这是 rename.

的要求

尽管这对您的情况可能无关紧要,但您还必须考虑其他消费者在您编写新文件时所做的事情。如果另一个程序想在您截断原始文件但尚未写入数据(或未完全写入)后立即读取原始文件,会发生什么?通常,您希望确保文件在可供其他程序使用之前是完整的。

如果您不喜欢临时文件,还有其他方法可以解决该问题。将原始文件移动到备份名称,然后读取并写入原始名称。或者,写入不同的文件名并将其移动到位。例如,请参阅 Perl's adjustments to the -i command-line switch 以了解此问题。