【问题标题】:PERL: Jumping to lines in a huge text filePERL:跳转到巨大文本文件中的行
【发布时间】:2018-08-12 18:57:26
【问题描述】:

我有一个非常大的文本文件(~4 GB)。 它的结构如下:

S=1
3 lines of metadata of block where S=1
a number of lines of data of this block
S=2
3 lines of metadata of block where S=2
a number of lines of data of this block
S=4
3 lines of metadata of block where S=4
a number of lines of data of this block
etc.

我正在编写一个读取另一个文件的 PERL 程序, 该文件的 foreach 行(其中必须包含一个数字), 在大文件中搜索该数字减去 1 的 S 值, 然后分析该块属于该S值的数据行。

问题是,文本文件很大,所以要处理每一行

foreach $line {...} loop

很慢。由于 S=value 是严格递增的,有什么方法可以跳转到所需 S 值的特定行吗?

【问题讨论】:

  • 改用while。如果这是不行的,请将一定大小的 MB 读入缓冲区并计算换行符以找到所需的行。
  • 每条记录的BYTES数是否相同?
  • 你需要在一个文件中找到很多这些 S 值(来分析它们的文本),还是不需要那么多?
  • 您可以使用 S-value/filepos 在该文件上创建一个索引,查找(二进制搜索)索引中的值,然后 seek() 到该文件位置。如果没有索引,您可以直接在文件中进行某种二进制搜索 - 即 seek() 到文件的一半,从该位置扫描第一个 S,并不断重复直到到达 S。这将需要多次文件读取( log n) 使用索引时只需要读入一次大文件。两种解决方案都几乎不会为大文件使用任何内存(第一个解决方案中只有索引的大小,第二个解决方案根本没有)。
  • 文件有变化吗?如果没有,请将其转换为更好的格式。

标签: perl bigdata


【解决方案1】:

有什么方法可以跳转到所需 S 值的特定行吗?

是的,如果文件没有更改,则创建索引。这需要完整读取文件一次,并使用tell 记录所有S=# 行的位置。 Store it in a DBM file 键是数字,值是文件中的字节位置。然后你可以使用seek to jump to that point in the file and read from there

但如果您要这样做,最好将数据导出到适当的数据库中,例如SQLite。编写程序将数据插入数据库并添加正常的 SQL 索引。这可能比编写索引更简单。然后您可以使用普通的 SQL 高效地查询数据,并进行复杂的查询。如果文件更改,您可以重做导出,或使用普通的insertupdate SQL 更新数据库。任何知道 SQL 的人都可以轻松使用它,而不是一堆自定义索引和搜索代码。

【讨论】:

    【解决方案2】:

    我知道操作员已经接受了答案,但对我有用的一种方法是基于更改“记录分隔符”($/) 将文件放入数组中。

    如果你做这样的事情(未经测试,但应该很接近):

    $/ = "S=";
    my @records=<fh>;
    print $records[4];
    

    输出应该是整个第五条记录(数组从 0 开始,但您的数据从 1 开始),从单独一行的记录号 (5) 开始(您可能需要稍后将其删除),紧随该记录中的所有剩余行。

    非常简单快速,虽然是记忆猪……

    【讨论】:

      【解决方案3】:

      如果文本块的长度相同(以字节或字符为单位),您可以计算所需 S 值在文件中的位置和seek 那里,然后读取。否则,原则上您需要读取行来找到 S 值。

      但是,如果只有几个 S 值可以找到,您可以估计所需的位置和seek,然后read 足以捕获 S 值。然后分析你读到的内容,看看你离你有多远,或者再次seek,或者读带有&lt;&gt;的行来得到S值。

      use warnings;
      use strict;
      use feature 'say';
      
      use Fcntl qw(:seek);
      
      my ($file, $s_target) = @ARGV;
      die "Usage: $0 filename\n" if not $file or not -f $file;
      $s_target //= 5;  #/ default, S=5
      
      open my $fh, '<', $file or die $!; 
      
      my $est_text_len = 1024;
      my $jump_by      = $est_text_len * $s_target;  # to seek forward in file
      
      my ($buff, $found);
      
      seek $fh, $jump_by, SEEK_CUR;  # get in the vicinity
      
      while (1) {
      
          my $rd = read $fh, $buff, $est_text_len;
          warn "error reading: $!" if not defined $rd;
          last if $rd == 0;
      
          while ($buff =~ /S=([0-9]+)/g) {
              my $s_val = $1;
      
              # Analyze $s_val and $buff:
              # (1) if overshot $s_target adjust $jump_by and seek back
              # (2) if in front of $s_target read with <> to get to it
              # (3) if $s_target is in $buff extract needed text
      
              if ($s_val == $s_target) {
                  say "--> Found S=$s_val at pos ", pos $buff, " in buffer";
                  seek $fh, - $est_text_len + pos($buff) + 1, SEEK_CUR;
                  while (<$fh>) {
                      last if /S=[0-9]+/;  # next block
                      print $_;
                  }
                  $found = 1;
                  last;
              }
          }   
          last if $found;
      }
      

      用您的样本进行测试,放大并清理(更改文本中的S=n,因为它与条件相同!),将$est_text_len$jump_by 设置为100 和20。

      这是一个草图。如代码中的 cmets 所述,完整的实现需要协商过度搜索和搜索不足。如果文本块大小变化不大,它可以在两次查找和读取中获得所需的 S 值前面,然后使用 &lt;&gt; 读取或使用正则表达式,如示例中所示。

      一些cmets

      • 上面勾勒的“分析”需要仔细进行。例如,一个缓冲区可能包含多个 S 值行。另外,请注意,如果 S 值不在缓冲区中,代码会继续读取。

      • 一旦你足够近并在$s_target前面阅读&lt;&gt;的行以到达它。

      • read 可能无法获得所需的数量,因此您应该真正将其放入循环中。有最近的帖子。

      • read 更改为 sysread 以提高效率。在这种情况下,请使用sysseek,并且不要与&lt;&gt;(已缓冲)混合使用。

      • 上面的代码假定要找到一个 S 值;调整更多。它绝对假定 S 值已排序。

      这显然比读取行要复杂得多,但它确实运行得更快,文件非常大,只需找到几个 S 值。如果有很多值,那么这可能无济于事。


      问题中指出的foreach (&lt;$fh&gt;) 将导致首先读取整个文件(为foreach 构建列表以通过);请改用while (&lt;$fh&gt;)


      如果文件没有改变(或者同一个文件需要多次搜索),你可以先处理一次,建立一个 S 值精确位置的索引。感谢Danny_ds 的评论。

      【讨论】:

      • +1 - 创建和使用索引(S-value/filepos)的第二个示例/答案也很棒。 (我不知道 Perl,所以我不能提供代码)。只要文件不更改,索引就可以创建一次并存储在磁盘上。而且由于 S 已经有序,因此创建该索引将很容易(只需继续添加 S-value/filepos)。然后只需对内存中较小的索引进行二分搜索。
      • @Danny_ds 确实如此。我假设每次都是(少量查询)一个新文件。添加了评论。
      【解决方案4】:

      有序列表的二分查找是一个 O(log N) 操作。像这样使用seek:

      open my $fh, '>>+', $big_file;
      $target = 123_456_789;
      
      $low = 0;
      $high = -s $big_file;
      
      while ($high - $low > 0.01 * -s $big_file) {
          $mid = ($low + $high) / 2;
          seek $fh, $mid, 0;
          while (<$fh>) {
              if (/^S=(\d+)/) {
                  if ($1 < $target) { $low = $mid; }
                  else              { $high = $mid }
                  last;
              }
          }
      }
      
      seek $fh, $low, 0;
      while (<$fh>) {
          # now you are searching through the 1% of the file that contains
          # your target S
      }
      

      【讨论】:

        【解决方案5】:

        对第二个文件中的数字进行排序。现在您可以按顺序处理大文件,根据需要处理每个 S 值。

        【讨论】:

          猜你喜欢
          • 2010-10-11
          • 2023-03-15
          • 2014-04-10
          • 1970-01-01
          • 2011-12-31
          • 2010-09-14
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多