在perl中解析一个巨大的文本文件

时间:2011-11-15 15:03:14

标签: perl

我有一个标签分隔的文本文件。它们可以达到1 GB。我将根据其中的样本数量具有可变数量的列。每个样本有八列。例如,sampleA:ID1,id2,MIN_A,AVG_A,MAX_A,AR1_A,AR2_A,AR_A,AR_5。其中ID1和id2是所有样本的共同点。我想要实现的是根据样本数将整个文件拆分成文件块。

ID1,ID2,MIN_A,AVG_A,MAX_A,AR1_A,AR2_A,AR3_A,AR4_A,AR5_A,MIN_B, AVG_B, MAX_B,AR1_B,AR2_B,AR3_B,AR4_B,AR5_B,MIN_C,AVG_C,MAX_C,AR1_C,AR2_C,AR3_C,AR4_C,AR5_C
12,134,3535,4545,5656,5656,7675,67567,57758,875,8678,578,57856785,85587,574,56745,567356,675489,573586,5867,576384,75486,587345,34573,45485,5447
454385,3457,485784,5673489,5658,567845,575867,45785,7568,43853,457328,3457385,567438,5678934,56845,567348,58567,548948,58649,5839,546847,458274,758345,4572384,4758475,47487

这是我的模型文件的外观,我希望将它们作为:

File A : 
ID1,ID2,MIN_A,AVG_A,MAX_A,AR1_A,AR2_A,AR3_A,AR4_A,AR5_A
12,134,3535,4545,5656,5656,7675,67567,57758,875
454385,3457,485784,5673489,5658,567845,575867,45785,7568,43853

File B:
ID1, ID2,MIN_B, AVG_B, MAX_B,AR1_B,AR2_B,AR3_B,AR4_B,AR5_B
12,134,8678,578,57856785,85587,574,56745,567356,675489
454385,3457,457328,3457385,567438,5678934,56845,567348,58567,548948

File C:

ID1, ID2,MIN_C,AVG_C,MAX_C,AR1_C,AR2_C,AR3_C,AR4_C,AR5_C
12,134,573586,5867,576384,75486,587345,34573,45485,5447
454385,3457,58649,5839,546847,458274,758345,4572384,4758475,47487.

有没有简单的方法可以做到这一点,而不是通过一个阵列?

我如何计算出我的逻辑是计算(标题数 - 2)并将它们除以8将得到文件中的样本数。然后遍历数组中的每个元素并解析它们。这样做似乎是一种乏味的方式。我很乐意知道任何更简单的处理方法。

由于 SIPRA

4 个答案:

答案 0 :(得分:8)

#!/bin/env perl

use strict;
use warnings;

# open three output filehandles
my %fh;
for (qw[A B C]) {
  open $fh{$_}, '>', "file$_" or die $!;
}

# open input
open my $in, '<', 'somefile' or die $!;

# read the header line. there are no doubt ways to parse this to
# work out what the rest of the program should do.
<$in>;

while (<$in>) {
  chomp;
  my @data = split /,/;

  print $fh{A} join(',', @data[0 .. 9]), "\n";
  print $fh{B} join(',', @data[0, 1, 10 .. 17]), "\n";
  print $fh{C} join(',', @data[0, 1, 18 .. $#data]), "\n";
}

更新:我感到无聊并且变得更加聪明,因此它会自动处理文件中的任意数量的8列记录。不幸的是,我没有时间解释它或添加评论。

#!/usr/bin/env perl

use strict;
use warnings;

# open input
open my $in, '<', 'somefile' or die $!;

chomp(my $head = <$in>);
my @cols = split/,/, $head;

die 'Invalid number of records - ' . @cols . "\n"
  if (@cols -2) % 8;

my @files;
my $name = 'A';
foreach (1 .. (@cols - 2) / 8) {
   my %desc;
   $desc{start_col} = (($_ - 1) * 8) + 2;
   $desc{end_col}   = $desc{start_col} + 7;
   open $desc{fh}, '>', 'file' . $name++ or die $!;
   print {$desc{fh}} join(',', @cols[0,1],
                               @cols[$desc{start_col} .. $desc{end_col}]),
                     "\n";

   push @files, \%desc;
}

while (<$in>) {
  chomp;
  my @data = split /,/;

  foreach my $f (@files) {
    print {$f->{fh}} join(',', @data[0,1],
                               @data[$f->{start_col} .. $f->{end_col}]),
                   "\n";
   }
}

答案 1 :(得分:2)

这与样本数量无关。我对输出文件名不太自信,因为你可能会达到超过26个样本。如果是这种情况,只需替换输出文件名的工作方式即可。 :)

use strict;
use warnings;

use File::Slurp;
use Text::CSV_XS;
use Carp qw( croak );

#I'm lazy
my @source_file = read_file('source_file.csv');
# you metion yours is tab separated
# just add the {sep_char => "\t"} inside new
my $csv = Text::CSV_XS->new()
  or croak "Cannot use CSV: " . Text::CSV_XS->error_diag();
my $output_file;

#read each row
while ( my $raw_line = shift @source_file ) {
    $csv->parse($raw_line);
    my @fields = $csv->fields();

    #get the first 2 ids
    my @ids = splice @fields, 0, 2;

    my $group = 0;
    while (@fields) {
        #get the first 8 columns
        my @columns = splice @fields, 0, 8;
        #if you want to change the separator of the output replace ',' with "\t"
        push @{ $output_file->[$group] }, (join ',', @ids, @columns), $/;
        $group++;
    }
}

#for filename purposes
my $letter = 65;
foreach my $data (@$output_file) {
    my $output_filename = sprintf( 'SAMPLE_%c.csv', $letter );
    write_file( $output_filename, @$data );
    $letter++;
}

#if you reach more than 26 samples then you might want to use numbers instead
#my $sample_number = 1;
#foreach my $data (@$output_file) {
#    my $output_filename = sprintf( 'sample_%s.csv', $sample_number );
#    write_file( $output_filename, @$data );
#    $sample_number++;
#}

答案 2 :(得分:0)

您说制表符已分隔,但您的示例显示它以逗号分隔。我认为将样本数据放入Markdown是一个限制吗?

我猜你有点关心内存,所以你要打开多个文件并在解析你的大文件时写下它们。

我想试试Text::CSV::Simple。但是,我相信它会将整个文件读入内存,这对于这么大的文件来说可能是一个问题。

读取一行非常容易,并将该行放入列表中。问题是将该列表中的字段映射到字段本身的名称。

如果您读入带有while循环的文件,则不会立即将整个文件读入内存。如果你读入每一行,解析该行,然后将该行写入各种输出文件,你不会占用大量内存。有一个缓存,但我相信在将\n写入文件后它会被清空。

诀窍是打开输入文件,然后在第一行读取。您想要创建某种字段映射结构,这样您就可以确定要写入每个输出文件的字段。

我会列出您需要写入的所有文件。这样,您可以浏览每个文件的列表。列表中的每个项目都应包含写入该文件所需的信息。

首先,您需要一个文件句柄,因此您知道要写入哪个文件。其次,您需要一个您必须写入该特定输出文件的字段编号列表。

我看到这样的处理循环:

while (my $line = <$input_fh>) {   #Line from the input file.
   chomp $line;
   my @input_line_array = split /\t/, $line;
   my $fileHandle;
   foreach my $output_file (@outputFileList) {  #List of output files.
       $fileHandle = $output_file->{FILE_HANDLE};
       my @fieldsToWrite;
       foreach my $fieldNumber (@{$output_file->{FIELD_LIST}}) {
          push $fieldsToWrite, $input_line_array[$field];
       }
       say $file_handle join "\t", @fieldsToWrite;
   }
}

我正在将输入文件的一行读入$line并将其划分为我放在@input_line_array中的字段。现在我有了这条线,我必须找出哪些字段写入每个输出文件。

我有一个名为@outputFileList的列表,它是我要写入的所有输出文件的列表。 $outputFileList[$fileNumber]->{FILE_HANDLE}包含输出文件$fileNumber的文件句柄。 $ouputFileList[$fileNumber]->{FIELD_LIST}是我要写入输出文件$fileNumber的字段列表。这是@input_line_array中的字段的索引。所以,如果

$outputFileList[$fileNumber]->{FIELD_LIST} = [0, 1, 2, 4, 6, 8];

表示我想将以下字段写入输出文件:$input_line_array[0]$input_line_array[1]$input_line_array[2]$input_line_array[4]$input_line_array[6]和{{ 1}}按顺序将输出文件$input_line_array[8]作为制表符分隔列表。

我希望这是有道理的。

最初的问题是在$outputFileList->[$fileNumber]->{FILE_HANDLE}的第一行读取并将其解析为所需的复杂结构。但是,现在你已经了解了如何存储这个结构,解析第一行不应该是一个太大的问题。

虽然在这个例子中我没有使用面向对象的代码(我把这些东西从我的...中拉出来...我的意思是...在我写这篇文章时的大脑)。我肯定会使用面向对象的代码方法。通过消除错误,它实际上会使事情变得更快。

答案 3 :(得分:0)

这是一个打印第一个样本的单行,你可以写一个shell脚本将不同样本的数据写入不同的文件

perl -F, -lane 'print "@F[0..1] @F[2..9]"' <INPUT_FILE_NAME>