在Perl中,我能否像使用fgets一样从文件中读取一行并限制其长度?

9
我想编写一段代码,按行读取文件并存储每行的输入数据,但要限制每行的长度。我希望防止最终用户恶意输入超过1GB的数据导致内存溢出,同时也要防止读入异常大的文件。使用$str = <FILE>仍然会读取整行,这可能非常长并导致内存溢出。
在PHP中,可以使用fgets函数指定要读取的字节数,并将一行分割成多个小块。请问Perl中是否有类似的方法?我看到了sv_gets,但不确定如何使用(虽然我只是进行了简单的谷歌搜索)。
这个练习的目标是在读取数据后避免进行额外的解析/缓冲操作。fgets在读取N个字节或遇到换行符时停止读取。
编辑:我想读取X行,每行最长为Y。我不想读取超过Z个字节的数据,也不想一次性读取所有Z个字节。我猜可以这样做并拆分行,但想知道是否还有其他方法。如果这是最好的方法,那么使用read函数并进行手动解析是最简单的选择。
谢谢。

为什么您不愿意一次性读取所有Z字节?您是否正在寻找一个名为get_n_lines_or_max_bytes(fh, n, z)的函数?这样的函数并不难编写... - geocar
我想这只是个人偏好的问题。当我可以逐步解析数据时,我讨厌吸入大量数据。此外,我也会忽略一些数据,所以为什么要一开始就占用不必要的内存呢?虽然我认为这是一种便于维护/编写的解决方案。 - NG.
@SB:测试一下吧。你会发现调用read()和split()比使用任何fgets()的实现方式更少占用内存且速度更快。 - geocar
5个回答

6

Perl没有内置的fgets函数,但是File::GetLineMaxLength可以实现它。

如果你想自己实现,可以使用getc轻松实现。

sub fgets {
    my($fh, $limit) = @_;

    my($char, $str);
    for(1..$limit) {
        my $char = getc $fh;
        last unless defined $char;
        $str .= $char;
        last if $char eq "\n";
    }

    return $str;
}

将每个字符连接到$str中是高效的,因为Perl会机会地重新分配内存。如果一个Perl字符串有16个字节,并且您连接另一个字符,则Perl将重新分配它为32个字节(32变为64,64变为128 ...)并记住长度。接下来的15个连接不需要重新分配内存或调用strlen。

1
我认为这样做很干净利落。我还看到你在另一个回答中讨论了在Perl中预分配字符串的问题。将这两者结合起来可以消除不必要的(如果有的话)常量重新分配的低效率,因为我只需要一次性分配最大长度即可。 - NG.
谢谢。我认为预分配不会带来太多好处。事实上,它可能会更慢,因为在Perl中预分配字符串可能比让Perl自己处理要慢。您还将浪费大量内存,因为每个字符串都将使用最大内存。基准测试证明了这一点。如果您真的希望这尽可能快,可以编写一个围绕fgets()的XS包装器。按XS标准来说,这相当简单。 - Schwern
我的意思是在调用 fgets 之外预分配字符串,并按引用传递给你的 fgets 进行追加。虽然不确定当我将字符串分配给另一个变量时会发生什么。我觉得最好就让它自己分配。 - NG.
@SB 我试过了,速度慢了约5%。我猜测循环内部的解引用比预分配节省的时间更耗时。像geocar那样使用$_[2]的别名也没有帮助(也不会有害)。Perl优化的经验法则是你无法用Perl来击败Perl。你可以在这里看到基准测试程序:http://gist.github.com/417919。我认为通过微调优化你不可能让它跑得更快,因为在Perl中循环遍历文件中的每个字符都需要一定的开销。 - Schwern
+1,但我不喜欢看到人们在使用解释型语言编写代码时担心速度变化5%。 - j_random_hacker
1
@j_random_hacker 嗯,其实不是5%,而是界面更差的那个并不更快。 - Schwern

4
sub heres_what_id_do($$) {
    my ($fh, $len) = @_;
    my $buf = '';

    for (my $i = 0; $i < $len; ++$i) {
        my $ch = getc $fh;
        last if !defined $ch || $ch eq "\n";
        $buf .= $ch;
    }

    return $buf;
}

虽然不是非常“Perl式”,但谁在乎呢? :) 操作系统(以及可能的Perl本身)将在下面进行所有必要的缓冲。


1
== '\n' 应该改为 eq "\n"。使用 getc 比使用 read 获取单个字符要简单得多。基准测试显示它比我的代码慢约15%。有趣的是,三个参数的 for 循环比 for my $i (0..$len-1) 要快得多,但不如 my $i; my $end = $len-1; for $i (0..$len)(这使它与我的代码相当),这表明 Perl 的 for(0..$foo) 迭代器优化很容易被击败。 - Schwern
感谢您的编辑,Schwern。虽然有些尴尬,但我之前并不知道 Perl 实际上有 getc() 函数!会进行编辑以使用它。 - j_random_hacker

3
作为一项练习,我实现了一个C语言fgets()函数的包装器。对于定义为“没有fileno”的复杂文件句柄(以覆盖绑定句柄等),它会回退到Perl实现。 File::fgets现在正在进入CPAN,您可以从存储库中获取副本。
一些基本的基准测试显示它比这里的任何实现都快10倍以上。但是,我不能说它没有错误或不会泄漏内存,我的XS技能不是很好,但它比这里的任何东西都经过了更好的测试。

1

fgets的美妙之处在于它可以读取N个数据块或在新行处停止。我认为read函数不会在新行处停止。 - NG.

-2

您可以轻松地自己实现fgets()。这里有一个像C一样工作的实现:

sub fgets{my($n,$c)=($_[1],''); ($_[0])=('');
  for(;defined($c)&&$c ne "\n"&&$n>0;$n--){$_[0].=($c=getc($_[2]));}
  defined($c)&&$_[0]; }

这是一个使用 PHP 的语义的例子:

sub fgets{my($n,$c,$x)=($_[1],'','');
  for(;defined($c)&&$c ne "\n"&&$n>0;$n--){$x.=($c=getc($_[0]));}
  ($x ne '')&&$x; }

如果您正在尝试实现资源限制(即防止不受信任的客户端耗尽所有内存),则真的不应该这样做。在调用脚本之前使用ulimit设置这些资源限制。一个好的系统管理员会设置资源限制,但他们喜欢程序员设置合理的限制启动脚本。

如果您想在将此数据代理到另一个站点之前限制输入(例如,限制SMTP输入行,因为您知道远程站点可能不支持超过511个字符),那么只需使用length()检查<INPUT>后的行长度。


无法理解代码!在检查$c是否已定义之前就进行了连接操作,因此它在eof处抛出一个警告。虽然它非常出色地模拟了C的fgets函数,但它不太符合Perlish风格。尽管它很难懂,但它并不比我的或j_random的代码更快。 - Schwern
@Schwem:如果你不介意的话,可以使用no strict - geocar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接