内容

名称

perlopentut - Perl 中打开文件和管道简单示例

描述

在 Perl 中,每次对文件进行 I/O 操作时,都是通过 Perl 中称为 文件句柄 的东西进行的。文件句柄是外部文件的内部名称。open 函数的作用是在内部名称和外部名称之间建立关联,而 close 函数的作用是断开这种关联。

为了方便起见,Perl 在您运行时会设置一些已经打开的特殊文件句柄。这些包括STDINSTDOUTSTDERRARGV。由于这些文件句柄是预先打开的,您可以直接使用它们,而无需费心自己打开它们。

print STDERR "This is a debugging message.\n";

print STDOUT "Please enter something: ";
$response = <STDIN> // die "how come no input?";
print STDOUT "Thank you!\n";

while (<ARGV>) { ... }

正如您从这些示例中看到的,STDOUTSTDERR 是输出句柄,而 STDINARGV 是输入句柄。它们全部使用大写字母,因为它们是 Perl 保留的,就像@ARGV 数组和 %ENV 哈希一样。它们的外部关联是由您的 shell 设置的。

您需要自己打开所有其他文件句柄。虽然有很多变体,但调用 Perl 的 open() 函数最常见的方式是使用三个参数和一个返回值。

OK = open(HANDLE, MODE, PATHNAME)

其中

OK

如果打开成功,将是某个定义的值,但如果失败,则为undef

HANDLE

应该是一个未定义的标量变量,如果打开成功,将由open 函数填充;

MODE

是打开文件的访问模式和编码格式;

PATHNAME

是要打开的文件的外部名称。

open 函数的大部分复杂性在于 MODE 参数可以取的许多可能值。

在我们向您展示如何打开文件之前,还有一件事:打开文件通常不会在 Perl 中自动锁定它们。有关如何锁定的信息,请参阅 perlfaq5

打开文本文件

以只读模式打开文本文件

如果您想从文本文件读取数据,请先以只读模式打开它,如下所示

my $filename = "/some/path/to/a/textfile/goes/here";
my $encoding = ":encoding(UTF-8)";
my $handle   = undef;     # this will be filled in on success

open($handle, "< $encoding", $filename)
    || die "$0: can't open $filename for reading: $!";

与 shell 一样,在 Perl 中,"<" 用于以只读模式打开文件。如果成功,Perl 会为您分配一个全新的文件句柄,并用对该句柄的引用填充您之前未定义的 $handle 参数。

现在,您可以在该句柄上使用诸如 readlinereadgetcsysread 之类的函数。可能最常见的输入函数是看起来像运算符的函数

$line = readline($handle);
$line = <$handle>;          # same thing

因为readline函数在文件末尾或出现错误时返回undef,所以你经常会看到它被这样使用

$line = <$handle>;
if (defined $line) {
    # do something with $line
}
else {
    # $line is not valid, so skip it
}

你也可以用这种方式在未定义的值上快速die

$line = <$handle> // die "no input found";

然而,如果遇到 EOF 是一个预期且正常的事件,你不想仅仅因为输入耗尽就退出。相反,你可能只想退出一个输入循环。然后,你可以测试是否实际错误导致循环终止,并采取相应的措施

while (<$handle>) {
    # do something with data in $_
}
if ($!) {
    die "unexpected error while reading from $filename: $!";
}

关于编码的说明:每次都必须指定文本编码可能有点麻烦。要为open设置默认编码,以便你无需每次都提供它,可以使用open pragma

use open qw< :encoding(UTF-8) >;

完成此操作后,你可以安全地省略open模式的编码部分

open($handle, "<", $filename)
    || die "$0: can't open $filename for reading: $!";

但永远不要在没有先设置默认编码的情况下使用裸"<"。否则,Perl 将无法知道你拥有哪种文本文件,Perl 将无法知道如何将文件中的数据正确映射到它可以处理的实际字符。其他常见的编码格式包括"ASCII""ISO-8859-1""ISO-8859-15""Windows-1252""MacRoman",甚至"UTF-16LE"。有关编码的更多信息,请参阅perlunitut

打开文本文件以供写入

当你想要写入文件时,首先要决定对该文件中的任何现有内容执行什么操作。你在这里有两个基本选择:保留或覆盖。

如果你想保留任何现有内容,那么你想要以追加模式打开文件。与 shell 中一样,在 Perl 中,你使用">>"以追加模式打开现有文件。">>"如果文件不存在,则创建该文件。

my $handle   = undef;
my $filename = "/some/path/to/a/textfile/goes/here";
my $encoding = ":encoding(UTF-8)";

open($handle, ">> $encoding", $filename)
    || die "$0: can't open $filename for appending: $!";

现在,你可以使用任何printprintfsaywritesyswrite来写入该文件句柄。

如上所述,如果文件不存在,则追加模式打开将为你创建它。但如果文件已经存在,则其内容将不会受到损害,因为你将在旧文本的末尾添加新文本。

另一方面,有时你想要覆盖可能已经存在的内容。要清空文件,然后开始写入它,你可以以只写模式打开它

my $handle   = undef;
my $filename = "/some/path/to/a/textfile/goes/here";
my $encoding = ":encoding(UTF-8)";

open($handle, "> $encoding", $filename)
    || die "$0: can't open $filename in write-open mode: $!";

再次,Perl 的工作方式与 shell 相似,">" 会覆盖现有文件。

与追加模式一样,当您以只写模式打开文件时,现在可以使用 printprintfsaywritesyswrite 中的任何一个来写入该文件句柄。

那么读写模式呢?您可能应该假装它不存在,因为以读写模式打开文本文件不太可能按您的预期工作。有关详细信息,请参阅 perlfaq5

打开二进制文件

如果要打开的文件包含二进制数据而不是文本字符,则 openMODE 参数略有不同。您不是指定编码,而是告诉 Perl 您的数据是原始字节。

my $filename = "/some/path/to/a/binary/file/goes/here";
my $encoding = ":raw :bytes"
my $handle   = undef;     # this will be filled in on success

然后像以前一样打开,根据需要选择 "<"">>"">"

open($handle, "< $encoding", $filename)
    || die "$0: can't open $filename for reading: $!";

open($handle, ">> $encoding", $filename)
    || die "$0: can't open $filename for appending: $!";

open($handle, "> $encoding", $filename)
    || die "$0: can't open $filename in write-open mode: $!";

或者,您可以通过以下方式更改现有句柄的二进制模式

binmode($handle)    || die "cannot binmode handle";

这对于 Perl 已为您打开的句柄特别有用。

binmode(STDIN)      || die "cannot binmode STDIN";
binmode(STDOUT)     || die "cannot binmode STDOUT";

您还可以将显式编码传递给 binmode 以动态更改它。这并不完全是“二进制”模式,但我们仍然使用 binmode 来执行此操作

binmode(STDIN,  ":encoding(MacRoman)") || die "cannot binmode STDIN";
binmode(STDOUT, ":encoding(UTF-8)")    || die "cannot binmode STDOUT";

一旦您以正确的模式正确打开了二进制文件,就可以使用与在文本文件上使用相同的 Perl I/O 函数。但是,您可能希望使用固定大小的 read 而不是可变大小的 readline 来进行输入。

以下是如何复制二进制文件的示例

my $BUFSIZ   = 64 * (2 ** 10);
my $name_in  = "/some/input/file";
my $name_out = "/some/output/flie";

my($in_fh, $out_fh, $buffer);

open($in_fh,  "<", $name_in)
    || die "$0: cannot open $name_in for reading: $!";
open($out_fh, ">", $name_out)
    || die "$0: cannot open $name_out for writing: $!";

for my $fh ($in_fh, $out_fh)  {
    binmode($fh)               || die "binmode failed";
}

while (read($in_fh, $buffer, $BUFSIZ)) {
    unless (print $out_fh $buffer) {
        die "couldn't write to $name_out: $!";
    }
}

close($in_fh)       || die "couldn't close $name_in: $!";
close($out_fh)      || die "couldn't close $name_out: $!";

打开管道

Perl 还允许您将文件句柄打开到外部程序或 shell 命令中,而不是打开到文件中。您可以这样做,以便将数据从您的 Perl 程序传递到外部命令以进行进一步处理,或者从另一个程序接收数据以供您的 Perl 程序处理。

指向命令的文件句柄也称为管道,因为它们的工作原理与 Unix 管道类似的进程间通信原理。这样的文件句柄在其外部端有一个活动程序,而不是一个静态文件,但在其他所有方面,它就像一个更典型的基于文件的句柄,本文前面讨论的所有技术都同样适用。

因此,您可以使用与打开文件相同的 open 调用打开管道,将第二个 (MODE) 参数设置为特殊字符,指示输入管道或输出管道。对于允许您的 Perl 程序从外部程序读取数据的文件句柄,使用 "-|",对于向该程序发送数据的文件句柄,使用 "|-"

打开用于读取的管道

假设您希望您的 Perl 程序处理存储在名为 unsorted 的附近目录中的数据,该目录包含多个文本文件。您还希望您的程序在开始处理这些文件之前,将所有这些文件的内容排序到一个唯一的行列表中,并按字母顺序排序。

您可以通过打开一个普通的文件句柄到每个文件,逐步构建一个包含所有加载文件内容的内存数组,最后在没有更多文件要加载时对该数组进行排序和过滤来实现这一点。或者,您可以通过直接打开一个管道到操作系统的sort命令的输出,将所有合并和排序工作卸载到操作系统本身,从而更快地完成工作。

以下是可能的样子

open(my $sort_fh, '-|', 'sort -u unsorted/*.txt')
    or die "Couldn't open a pipe into sort: $!";

# And right away, we can start reading sorted lines:
while (my $line = <$sort_fh>) {
    #
    # ... Do something interesting with each $line here ...
    #
}

open的第二个参数"-|",使其成为一个指向另一个程序的读管道,而不是一个指向文件的普通文件句柄。

请注意,open的第三个参数是一个字符串,包含程序名称(sort)及其所有参数:在本例中,-u用于指定唯一排序,然后是一个文件通配符,指定要排序的文件。生成的句柄$sort_fh就像一个只读("<")文件句柄,您的程序随后可以像打开一个普通的单个文件一样从它读取数据。

打开一个用于写入的管道

继续前面的例子,假设您的程序已经完成了处理,结果存储在一个名为@processed的数组中。您希望将这些行打印到一个名为numbered.txt的文件中,并带有一个格式整齐的列号。

当然,您可以编写自己的代码来完成这项工作——或者,再一次,您可以将这项工作交给另一个程序。在本例中,cat,运行时使用它自己的-n选项来激活行号,应该可以解决问题。

open(my $cat_fh, '|-', 'cat -n > numbered.txt')
    or die "Couldn't open a pipe into cat: $!";

for my $line (@processed) {
    print $cat_fh $line;
}

在这里,我们使用open的第二个参数"|-",表示分配给$cat_fh的文件句柄应该是一个写管道。然后,我们可以像使用一个只写普通文件句柄一样使用它,包括print数据的基本功能。

请注意,第三个参数指定了我们希望管道到的命令,它设置了cat,通过">"符号将它的输出重定向到文件numbered.txt中。这可能看起来有点棘手,因为如果在open的第二个参数中显示它,同一个符号将具有完全不同的含义!但是在这里,在第三个参数中,它只是 Perl 将打开管道的 shell 命令的一部分,Perl 本身不会赋予它任何特殊的含义。

将命令表示为列表

对于打开管道,Perl 提供了使用一个列表调用open的选项,该列表包含所需的命令及其所有参数作为单独的元素,而不是像上面的例子那样将它们组合成一个字符串。例如,我们可以像这样表达第一个例子中的open调用

open(my $sort_fh, '-|', 'sort', '-u', glob('unsorted/*.txt'))
    or die "Couldn't open a pipe into sort: $!";

当您以这种方式调用open时,Perl 会直接调用给定的命令,绕过 shell。因此,shell 不会尝试解释命令参数列表中的任何特殊字符,这些字符可能会产生意外的影响。这可以使open调用更安全、更不容易出错,在将变量作为参数传递或只是引用包含空格的文件名等情况下很有用。

但是,当您确实想要将有意义的元字符传递给 shell 时,例如在最后的 unsorted/*.txt 参数中的 "*",您不能使用这种替代语法。在这种情况下,我们通过 Perl 方便的 glob 内置函数解决了这个问题,该函数将它的参数评估为一个文件名列表——我们可以安全地将这个结果列表直接传递给 open,如上所示。

还要注意,像这样以列表形式表示管道命令参数并非在所有平台上都适用。它将在任何提供真实 fork 函数的基于 Unix 的操作系统(例如 macOS 或 Linux)上运行,以及在运行 Perl 5.22 或更高版本的 Windows 上运行。

另请参阅

有关 open 的完整文档提供了对该函数的全面参考,超出了此处介绍的最佳实践基础。

作者和版权

版权 2013 Tom Christiansen;现在由 Perl5 Porters 维护

本文档是免费的;您可以在与 Perl 本身相同的条款下重新分发和/或修改它。