【问题标题】:Delete a specific line from a 12GB file从 12GB 文件中删除特定行
【发布时间】:2018-10-08 14:56:21
【问题描述】:

我正在尝试从 12GB 文本文件中删除特定行。

我在 HP-UX 上没有可用的 sed -i 选项,并且其他选项(例如保存到临时文件)也不起作用,因为我只有 20 GB 可用空间,其中 12 GB 已被文本文件使用。

考虑到空间要求,我尝试使用 Perl 来完成此操作。

此解决方案可从 12 GB 的文件中删除最后 9 行。

#!/usr/bin/env perl

use strict;
use warnings;

use Tie::File;

tie my @lines, 'Tie::File', 'test.txt' or die "$!\n";
$#lines -= 9;
untie @lines;

我想修改上面的代码来删除任何特定的行号。

【问题讨论】:

  • @Inian 写一个新文件,他们没有空间。
  • perl -i 不是真的到位;它写入一个临时文件并在脚本完成后替换原始文件。
  • UNIX 通常不支持这一点——文件系统原语不允许您进行就地删除,而实际上不需要重写删除发生点之后的所有内容。 Linux 有一些新的内核级原语(只有极少数文件系统支持)来执行块的就地插入和删除,但即便如此,您的更改也需要与 4kb 页面对齐。
  • 20 美元的 16GB USB 记忆棒怎么样?
  • 是否有足够的内存 (RAM) 来读取整个内容?或者更确切地说,有多少?

标签: bash perl file unix


【解决方案1】:

Tie::File 永远不是答案。

  • 非常的慢。
  • 即使您限制其缓冲区的大小,它也会比将整个文件放入内存中消耗更多的内存。

您遇到了这两个问题。您遇到文件的每一行,因此 Tie::File 将读取整个文件并将每一行的索引存储在内存中。这在 64 位 Perl 构建中每行占用 28 个字节(不计算内存分配器中的任何开销)。


要删除文件的最后 9 行,可以使用以下命令:

use File::ReadBackwards qw( );

my $qfn = '...';

my $pos;
{
   my $bw = File::ReadBackwards->new($qfn)
      or die("Can't open \"$qfn\": $!\n");

   for (1..9) {
      defined( my $line = $bw->readline() )
         or last;
   }

   $pos = $bw->tell();
}

# Can't use $bw->get_handle because it's a read-only handle.
truncate($qfn, $pos)
   or die("Can't truncate \"$qfn\": $!\n");

要删除任意行,可以使用以下命令:

my $qfn = '...';

open(my $fh_src, '<:raw', $qfn)
   or die("Can't open \"$qfn\": $!\n");    
open(my $fh_dst, '+<:raw', $qfn)
   or die("Can't open \"$qfn\": $!\n");

while (<$fh_src>) {
   next if $. == 9;  # Or "if /keyword/", or whatever condition you want.

   print($fh_dst $_)
      or die($!);
}

truncate($fh_dst, tell($fh_dst))
   or die($!);    

以下优化版本假设只有一行(或一行)要删除:

use Fcntl qw( SEEK_CUR SEEK_SET );

use constant BLOCK_SIZE => 4*1024*1024;

my $qfn = 'file';

open(my $fh_src, '<:raw', $qfn)
   or die("Can't open \"$qfn\": $!\n");
open(my $fh_dst, '+<:raw', $qfn)
   or die("Can't open \"$qfn\": $!\n");

my $dst_pos;
while (1) {
   $dst_pos = tell($fh_src);
   defined( my $line = <$fh_src> )
      or do {
         $dst_pos = undef;
         last;
      };

   last if $. == 9;  # Or "if /keyword/", or whatever condition you want.
}

if (defined($dst_pos)) {
   # We're switching from buffered I/O to unbuffered I/O,
   # so we need to move the system file pointer from where the
   # buffered read left off to where we actually finished reading.
   sysseek($fh_src, tell($fh_src), SEEK_SET)
      or die($!);

   sysseek($fh_dst, $dst_pos, SEEK_SET)
      or die($!);

   while (1) {
      my $rv = sysread($fh_src, my $buf, BLOCK_SIZE);
      die($!) if !defined($rv);
      last if !$rv;

      my $written = 0;
      while ($written < length($buf)) {
         my $rv = syswrite($fh_dst, $buf, length($buf)-$written, $written);
         die($!) if !defined($rv);
         $written += $rv;
      }
   }

   # Must use sysseek instead of tell with sysread/syswrite.    
   truncate($fh_dst, sysseek($fh_dst, 0, SEEK_CUR))
      or die($!);
}

【讨论】:

  • "你需要文件中的行数..."该死的,你是对的。即使您在 for 循环中避免它,splice 也会调用 FETCHSIZE
  • @Schwern,只需访问一行就足以缓存其位置,并且 OP 总是需要访问每一行(作为查找要删除的行的一部分,或者作为在之后移动每一行的一部分“已删除”行)。因此,虽然避免 FETCHSIZE 可以避免您读取文件两次,但它不会为您节省任何内存。
  • 有趣的方法!我很好奇为什么最后一个版本会比第二个更快?是因为它使用缓冲区大小为 4MB 的 read 而不是我假设使用 8KB 缓冲区大小的 readline
  • @HåkonHægland、readreadline 都是缓冲 io,因此两者都应该使用相同的缓冲区。 (在较新的 Perls 中为 8 KiB。)/// 我的意思是使用 sysread,尽管您仍然应该从使用 read 中获得节省。构建的标量更少,在 C 中的时间更多/在 Perl 中的时间更少。当我可以测试时将切换到sysread
猜你喜欢
  • 1970-01-01
  • 2018-08-31
  • 2017-11-23
  • 1970-01-01
  • 2011-08-13
  • 2018-06-21
  • 2023-03-23
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多