Perl, объединяющий 2 файла CSV линию за линией с первичным ключом

Править: решение добавляется.

Привет, у меня в настоящее время есть некоторая работа хотя медленный код.

Это объединяет 2 файла CSV линию за линией с помощью первичного ключа. Например, если файл 1 имеет строку:

"one,two,,four,42"

и файл 2 имеет эту строку;

"one,,three,,42"

где в 0 индексируемых $position = 4 имеет первичный ключ = 42;

затем sub: merge_file ($file1, $file2, $outputfile, $position);

произведет файл со строкой:

"one,two,three,four,42";

Каждый первичный ключ уникален в каждом файле, и ключ мог бы существовать в одном файле, но не в другом (и наоборот)

В каждом файле существует приблизительно 1 миллион строк.

Проходя каждую строку в первом файле, я использую хеш для хранения первичного ключа и храню номер строки как значение. Номер строки соответствует массиву [цифра строки], который хранит каждую строку в первом файле.

Затем я прохожу каждую строку во втором файле, и проверка, если первичный ключ находится в хеше, и если это, получает строку от file1array и затем добавляет столбцы, в которых я нуждаюсь от первого массива до второго массива, и затем concat. в конец. Затем удалите хеш, и затем в самом конце, выведите всю вещь зарегистрировать. (Я использую SSD, таким образом, я хочу минимизировать записи файла.)

Это, вероятно, лучше всего объяснено с кодом:

sub merge_file2{
 my ($file1,$file2,$out,$position) = ($_[0],$_[1],$_[2],$_[3]);
 print "merging: \n$file1 and \n$file2, to: \n$out\n";
 my $OUTSTRING = undef;

 my %line_for;
 my @file1array;
 open FILE1, "<$file1";
 print "$file1 opened\n";
 while (<FILE1>){
      chomp;
      $line_for{read_csv_string($_,$position)}=$.; #reads csv line at current position (of key)
      $file1array[$.] = $_; #store line in file1array.
 }
 close FILE1;
 print "$file2 opened - merging..\n";
 open FILE2, "<", $file2;
 my @from1to2 = qw( 2 4 8 17 18 19); #which columns from file 1 to be added into cols. of file 2.
 while (<FILE2>){
      print "$.\n" if ($.%1000) == 0;
      chomp;
      my @array1 = ();
      my @array2 = ();
      my @array2 = split /,/, $_; #split 2nd csv line by commas

      my @array1 = split /,/, $file1array[$line_for{$array2[$position]}];
      #                            ^         ^                  ^
      # prev line  lookup line in 1st file,lookup hash,     pos of key
      #my @output = &merge_string(\@array1,\@array2); #merge 2 csv strings (old fn.)

      foreach(@from1to2){
           $array2[$_] = $array1[$_];
      }
      my $outstring = join ",", @array2;
      $OUTSTRING.=$outstring."\n";
      delete $line_for{$array2[$position]};
 }
 close FILE2;
 print "adding rest of lines\n";
 foreach my $key (sort { $a <=> $b } keys %line_for){
      $OUTSTRING.= $file1array[$line_for{$key}]."\n";
 }

 print "writing file $out\n\n\n";
 write_line($out,$OUTSTRING);
}

Первое, в то время как прекрасен, занимает меньше чем 1 минуту, однако второй цикл с условием продолжения занимает приблизительно 1 час для выполнения, и я задаюсь вопросом, проявил ли я правильный подход. Я думаю, что это возможно для большого количества ускорения?:) Заранее спасибо.


Решение:

sub merge_file3{
my ($file1,$file2,$out,$position,$hsize) = ($_[0],$_[1],$_[2],$_[3],$_[4]);
print "merging: \n$file1 and \n$file2, to: \n$out\n";
my $OUTSTRING = undef;
my $header;

my (@file1,@file2);
open FILE1, "<$file1" or die;
while (<FILE1>){
    if ($.==1){
        $header = $_;
        next;
    }
    print "$.\n" if ($.%100000) == 0;
    chomp;
    push @file1, [split ',', $_];
}
close FILE1;

open FILE2, "<$file2" or die;
while (<FILE2>){
    next if $.==1;
    print "$.\n" if ($.%100000) == 0;
    chomp;
    push @file2, [split ',', $_];
}
close FILE2;

print "sorting files\n";
my @sortedf1 = sort {$a->[$position] <=> $b->[$position]} @file1;
my @sortedf2 = sort {$a->[$position] <=> $b->[$position]} @file2;   
print "sorted\n";
@file1 = undef;
@file2 = undef;
#foreach my $line (@file1){print "\t [ @$line ],\n";    }

my ($i,$j) = (0,0);
while ($i < $#sortedf1 and $j < $#sortedf2){
    my $key1 = $sortedf1[$i][$position];
    my $key2 = $sortedf2[$j][$position];
    if ($key1 eq $key2){
        foreach(0..$hsize){ #header size.
            $sortedf2[$j][$_] = $sortedf1[$i][$_] if $sortedf1[$i][$_] ne undef;
        }
        $i++;
        $j++;
    }
    elsif ( $key1 < $key2){
        push(@sortedf2,[@{$sortedf1[$i]}]);
        $i++;
    }
    elsif ( $key1 > $key2){ 
        $j++;
    }
}

#foreach my $line (@sortedf2){print "\t [ @$line ],\n"; }

print "outputting to file\n";
open OUT, ">$out";
print OUT $header;
foreach(@sortedf2){
    print OUT (join ",", @{$_})."\n";
}
close OUT;

}

Спасибо все, решение отправляется выше. Теперь требуется приблизительно 1 минута для слияния всего этого!:)

6
задан Dave 28 June 2010 в 10:48
поделиться

6 ответов

На ум приходят два метода.

  1. Считать данные из CSV-файлов в две таблицы в СУБД (SQLite подойдет как нельзя лучше), а затем использовать СУБД для объединения и записи данных обратно в CSV. База данных будет использовать индексы для оптимизации объединения.

  2. Сначала отсортируйте каждый файл по первичному ключу (используя perl или unix sort), затем выполните линейное сканирование каждого файла параллельно (прочитайте запись из каждого файла; если ключи равны, выведите объединенную строку и продвиньте оба файла; если ключи неравны, продвиньте файл с меньшим ключом и повторите попытку). Этот шаг занимает O(n + m) времени вместо O(n * m), и O(1) памяти.

4
ответ дан 10 December 2019 в 02:42
поделиться

Что убивает производительность, так это этот код, который конкатенирует миллионы раз.

$OUTSTRING.=$outstring."\n";

....

foreach my $key (sort { $a <=> $b } keys %line_for){
    $OUTSTRING.= $file1array[$line_for{$key}]."\n";
}

Если вы хотите записывать в выходной файл только один раз, накапливайте результаты в массиве, а затем выводите их в самом конце, используя join. Или, что еще лучше, включите новые строки в результаты и запишите массив напрямую.

Чтобы увидеть, как конкатенация не масштабируется при обработке больших данных, поэкспериментируйте с этим демонстрационным скриптом. Когда вы запускаете его в режиме concat, все начинает значительно замедляться после пары сотен тысяч конкатенаций - я сдался и убил скрипт. Для сравнения, простая печать массива из миллиона строк заняла на моей машине меньше минуты.

# Usage: perl demo.pl 50 999999 concat|join|direct
use strict;
use warnings;

my ($line_len, $n_lines, $method) = @ARGV;
my @data = map { '_' x $line_len . "\n" } 1 .. $n_lines;

open my $fh, '>', 'output.txt' or die $!;

if ($method eq 'concat'){         # Dog slow. Gets slower as @data gets big.
    my $outstring;
    for my $i (0 .. $#data){
        print STDERR $i, "\n" if $i % 1000 == 0;
        $outstring .= $data[$i];
    }
    print $fh $outstring;
}
elsif ($method eq 'join'){        # Fast
    print $fh join('', @data);
}
else {                            # Fast
    print $fh @data;
}
3
ответ дан 10 December 2019 в 02:42
поделиться

Если вы хотите слиться, вам действительно нужно слияние. Прежде всего вам нужно отсортировать данные по ключу, а затем объединить! Вы превзойдете даже MySQL по производительности. У меня большой опыт работы с этим.

Вы можете написать что-нибудь в этом роде:

#!/usr/bin/env perl
use strict;
use warnings;

use Text::CSV_XS;
use autodie;

use constant KEYPOS => 4;

die "Insufficient number of parameters" if @ARGV < 2;
my $csv = Text::CSV_XS->new( { eol => $/ } );
my $sortpos = KEYPOS + 1;
open my $file1, "sort -n -k$sortpos -t, $ARGV[0] |";
open my $file2, "sort -n -k$sortpos -t, $ARGV[1] |";
my $row1 = $csv->getline($file1);
my $row2 = $csv->getline($file2);
while ( $row1 and $row2 ) {
    my $row;
    if ( $row1->[KEYPOS] == $row2->[KEYPOS] ) {    # merge rows
        $row  = [ map { $row1->[$_] || $row2->[$_] } 0 .. $#$row1 ];
        $row1 = $csv->getline($file1);
        $row2 = $csv->getline($file2);
    }
    elsif ( $row1->[KEYPOS] < $row2->[KEYPOS] ) {
        $row  = $row1;
        $row1 = $csv->getline($file1);
    }
    else {
        $row  = $row2;
        $row2 = $csv->getline($file2);
    }
    $csv->print( *STDOUT, $row );
}

# flush possible tail
while ( $row1 ) {
    $csv->print( *STDOUT, $row1 );
    $row1 = $csv->getline($file1);
}
while ( $row2 ) {
    $csv->print( *STDOUT, $row2 );
    $row2 = $csv->getline($file1);
}
close $file1;
close $file2;

Перенаправить вывод в файл и измерить.

Если вам нравится больше здравомыслия в отношении аргументов сортировки, вы можете заменить часть открытия файла на

(open my $file1, '-|') || exec('sort',  '-n',  "-k$sortpos",  '-t,',  $ARGV[0]);
(open my $file2, '-|') || exec('sort',  '-n',  "-k$sortpos",  '-t,',  $ARGV[1]);
1
ответ дан 10 December 2019 в 02:42
поделиться

Если предположить, что каждый ваш файл занимает около 20 байт, то его размер составит около 20 МБ, что не слишком много. Поскольку вы используете хэш, временная сложность не кажется проблемой.

Во втором цикле вы печатаете на консоль каждую строку, это очень медленно. Попробуйте удалить это, это должно помочь. Вы также можете избежать удаления во втором цикле.

Чтение нескольких строк за раз также должно помочь. Но не слишком сильно, я думаю, всегда будет чтение вперед за сценой.

0
ответ дан 10 December 2019 в 02:42
поделиться

Я не вижу ничего, что мне показалось бы явно медленным, но я бы внес следующие изменения:

  • Во-первых, я бы удалил переменную @ file1array . Вам это не нужно; просто сохраните саму строку в хеше:

     while () {
    чавкать;
     $ line_for {read_csv_string ($ _, $ position)} = $ _;
    }
    
  • Во-вторых, хотя это не должно иметь большого значения для perl, я бы не стал постоянно добавлять $ OUTSTRING . Вместо этого сохраните массив строк вывода и нажимайте на него каждый раз. Если по какой-то причине вам все еще нужно вызвать write_line с массивной строкой, вы всегда можете использовать join ('', @OUTLINES) в конце.

  • Если write_line не использует syswrite или что-то подобное низкоуровневого, а использует print или другие вызовы на основе stdio, то вы не не сохраняет записи на диск, создавая выходной файл в памяти. Следовательно, вы можете вообще не создавать свой вывод в памяти, а вместо этого просто записывать его по мере создания. Конечно, если вы используете syswrite , забудьте об этом.

  • Поскольку очевидно, что нет ничего медленного, попробуйте ввести Devel :: SmallProf в свой код. Я обнаружил, что это лучший профилировщик Perl для создания таких "О! Это медленная линия!" идеи.

1
ответ дан 10 December 2019 в 02:42
поделиться

Я бы сохранил каждую запись в хеше, ключи которого являются первичными ключами. Значение данного первичного ключа - это ссылка на массив значений CSV, где undef представляет неизвестное значение.

use 5.10.0;  # for // ("defined-or")
use Carp;
use Text::CSV;

sub merge_csv {
  my($path,$record) = @_;

  open my $fh, "<", $path or croak "$0: open $path: $!";

  my $csv = Text::CSV->new;
  local $_;
  while (<$fh>) {
    if ($csv->parse($_)) {
      my @f = map length($_) ? $_ : undef, $csv->fields;
      next unless @f >= 1;

      my $primary = pop @f;
      if ($record->{$primary}) {
        $record->{$primary}[$_] //= $f[$_]
          for 0 .. $#{ $record->{$primary} };
      }
      else {
        $record->{$primary} = \@f;
      }
    }
    else {
      warn "$0: $path:$.: parse failed; skipping...\n";
      next;
    }
  }
}

Ваша основная программа будет напоминать

my %rec;
merge_csv $_, \%rec for qw/ file1 file2 /;

Модуль Data :: Dumper показывает, что полученный хэш с учетом простых входных данных из вашего вопроса равен

$VAR1 = {
  '42' => [
    'one',
    'two',
    'three',
    'four'
  ]
};
0
ответ дан 10 December 2019 в 02:42
поделиться
Другие вопросы по тегам:

Похожие вопросы: