【问题标题】:Comparing value range in 2 columns of 2 files比较 2 个文件的 2 列中的值范围
【发布时间】:2012-01-30 13:34:18
【问题描述】:

我有 2 个大文件(制表符分隔)。

第一个文件 ->

Col1 Col2 Col3 Col4 Col5 Col6 Col7 Col8
101_#2 1 2 F0 263 278 2 1.5
102_#1 1 6 F1 766 781 1 1.0
103_#1 2 15 V1 526 581 1 0.0
103_#1 2 9 V2 124 134 1 1.3
104_#1 1 12 V3 137 172 1 1.0
105_#1 1 17 F2 766 771 1 1.0

第二个文件->

Col1 Col2 Col3 Col4
97486 9 262 279
67486 9 118 119
87486 9 183 185
248233 9 124 134

我想将文件 1 的 col5 和 col6(如范围值)与文件 2 的 col3 和 col4 进行比较。如果文件 1 的范围存在于文件 2 中,则返回该行(从文件 1)。

预期输出 ->

Col1 Col2 Col3 Col4 Col5 Col6 Col7 Col8
101_#2 1 2 F0 263 278 2 1.5
103_#1 2 9 V2 124 134 1 1.3

到目前为止我已经尝试过->

@ARGV or die "No input file specified";

open my $first, '<',$ARGV[0] or die "Unable to open input file: $!";
open my $second,'<', $ARGV[1] or die "Unable to open input file: $!";


print scalar (<$first>);

while (<$first>) {
    @cols = split /\s+/;
    $p1 = $cols[4];
    $p2 = $cols[5];

   while(<$second>) {
   @sec=split /\s+/;
   print join("\t",@cols),"\n" if ($p1>=$sec[2] && $p2<=$sec[3]);
}

}

但这仅适用于第一行。文件也很大(大约 6GB)。

我刚刚尝试了一些带有哈希的东西。

@ARGV or die "No input file specified";
open my $first, '<',$ARGV[0] or die "Unable to open input file: $!";
open my $second,'<', $ARGV[1] or die "Unable to open input file: $!";
print scalar (<$first>);
while(<$second>){
chomp;
@line=split /\s+/;
$hash{$line[2]}=$line[3];
}
while (<$first>) {
    @cols = split /\s+/;
    $p1 = $cols[4];
    $p2 = $cols[5];
foreach $key (sort keys %hash){

if ($p1>= "$key"){
if ($p2<=$hash{$key})
{
print join("\t",@cols),"\n";
}
}
else{next;}
}
}

但这也需要大量的时间和内存。任何人都可以建议我如何使用哈希使其快速。非常感谢。

【问题讨论】:

  • 包含这些范围的第一个文件也是千兆字节大小?在这种情况下,您可能希望将这些数据(其中一个文件就足够了)放入某个数据库并查询它以获得结果。
  • 我说的是总尺寸。我的第一个文件大约 3 GB,第二个文件大约 1 GB。

标签: perl


【解决方案1】:

当第二个文件已经在文件末尾时,您正试图再次读取它。要使其工作,您需要在内部 while 循环之前编写 seek $second, 0, 0

但是,这种方法会非常慢,如果您先将第二个文件中的所有范围读入内存,它会大大改善情况。这段代码就是这样做的。我建议你试试看它是否能在你的可用内存范围内工作。

use strict;
use warnings;

use List::Util;

my @ranges;

open my $fh, '<', 'f2.txt' or die $!;

while (<$fh>) {
  my ($beg, $end) = (split)[2,3];
  next if $beg =~ /\D/ or $end =~ /\D/;
  push @ranges, [$beg, $end];
}

open $fh, '<', 'f1.txt' or die $!;

while (<$fh>) {
  my ($beg, $end) = (split)[4,5];
  next if $beg =~ /\D/ or $end =~ /\D/;
  print if first { $beg >= $_->[0] and $end <= $_->[1] } @ranges;
}

【讨论】:

  • 非常感谢。它工作正常,但正如您所说,它非常慢并且占用大量内存。你能快速提出一些建议吗?可能与哈希有关(但我对它们不太熟悉)。
  • 我已经更改了我的解决方案以使用List::Util,这将加快速度,因为它是用 C 而不是 Perl 编写的,并且当它找到匹配而不是查找时也会停止扫描范围数组一路走来。如果这仍然慢得无法使用,那么您将不得不通过对其进行排序来优化范围列表,以便您可以进行二进制搜索,或者如果可能存在重叠范围,则可能组合它们。否则,您必须按照其他人的建议将数据放入数据库。
  • 第二个文件中范围上端的最大值是多少?如您的示例数据所示,它是几百个吗?
  • 不不。它可以更大。最多 8 位或 9 位数字。
【解决方案2】:

看看http://search.cpan.org/dist/Data-Range-Compare-Stream/lib/Data/Range/Compare/Stream.pod

这是一个基于您的源文件的示例。令人惊奇的是,无论源文件有多大,perl 脚本的内存都不会超过几 MB!只需确保您拥有 Data::Range::Compare::Stream 3.023 或更高版本!

注意事项:

此脚本使用磁盘合并排序对您的输入文件进行排序。对于非常大的文件,磁盘上的合并排序可能需要很长时间。您可以通过调整 Data::Range::Compare::Stream::Iterator::File::MergeSortAsc 构造函数的 bucket_size 参数来调整性能。详情请见:http://search.cpan.org/dist/Data-Range-Compare-Stream/lib/Data/Range/Compare/Stream/Iterator/File/MergeSortAsc.pod#OO_Methods

use Data::Range::Compare::Stream;
use Data::Range::Compare::Stream::Iterator::File::MergeSortAsc;
use Data::Range::Compare::Stream::Iterator::Compare::Asc;
use Data::Range::Compare::Stream::Iterator::Consolidate::OverlapAsColumn;

my $cmp=new Data::Range::Compare::Stream::Iterator::Compare::Asc;

sub parse_file_one {
  my ($line)=@_;
  my @list=split /\s+/,$line;
  return [@list[4,5],$line]
}

sub parse_file_two {
   my ($line)=@_;
   my @list=split /\s+/,$line;
   return [@list[2,3],$line]
}

sub range_to_line {
  my ($range)=@_;
  return $range->data;
}

my $file_one=new Data::Range::Compare::Stream::Iterator::File::MergeSortAsc(
  result_to_line=>\&range_to_line,
  parse_line=>\&parse_file_one,
  filename=>'custom_file_1.src',
);

my $file_two=new Data::Range::Compare::Stream::Iterator::File::MergeSortAsc(
  result_to_line=>\&range_to_line,
  parse_line=>\&parse_file_two,
  filename=>'custom_file_2.src',
);

my $set_one=new Data::Range::Compare::Stream::Iterator::Consolidate::OverlapAsColumn(
  $file_one,
  $cmp
);

my $set_two=new Data::Range::Compare::Stream::Iterator::Consolidate::OverlapAsColumn(
  $file_two,
  $cmp
);

$cmp->add_consolidator($set_one);
$cmp->add_consolidator($set_two);

while($cmp->has_next) {
  my $result=$cmp->get_next;
  next if $result->is_empty;

  my $ref=$result->get_root_results;
  next if $#{$ref->[0]}==-1;
  next if $#{$ref->[1]}==-1;

  foreach my $overlap (@{$ref->[0]}) {
    print $overlap->get_common->data;
  }

}

唯一的怪癖是输出的顺序不同:

103_#1          2       9    V2       124        134        1       1.3
101_#2          1       2    F0       263        278        2       1.5

【讨论】:

    【解决方案3】:

    这是基本的“查询优化”,从 SQL 优化器的意义上说。您有多种选择。

    一个选项是一次读取 File1 一行,并为 File1 的每一行读取 File2,打印匹配的数据。显然,这很慢。这不是最慢的方法:依次读取 File2 的每一行并扫描 File1(较大的文件)以查找匹配项。无论文件中内容的顺序如何,这种技术都有效。

    另一个不依赖于被排序的数据的选项是将较小的文件读入内存,然后一次一行地读取较大的文件,拉取匹配的数据。在最简单的形式中,您使用内存中数据的线性搜索;最好对其进行组织,以便可以更快地停止对内存中数据的搜索(可能按 Col3 值排序,其次按 Col4 值排序)。

    如果磁盘上的数据已经正确排序,那么您可以不使用内存中的某个文件,而只需对文件执行类似合并的操作。您可能希望 File1 按 Col5 的顺序排序(其次是 Col6),而 File2 将按 Col3 和 Col4 的顺序排序。这减少了内存中的数据量,代价是预先对数据进行排序。您需要仔细考虑这一点:您的目标是避免将太多数据读入内存,但由于匹配条件在范围内,您可能需要至少保留一个文件中的一些行在内存中以供重用。

    如果您有足够的内存并且数据没有预先排序,您可能会决定将两个文件都读入内存,进行适当的排序,然后执行合并选项。

    由于您是按范围排序,理论上您可能会使用 R-Tree 索引机制。然而,这对于几个文本文件来说可能是多余的,除非你经常这样做。

    最后,由于我认为这是 SQL 优化器一直在做的事情,您最好将数据加载到实际数据库中,然后运行查询:

    SELECT F1.*, F2.*
      FROM File1 AS F1 JOIN File2 AS F2
        ON F1.Col5 <= F2.Col4 AND F1.Col6 >= F2.Col3
    

    条件测试 F1.Col5 .. F1.Col6 与 F2.Col3 .. F2.Col4 重叠。它假设如果你有 [129..145] 和 [145..163],那么你需要匹配。如果不正确,请适当调整&lt;=&gt;=。请参阅How do I compare overlapping values in a row,尤其是Determine whether two date ranges overlap。尽管两者都在谈论日期和时间,但答案同样适用于数字范围(或任何其他范围)。

    在列出的选项中,具有合理性能特征的最简单的选项是第二个:

    • 将较小的文件读入内存并组织起来以便快速访问,然后一次扫描较大的文件一行。

    但是,如果存在内存限制或时间限制,阻止此工作,那么您将需要选择其他机制之一。

    【讨论】:

      【解决方案4】:

      您在阅读第一个文件的第二条记录后立即阅读整个第二个文件。变化:

      while(<$second>) {
      

      类似于:

      if (defined($_ = <$second>)) {
      

      所以你有:

      #!/usr/bin/env perl
      use strict;
      use warnings;
      my ( @cols, $p1, $p2, @sec );
      @ARGV or die "No input file specified";
      open my $first , '<',$ARGV[0] or die "Unable to open input file: $!";
      open my $second,'<', $ARGV[1] or die "Unable to open input file: $!";
      print scalar <$first>;
      <$second>; #...throw away first line...
      while (<$first>) {
          @cols = split /\s+/;
          $p1   = $cols[4];
          $p2   = $cols[5];
      
          if (defined($_ = <$second>)) {
              @sec=split /\s+/;
              print join("\t",@cols),"\n" if ($p1>=$sec[2] && $p2<=$sec[3]);
          }
      }
      

      【讨论】:

      • 感谢您的回复。但是我试过了,现在它甚至没有给出第一行。有没有办法使用while作为嵌套while并一次又一次地读取文件?
      • @Vikas:我已经编辑了我的回复以显示完整的工作版本。
      • 谢谢。我只是尝试在内部 while 循环之前(由 Borodin 编写),它工作正常。但是代码非常慢并且占用大量内存。您能快速提出一些建议吗?
      【解决方案5】:

      这似乎工作得很好(它与您的原始代码相当接近)

      @ARGV or die "No input file specified"; open my $first, '<', $ARGV[0] or die "Unable to open input file: $!"; open my $second, '<', $ARGV[1] or die "Unable to open input file: $!"; print scalar(<$first>); my $secondHeader = <$second>; while (<$first>) { @cols = split /\s+/; $p1 = $cols[4]; $p2 = $cols[5]; my $secondLine = <$second>; if ( defined $secondLine ) { @sec = split( /\s+/, $secondLine ); print join( "\t", @cols ), "\n" if ( $p1 >= $sec[2] && $p2 <= $sec[3] ); } }

      【讨论】:

        【解决方案6】:

        您通过双循环意识到您正在创建一个效率为 O2 的算法。例如,如果两个文件每个文件都包含 100 行,那么您将循环通过您的内部循环 10,000。如果两个文件都包含 1000 个项目,那么您将花费的时间不是 10 倍,而是 1000 倍。如果这些文件和您声称的一样大,那么您将等待很长时间才能完成您的程序。

        最好的办法是将数据放入 SQL 数据库(用于处理大型数据源的数据库)。

        否则,您必须以一种可以快速搜索正确范围的格式存储您的第一个文件,例如二叉树。

        根据下限将您的第一个文件存储为二叉树,但将下限和上限都存储在二叉树节点中以进行比较。

        对于第二个文件中的每一行,您将在二叉树中搜索正确的较低范围,比较较高范围,如果匹配,则您已找到您的节点。

        这对我来说太复杂了,无法快速写出算法。但是,CPAN 中有几个二叉树模块,它们应该使您的树的存储和搜索变得更加容易。不幸的是,我从来没有使用过,所以我无法推荐。但是,您可能应该找到像 Tree::AVL 这样的平衡树算法。

        使用这样的结构肯定比你的双循环更复杂,但它要快得多。用起来效率会比两个文件加起来的大一点。


        另一种可能性是将两个文件排序到两个单独的数组中。 Perl 的排序算法在 OlogO 附近,它比双循环高效得多,但不如构建二叉树高效。但是,如果这两个文件或多或少已经有序,那么它在效率上会更接近二叉树,实现起来也快很多。

        如果对两个数组都进行排序,则可以在文件#2 中依次向下,并在文件#1 中找到行。由于这两个文件都是按顺序排列的,因此在文件 #2 中搜索下一个匹配行时,您不必从文件 #1 的开头开始。

        希望这会有所帮助。抱歉没有编码示例。

        【讨论】:

        • 非常感谢您的回复。问题是我想在 perl 中完成这项任务,因为我想学习它。我也在尝试不同的方法,但为了更高效,我需要 stackoverflow 上的冠军的帮助。
        • @Vikas - 如果您是 Perl 新手,我会先对两个数组进行排序,然后一起遍历它们。效率不高,但肯定比双循环效率高,而且语法对于初学者来说也更容易理解。
        【解决方案7】:

        我发现另一个大大加快速度的解决方案是使用子例程: 假设您正在比较两个文件的第一列和第二列,这是我的意图。首先,您需要按第一列,然后是第二列对两个文件进行排序。然后,您将第一个文件范围读入一个数组并调用子例程在第二个文件中进行匹配,并在匹配时将匹配的行写入文件被发现。在子例程中,您还保存了找到最后一个匹配项的行号,以便 perl 直接进入该行而不会延迟!请注意,我从第二个文件的第一行开始。

        use warnings; use strict; open my $first, '<', "first_file.txt" or die$!; open my $second, '<', "second_file" or die$!; open output, ">output.txt" or die$!; my $line_number=1; foreach (<$first>) { my @cols=(); chomp $_; my @cols = split( /\s+/, $_ ); my $p1 = $cols[0]; my $p2 = $cols[1]; match($p1,$p2,$line_number); } sub match{ while (<$second>) { next if ($. < $line_number); chomp $_; my @list = @_; my $p1=(@list[0]); my $p2=(@list[1]); my $line_number=(@list[2]); my @sec = split( /\s+/, $_ ); if ( $p1 == $sec[0] && $p2 == $sec[1] ) { print output2 $_."\n"; return $line_number; next;} } }

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2018-02-12
          • 1970-01-01
          • 1970-01-01
          • 2019-09-08
          • 1970-01-01
          • 1970-01-01
          • 2013-02-22
          相关资源
          最近更新 更多