多个文件句柄打开同一个文件 - 这是一个好习惯吗?
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 以了解此问题。
我有 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 以了解此问题。