比较两列2个文件中的值范围

时间:2012-01-30 13:34:18

标签: perl

我有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(如范围值)与file2的col3和col4进行比较。如果文件2中存在文件1的范围,则返回该行(来自file1)。

预期输出 - >

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;}
}
}

但是这也需要花费大量的时间和记忆。任何人都可以建议我如何使用哈希来快速制作它。非常感谢。

7 个答案:

答案 0 :(得分:1)

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

然而,这种方法会非常慢,如果你要将第二个文件中的所有范围首先读入内存,它会大大改善。这段代码就是这样。我建议你试试它是否能在你的可用记忆中运作。

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;
}

答案 1 :(得分:1)

查看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

答案 2 :(得分:0)

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

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]);
    }
}

答案 3 :(得分:0)

这是SQL优化器执行的基本“查询优化”。你有多种选择。

一种选择是一次读取一行文件,并通过File2读取File1的每一行,打印匹配的数据。显然,这很慢。它不是最慢的方式:依次读取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],那么你需要匹配。如果这不正确,请适当调整<=>=。请参阅How do I compare overlapping values in a row,尤其是Determine whether two date ranges overlap。虽然两者都在讨论日期和时间,但答案也适用于数值范围(或任何其他范围)。

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

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

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

答案 4 :(得分:0)

这似乎工作得很好(并且它与原始代码非常接近)

@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] ); } }

答案 5 :(得分:0)

你用双循环意识到你正在创建一个效率为O 2 的算法。例如,如果两个文件的每个文件都包含100行,那么您将循环遍历内部循环10,000。如果两个文件都包含1000个项目,则不会超过10倍,而是会长1000倍。如果这些文件与您声称的一样大,那么您将需要等待很长时间才能完成程序。

最好的办法是将您的数据放入SQL数据库(这是为处理大型数据源而设计的)。

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

根据低范围将第一个文件存储为二叉树,但将低范围和高范围存储在二叉树节点中进行比较。

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

这对我来说太复杂了,无法写出快速算法。但是,CPAN中有几个二叉树模块,可以更容易地存储和搜索树。不幸的是,我从未使用过,所以我不能提出建议。但是,您应该找到一个平衡的树算法,如Tree::AVL

使用这样的结构肯定比双循环更复杂,但它更快,更快。效率将略高于两个文件的组合大小。


另一种可能性是将两个文件分成两个单独的数组。 Perl的排序算法在OlogO周围,它比双循环更有效,但不如构建二叉树有效。但是,如果这两个文件或多或少已经按顺序排列,那么它的效率将更接近二叉树,并且实现起来要快得多。

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

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

答案 6 :(得分:0)

我发现的另一个解决方案可以大大提高速度,使用子程序: 假设您正在比较两个文件的第一列和第二列,这是我的意图。首先,您需要按第一列和第二列对两个文件进行排序。然后将第一个文件范围读入数组并调用子例程以在第二个文件中进行匹配,并在匹配时将匹配行写入文件找到了。在子例程中,您还保存了找到最后一个匹配的行号,以便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;} } }