Perl / Linux使用另一个文件的内容过滤大文件

时间:2016-11-08 20:39:12

标签: linux perl awk

我正在使用另一个较小文件的内容过滤580 MB文件。 File1(较小的文件)

chr start  End
1    123   150
2    245   320
2    450   600

File2(大文件)

chr pos RS ID A B C D E F
1   124 r2 3  s 4 s 2 s 2
1   165 r6 4  t 2 k 1 r 2
2   455 t2 4  2 4 t 3 w 3
3   234 r4 2  5 w 4 t 2 4

如果满足以下条件,我想从File2捕获行。 File2.Chr == File1.Chr && File2.Pos > File1.Start && File2.Pos < File1.End 我尝试过使用awk,但运行速度很慢,我也想知道是否有更好的方法来实现同样的目标?

谢谢。

以下是我正在使用的代码:

#!/usr/bin/perl -w
use strict;
use warnings;

my $bed_file = "/data/1000G/Hotspots.bed";#File1 smaller file
my $SNP_file = "/data/1000G/SNP_file.txt";#File2 larger file
my $final_file = "/data/1000G/final_file.txt"; #final output file

open my $in_fh, '<', $bed_file
        or die qq{Unable to open "$bed_file" for input: $!};

    while ( <$in_fh> ) {

     my $line_str = $_;

     my @data = split(/\t/, $line_str);

     next if /\b(?:track)\b/;# skip header line
     my $chr = $data[0]; $chr =~ s/chr//g; print "chr is $chr\n";
     my $start = $data[1]-1; print "start is $start\n";
     my $end = $data[2]+1; print "end is $end\n";

     my $cmd1 = "awk '{if(\$1==chr && \$2>$start && \$2</$end) print (\"chr\"\$1\"_\"\$2\"_\"\$3\"_\"\$4\"_\"\$5\"_\"\$6\"_\"\$7\"_\"\$8)}' $SNP_file >> $final_file"; print "cmd1\n";
     my $cmd2 = `awk '{if(\$1==chr && \$2>$start && \$2</$end) print (\"chr\"\$1\"_\"\$2\"_\"\$3\"_\"\$4\"_\"\$5\"_\"\$6\"_\"\$7\"_\"\$8)}' $SNP_file >> $final_file`; print "cmd2\n";

}

5 个答案:

答案 0 :(得分:2)

将小文件读入数据结构并检查其他文件的每一行。

这里我把它读成一个数组,每个元素都是一个带有一行字段的arrayref。然后根据此数组中的arrayrefs检查数据文件的每一行,比较每个要求的字段。

use warnings 'all';
use strict;

my $ref_file = 'reference.txt';
open my $fh, '<', $ref_file or die "Can't open $ref_file: $!";
my @ref = map { chomp; [ split ] } grep { /\S/ } <$fh>;

my $data_file = 'data.txt';
open $fh, '<', $data_file or die "Can't open $data_file: $!";

# Drop header lines
my $ref_header  = shift @ref;    
my $data_header = <$fh>;

while (<$fh>) 
{
    next if not /\S/;  # skip empty lines
    my @line = split;

    foreach my $refline (@ref) 
    {
        next if $line[0] != $refline->[0];
        if ($line[1] > $refline->[1] and $line[1] < $refline->[2]) {
            print "@line\n";
        }
    }   
}
close $fh;

这将从提供​​的样本中打印出正确的行。它允许多行匹配。如果这不可能,请在last块中添加if,以便在找到匹配后退出foreach

对代码的一些评论。如果有更多内容可以使用,请告诉我。

在阅读参考文件时,<$fh>在列表上下文中使用,因此它返回所有行,grep过滤掉空行。 map首先chomp换行,然后按[ ]生成一个arrayref,其中的元素是split获得的行上的字段。输出列表分配给@ref

当我们重复使用$fh时,它首先关闭(如果它已打开),因此不需要close

我存储标题行,可能是打印或检查。我们真的只需要排除它们。

答案 1 :(得分:1)

另一种方式,这次将较小的文件存储在一个Hash of Arrays(HoA)中,基于&#39; chr&#39;字段:

use strict;
use warnings;

my $small_file = 'small.txt';
my $large_file = 'large.txt';

open my $small_fh, '<', $small_file or die $!;

my %small;

while (<$small_fh>){
    next if $. == 1;
    my ($chr, $start, $end) = split /\s+/, $_;
    push @{ $small{$chr} }, [$start, $end];
}

close $small_fh;

open my $large_fh, '<', $large_file or die $!;

while (my $line = <$large_fh>){
    my ($chr, $pos) = (split /\s+/, $line)[0, 1];

    if (defined $small{$chr}){
        for (@{ $small{$chr} }){
            if ($pos > $_->[0] && $pos < $_->[1]){
                print $line;
            }
        }
    }
}

答案 2 :(得分:1)

将它们放入SQLite数据库,进行连接。与试图自己编写内容相比,这将更快,更少的错误和使用更少的内存。而且它更灵活,现在您只需对数据进行SQL查询,您就不必继续编写新脚本并重新编写文件。

您可以通过解析和插入自己来导入它们,也可以将它们转换为CSV并使用SQLite's CSV import ability。使用该简单数据转换为CSV可以像s{ +}{,}g一样简单,也可以使用完整的Text::CSV_XS

您的表格看起来像这样(您希望为表格和字段使用更好的名称)。

create table file1 (
    chr integer not null,
    start integer not null,
    end integer not null
);

create table file2 (
    chr integer not null,
    pos integer not null,
    rs integer not null,
    id integer not null,
    a char not null,
    b char not null,
    c char not null,
    d char not null,
    e char not null,
    f char not null
); 

在您要搜索的列上创建一些索引。索引会降低导入速度,因此请确保在导入后执行此操作。

create index chr_file1 on file1 (chr);
create index chr_file2 on file2 (chr);
create index pos_file2 on file2 (pos);
create index start_file1 on file1 (start);
create index end_file1 on file1 (end);

加入。

select *
from file2
join file1 on file1.chr == file2.chr
where file2.pos between file1.start and file1.end;

1,124,r2,3,s,4,s,2,s,2,1,123,150
2,455,t2,4,2,4,t,3,w,3,2,450,600

您可以通过DBIDBD::SQLite驱动程序在Perl中执行此操作。

答案 3 :(得分:0)

如前所述,在每次迭代时调用awk非常慢。一个完整的awk解决方案是可能的,我刚看到一个Perl解决方案,这是我的Python解决方案,因为OP不会介意:

  • 从小文件创建字典:chr =&gt;情侣名单开始/结束
  • 遍历大文件并尝试匹配chr&amp;其中一个开始/结束元组之间的位置。

代码:

with open("smallfile.txt") as f:
    next(f) # skip title
    # build a dictionary with chr as key, and list of start,end as values
    d = collections.defaultdict(list)
    for line in f:
        toks = line.split()
        if len(toks)==3:
            d[int(toks[0])].append((int(toks[1]),int(toks[2])))


with open("largefile.txt") as f:
    next(f) # skip title
    for line in f:
        toks = line.split()
        chr_tok = int(toks[0])
        if chr_tok in d:
            # key is in dictionary
            pos = int(toks[1])
            if any(lambda x : t[0]<pos<t[1] for t in d[chr_tok]):
                print(line.strip())

通过对元组列表和appyling bisect进行排序以避免线性搜索,我们可以稍快一些。只有在&#34;小&#34;中的元组列表很大时,这才是必要的。文件。

答案 4 :(得分:0)

单次通过awk功率。你的代码迭代file2的次数与file1中的行一样多次,因此执行时间呈线性增长。如果此单通道解决方案比其他解决方案慢,请告诉我。

awk 'NR==FNR {
    i = b[$1];        # get the next index for the chr
    a[$1][i][0] = $2; # store start
    a[$1][i][1] = $3; # store end
    b[$1]++;          # increment the next index
    next;
}

{
    p = 0;
    if ($1 in a) {
        for (i in a[$1]) {
            if ($2 > a[$1][i][0] && \
                $2 < a[$1][i][1])
                p = 1                 # set p if $2 in range
        }
    }
}

p {print}'

<强>单行

awk 'NR==FNR {i = b[$1];a[$1][i][0] = $2; a[$1][i][1] = $3; b[$1]++;next; }{p = 0;if ($1 in a){for(i in a[$1]){if($2>a[$1][i][0] && $2<a[$1][i][1])p=1}}}p' file1 file2