2010-06-27 25 views
6

編輯:添加解決方案。Perl將主要鍵合併成2個csv文件

嗨,我目前有一些工作,雖然代碼很慢。

它合併2 CSV文件逐行使用主鍵。 例如,如果文件1具有行:

"one,two,,four,42" 

和文件2具有這條線;

"one,,three,,42" 

其中0索引$ position = 4的主鍵爲42;

then sub:merge_file($ file1,$ file2,$ outputfile,$ position);

將輸出與行一個文件:

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

每個主鍵是在每個文件中唯一的,並且一個密鑰可能存在於一個文件而不是在其他(並且反之亦然)

每個文件中大約有100萬行。

通過第一個文件中的每一行,我使用散列來存儲主鍵,並將行號存儲爲值。行號對應於存儲第一個文件中每行的數組[行號]。

然後我遍歷第二個文件中的每一行,並檢查主鍵是否位於散列中,如果是,則從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分鐘,但第二while循環需要大約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分鐘來合併整個事情! :)

+0

(供參考(數組的數組替換文件打開部分)更理智:http://sunsite.ualberta.ca/Documentation/Misc/perl- 5.6.1/pod/perllol.html) – Dave 2010-06-27 14:06:56

+0

我認爲仍然有足夠的空間進行優化,但是如果速度足夠快就可以使用它。 – 2010-06-27 18:47:54

回答

4

想到兩種技巧。

  1. 閱讀從CSV文件中的數據爲在DBMS兩個表(SQLite的會工作得很好),然後使用DB做一個連接和數據寫回爲CSV。數據庫將使用索引來優化連接。

  2. 首先,按主鍵對每個文件進行排序(使用perl或unix sort),然後對每個文件並行執行線性掃描(從每個文件讀取一條記錄;如果鍵相等則輸出連接的行並推進兩個文件;如果密鑰不相等,則用較小的密鑰推進文件並再次嘗試)。這一步是O(n + m)時間而不是O(n * m)和O(1)存儲器。

+0

第二個想法非常好。謝謝! – Dave 2010-06-27 01:06:47

+2

O(n * m)是什麼意思?他在這裏沒有做任何O(n * m)。他在一個文件上循環一次,在第二個循環中循環一次,而不像第二個循環內數組的順序掃描那樣做任何事情。 – 2010-06-27 01:20:22

+0

@Daniel:如果你認爲哈希查找是O(1),那麼你太天真了。這只是在書本上,但實際上並非如此。首先,哈希映射查找與哈希計算成正比,它與哈希長度成比例,並與哈希長度成正比,哈希長度通常是關鍵空間的logN。所以查找實際上至少是O(logN)。 (是的,Perl使用自適應散列長度。)其次,還有一些額外的效果作爲CPU緩存命中等等。實際上,O(N)比O(logN)和O(1)永遠都要多得多。 – 2010-06-27 10:33:00

0

假設大約20個字節行,每個文件將大約20 MB,這不是太大。 由於你使用散列,你的時間複雜度似乎不成問題。

在你的第二個循環中,你打印到每一行的控制檯,這個位很慢。嘗試刪除應該幫助很多。 您也可以避免在第二個循環中刪除。

一次讀多行也應該有所幫助。但我認爲不會太多,所以在幕後總會有一個預先閱讀。

+0

嗯,他每1000行打印到控制檯一次,「刪除」對於他在while語句後循環中做什麼非常重要。 – 2010-06-27 01:02:33

+0

哦對!我需要一些睡眠:) – 2010-06-27 01:17:53

+0

每行20個字節。大聲笑。你不知道Perl的內存效率。如果解析它並以散列方式存儲,則需要更多。 – 2010-06-27 09:44:01

3

什麼是性能的殺死這個代碼,它連接了數百萬次。

$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; 
} 
+0

'join'會一樣慢,我認爲......但這會解決這個問題:'foreach my $ line(@outputarray){print $ line,「\ n」; }' – Ether 2010-06-27 03:46:41

+1

@Ether不,''join'速度非常快 - 比通過重複連接構建一個巨大的字符串要快幾個數量級。試一試:我修改了我的演示腳本。 – FMc 2010-06-27 10:38:25

+0

謝謝,在我發佈的解決方案中,文件是從數組輸出的。 – Dave 2010-06-27 14:01:29

1

我看不到任何東西,在我看來是明顯緩慢,但我會做這些改變:

  • 首先,要消除@file1array變量。你不需要它;只是存儲行本身在散:

    while (<FILE1>){ 
        chomp; 
        $line_for{read_csv_string($_,$position)}=$_; 
    } 
    
  • 其次,雖然這應該算不上多大用Perl的差別,我也不會加入到$OUTSTRING所有的時間。相反,每次都要將一組輸出行和push放在它上面。如果由於某種原因,您仍然需要撥打write_line以獲得大量字符串,您最後可以始終使用join('', @OUTLINES)

  • 如果write_line不使用syswrite什麼低級別的那樣,而是使用print或其他基於標準輸入輸出通話,那麼你就不會保存任何磁盤在內存中建立的輸出文件中寫入。因此,你可能不會在內存中建立你的輸出,而是在你創建它時寫出它。當然,如果你使用syswrite,忘記這一點。

  • 由於沒有什麼明顯的緩慢,請嘗試在您的代碼處投擲Devel::SmallProf。我發現這是生產這些最好的perl分析器「哦!是慢行!」見解。

+0

感謝您的提示! :) – Dave 2010-06-27 13:52:26

+0

1.最初我將行存儲在一個散列中,但我認爲它減慢了速度,所以我試圖將鍵值大小最小化爲鍵和行號,看看它是否有幫助。 (顯然它沒有) 2.是的,這一點是成立的。我將使用數組而不是將所有內容連接成一個大字符串。 3.不使用syswrite,建議採取。 4.是的,將考慮使用SmallProf作爲未來的代碼。 – Dave 2010-06-27 14:04:53

+0

3。順便說一句,我發現,如果我用foreach()循環中的print_out,$ _語句逐行寫入,它會崩潰/斷開我的SSD驅動器。而如果我使用單個打印OUT $ OUTSTRING;那麼這將工作正常。 (也許SSD驅動器的控制器不好)。 當我在一個機械旋轉硬盤驅動器上運行程序時,我可以做到兩個都沒有問題。 – Dave 2010-06-27 23:46:11

0

我會將每條記錄存儲在一個散列中,其中的鍵是主鍵。給定主鍵的值是對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' 
    ] 
};
1

如果你想合併,你真的應該合併。首先,您必須按鍵重新排序數據,而不是合併!在性能上你甚至會擊敗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]); 
+0

此代碼非常有用,謝謝! – Dave 2010-06-27 13:57:46