【问题标题】:Multiple filehandles opening the same file - is it a good practice?打开同一个文件的多个文件句柄 - 这是一个好习惯吗?
【发布时间】:2020-01-30 17:21:50
【问题描述】:

我有 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;

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

【问题讨论】:

  • 你不应该使用名称DATA,它是special package handle
  • 你应该使用三参数 open() 和词法文件句柄:open my $data, '&lt;', 'grades.tsv' or die ...; while (my $line = &lt;$data&gt;) { ... }
  • 我能看到的唯一危险——如果你的代码不能正常工作,你会覆盖原始文件(丢失它的内容,保留备份副本)。为什么不为每个学生创建一个带有摘要的新文件? student number_of_subjects_taken average_mark
  • 我还要提一下 split \t 和 join \t 不足以处理 TSV 文件。见thomasburette.com/blog/2014/05/25/…。考虑Text::CSV

标签: perl filehandle


【解决方案1】:

重新打开文件就好了。一种替代方法是将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)。

【讨论】:

    【解决方案2】:

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

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

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

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

    rename $tempfile => $original;
    

    Shawn 还正确地指出,这会改变 inode,从而破坏硬链接。如果这对您很重要,您可以将新文件复制到旧文件中,但我很少看到技术如此先进的情况:)

    如果你搞砸了,原来的数据还在,你可以再试一次。注意:这假设这两个文件在同一个分区上,因为这是rename 的要求。

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

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

    【讨论】:

    【解决方案3】:

    学生成绩单示例代码

    #!/usr/bin/perl
    #
    # USAGE:
    #   prog.pl
    #
    # Description:
    #   Demonstration code for StackOverflow Q59991322
    #
    # StackOverflow: 
    #   Question 59991322
    #
    # Author:
    #   Polar Bear    https://stackoverflow.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    |
    +----------------------------+
    

    【讨论】:

    • 提示:Perl6::Form(Perl5 的一个模块)提供的表单比内置的要干净得多
    • 我不太喜欢在 void 上下文中使用 map。使用for 会更清晰。
    • 我仍然对格式情有独钟。如此之多,以至于我有权从原始 Learning Perl 书中发布 Formats chapter
    • @brian d foy,Perl6::Form 基本相同,没有奇怪的全局变量。
    • 我不重视为了更多不必要的依赖而牺牲某种纯度。人们可以根据自己的情况做出自己的价值判断。
    猜你喜欢
    • 2011-07-17
    • 2019-11-07
    • 1970-01-01
    • 2016-07-26
    • 2010-09-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-01-06
    相关资源
    最近更新 更多