【问题标题】:Deleting a line from a huge file in Perl从 Perl 中的大文件中删除一行
【发布时间】:2016-10-26 17:53:28
【问题描述】:

我有一个巨大的文本文件,前五行如下所示:

This is fist line
This is second line
This is third line
This is fourth line
This is fifth line

现在,我想在该文件第三行的随机位置写一些东西,它将用我正在编写的新字符串替换该行中的字符。我可以通过以下代码实现:

use strict;
use warnings;

my @pos = (0);
open my $fh, "+<", "text.txt";

while(<$fh) {
    push @pos, tell($fh);
}

seek $fh , $pos[2]+1, 0;
print $fh "HELLO";

close($fh);

但是,我无法用同样的方法弄清楚如何从该文件中删除整个第三行,以便文本如下所示:

This is fist line
This is second line
This is fourth line
This is fifth line

我不想将整个文件读入数组,也不想使用 Tie::File。是否可以使用 seek 和 tell 来满足我的要求?解决方案将非常有帮助。

【问题讨论】:

  • 为什么不想使用Tie::File?我认为这将是实现此目的的理想选择。
  • @Borodin 即使是 Tie::File 也会将文件读入一个数组,这不会消耗内存吗?在这种情况下,模块的 -memory 选项可以提供一些帮助吗?

标签: perl file seek tell


【解决方案1】:

文件是一个字节序列。我们可以替换(覆盖)其中的一些,但是我们如何删除它们呢?一旦文件被写入,它的字节就不能以任何方式从序列中“拉出”或“空白”。 (可以通过根据需要截断文件来消除文件末尾的那些。)

其余内容必须“向上”移动,以便要删除的文本后面的内容覆盖它。我们必须重写文件的其余部分。实际上,重写整个文件通常要简单得多。

作为一个非常基本的例子

use warnings 'all';
use strict;
use File::Copy qw(move);

my $file_in = '...';
my $file_out = '...';  # best use `File::Temp`

open my $fh_in,  '<', $file_in  or die "Can't open $file_in: $!";
open my $fh_out, '>', $file_out or die "Can't open $file_out: $!";

# Remove a line with $pattern
my $pattern = qr/this line goes/;

while (<$fh_in>) 
{
    print $fh_out $_  unless /$pattern/;
}
close $fh_in;
close $fh_out;

# Rename the new fie into the original one, thus replacing it
move ($file_out, $file_in) or die "Can't move $file_out to $file_in: $!";

这会将输入文件的每一行写入输出文件,除非一行匹配给定的模式。然后重命名该文件,替换原始文件(不涉及数据复制)。见this topic in perlfaq5

由于我们确实使用了一个临时文件,因此我建议使用核心模块 File::Temp


通过以更新'+&lt;' 模式打开以仅覆盖文件的一部分,这可能会更有效,但也更复杂。您迭代直到带有模式的行,记录(tell)它的位置和行长,然后复制内存中所有剩余的行。然后seek 回到减去该行长度的位置,并转储复制的文件的其余部分,覆盖该行及其后面的所有内容。

请注意,现在文件其余部分的数据被复制两次,尽管一份副本在内存中。如果要删除的行在一个非常大的文件中很远,那么解决这个问题可能是有意义的。如果有更多行要删除,这会变得更加混乱。


写出一个新文件并将其复制到原始文件上会更改文件的 inode 编号。这可能是某些工具或程序的问题,如果是,您可以改为更新原始版本

  • 写出新文件后,打开它进行读取并打开原始文件进行写入。这会破坏原始文件。然后从新文件中读取并写入原始文件,从而将内容复制回同一个inode。完成后删除新文件。

  • 以读写模式('+&lt;')打开原始文件开始。写入新文件后,seek 到原始文件的开头(或要覆盖的位置)并将新文件的内容写入其中。如果新文件较短,请记住还要设置文件结尾,

    truncate $fh, tell($fh); 
    

复制完成后。这需要一些小心,第一种方法通常可能更安全。

如果文件不是很大,则可以将新的“文件”以数组或字符串的形式“写入”内存中。

【讨论】:

  • 我的意思是我们不能用什么都覆盖那一行,这样该行就不再存在,而下一行自动出现吗?
  • This is third line\n 占用 19 个字符。您只能将其替换为其他 19 个字符。
  • @H.Burns 对,就是这样——没有“什么都没有”,只有字节,所以有一些内容。 “删除”它的唯一方法是移动其余部分。想象一排小盒子,每个里面都有一块——每个里面都必须有一些东西。文件系统中没有办法神奇地取出一个盒子。我们唯一能做的就是将下一个盒子的内容移动到我们想要“删除”的内容中,等等。最后的字节可能会被丢弃。
  • @zdim 对我来说这听起来不错,您能否演示一些将内容移动到我们要删除并丢弃末尾的代码?
  • @H.Burns,将文件视为网格纸。您无法删除文件的一部分,就像您无法从页面中删除方块一样。如果要从文件/页面中删除某些内容,则需要将每个后续字节/正方形复制到较早的字节/正方形中。唯一的例外是您可以通过播放文件的大小从文件末尾删除。
【解决方案2】:

在 Perl 的 Linux 命令行中使用 sed 命令:

my $return = `sed -i '3d' text.txt`;

其中“3d”表示删除第 3 行。

【讨论】:

  • 为什么投反对票? OP 要求一种从 perl 中的大文件中删除一行的方法。它做他想做的事。
  • 也许是因为这不完全是 Perl 解决方案,而仅仅是 sed 解决方案。另外,$return 的内容是没用的。它总是空的。 (顺便说一句,我不是反对者。)
【解决方案3】:

查看perlrun 并了解 perl 本身如何“就地”修改文件很有用。

给定:

$ cat text.txt
This is fist line
This is second line
This is third line
This is fourth line
This is fifth line

您显然可以通过使用-i-p 开关调用Perl 来“就地修改”,类似于sed:

$ perl -i -pe 's/This is third line\s*//' text.txt
$ cat text.txt
This is fist line
This is second line
This is fourth line
This is fifth line

但是,如果您参考 Perl Cookbook 配方 7.9(或查看 perlrun),您会看到:

$ perl -i -pe 's/This is third line\s*//' text.txt

相当于:

while (<>) {
    if ($ARGV ne $oldargv) {           # are we at the next file?
        rename($ARGV, $ARGV . '.bak');
        open(ARGVOUT, ">$ARGV");       # plus error check
        select(ARGVOUT);
        $oldargv = $ARGV;
    }
    s/This is third line\s*//;
}
continue{
    print;
}
select (STDOUT);                      # restore default output

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-07-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多