2010-06-27 21 views
6

Edición: solución agregada.Perl fusionando 2 archivos csv línea por línea con una clave principal

Hola, actualmente tengo algo de trabajo, aunque código lento.

Combina 2 CSV archivos línea por línea usando una clave principal. Por ejemplo, si el archivo 1 tiene la línea:

"one,two,,four,42" 

y presentar 2 tiene esta línea;

"one,,three,,42" 

where in 0 indexed $ position = 4 tiene la clave principal = 42;

luego el sub: merge_file ($ archivo1, $ archivo2, $ archivo de salida, $ posición);

dará salida a un archivo con la línea:

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

Cada clave principal es único en cada archivo, y podría existir una clave en un solo archivo, pero no en el otro (y viceversa)

Hay aproximadamente 1 millón de líneas en cada archivo.

Pasando por cada línea en el primer archivo, estoy usando un hash para almacenar la clave principal, y almacenar el número de línea como el valor. El número de línea corresponde a una matriz [línea num] que almacena cada línea en el primer archivo.

Luego reviso cada línea en el segundo archivo, y verifico si la clave primaria está en el hash, y si es así, obtengo la línea del archivo1array y luego agrego las columnas que necesito del primer arreglo al segundo conjunto, y luego concat. hasta el final. A continuación, elimine el hash y, al final, vacíe todo en el archivo. (Estoy usando un SSD, así que quiero minimizar el archivo escribe.)

Se explica probablemente con un código:

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

El primero, mientras que está muy bien, se tarda menos de 1 minuto, sin embargo, el segundo mientras que el ciclo tarda aproximadamente 1 hora en ejecutarse, y me pregunto si he tomado el enfoque correcto. Creo que es posible una gran aceleración? :) Gracias por adelantado.


Solución:

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; 

} 

Gracias a todos, la solución se publicó anteriormente. ¡Ahora se tarda aproximadamente 1 minuto en fusionar todo! :)

+0

(para la referencia (matrices de matrices): http://sunsite.ualberta.ca/Documentation/Misc/perl- 5.6.1/pod/perllol.html) – Dave

+0

Creo que todavía hay espacio suficiente para las optimizaciones, pero si es lo suficientemente rápido, vaya con él. –

Respuesta

4

Dos técnicas vienen a la mente.

  1. leer los datos de los archivos CSV en dos tablas en un DBMS (SQLite funcionaría bien), y luego usar la base de datos para hacer una combinación y escribir los datos de vuelta a CSV. La base de datos usará índices para optimizar la unión.

  2. Primero, clasifique cada archivo por clave principal (usando perl o unix sort), luego haga un escaneo lineal sobre cada archivo en paralelo (lea un registro de cada archivo; si las claves son iguales, imprima una fila unida y avance ambos archivos; si las teclas son desiguales, avance el archivo con la tecla menor y vuelva a intentar). Este paso es O (n + m) tiempo en lugar de O (n * m) y O (1) memoria.

+0

La segunda idea es muy buena. ¡Gracias! – Dave

+2

¿Qué quiere decir con O (n * m)? Él no está haciendo nada O (n * m) aquí. Está girando una vez sobre un archivo, y una vez sobre el segundo, y sin hacer nada tonto como un escaneo secuencial de la matriz dentro del segundo ciclo. –

+0

@Daniel: Si crees que la búsqueda hash es O (1), entonces eres ingenuo. Está solo en los libros, pero en realidad no es verdad. En primer lugar, la búsqueda del mapa hash es proporcional al cálculo hash, es proporcional a la longitud de la clave multiplicada por la longitud del hash y la longitud del hash es típicamente logN del espacio clave. Así que la búsqueda es en realidad al menos O (logN). (Sí, Perl usa la longitud de hash adaptable). En segundo lugar, hay algunos efectos adicionales cuando la caché de la CPU impacta y algo así. En realidad, es mucho más O (N) que O (logN) y O (1) nunca. –

0

Suponiendo alrededor de 20 líneas de bytes, cada archivo sumaría aproximadamente 20 MB, que no es demasiado grande. Dado que está usando hash, su complejidad de tiempo no parece ser un problema.

En su segundo bucle, está imprimiendo en la consola para cada línea, este bit es lento. Intenta eliminar eso, que debería ser de mucha ayuda. También puede evitar la eliminación en el segundo ciclo.

Leer varias líneas a la vez también debería ayudar. Pero no demasiado, creo, siempre habrá una lectura detrás de escena.

+0

Um, imprime en la consola solo una vez cada 1000 líneas, y la "eliminación" es muy importante para lo que hace en el ciclo siguiente a esa declaración while. –

+0

¡oh, claro! Necesito dormir un poco :) –

+0

20 bytes por línea. LOL. No conoce mucho la eficiencia de la memoria Perl. Si lo analiza y almacena en hash, toma más. –

3

Lo que está matando el rendimiento es este código, que está concatenándose millones de veces.

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

.... 

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

Si desea escribir en el fichero de salida de una sola vez, se acumulan los resultados en una matriz, y luego imprimirlos en el final, usando join. O, mejor aún, incluya las líneas nuevas en los resultados y escriba la matriz directamente.

Para ver cómo la concatenación no se escala al procesar datos grandes, experimente con esta secuencia de comandos de demostración. Cuando lo ejecuta en el modo concat, las cosas comienzan a desacelerarse considerablemente después de un par de cientos de miles de concatenaciones: abandoné y eliminé el script. Por el contrario, simplemente imprimir una serie de un millón de líneas tomó menos de un minuto en mi máquina.

# 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' sería tan lento, creo ... pero esto evitaría eso: 'foreach my $ line (@outputarray) {print $ line," \ n "; } ' – Ether

+1

@Ether No,' join' es muy rápido: órdenes de magnitud más rápidos que la creación de una cadena gigante mediante concatenación repetida. Pruébalo: modifiqué mi script de demostración. – FMc

+0

Gracias, en mi solución que publiqué, el archivo sale de la matriz. – Dave

1

no puedo ver nada de lo que me parece obvio lento, pero me gustaría hacer estos cambios:

  • En primer lugar, me gustaría eliminar la variable @file1array. No lo necesitas; simplemente almacenar la propia línea en el hash:

    while (<FILE1>){ 
        chomp; 
        $line_for{read_csv_string($_,$position)}=$_; 
    } 
    
  • En segundo lugar, aunque esto no debería realmente hacer una gran diferencia con el Perl, no añadiría a $OUTSTRING todo el tiempo. En su lugar, mantenga una matriz de líneas de salida y push en ella cada vez. Si por algún motivo aún necesita llamar al write_line con una cadena masiva, siempre puede usar join('', @OUTLINES) al final.

  • Si write_line no utiliza syswrite o algo de bajo nivel de esa manera, sino que usa print u otras llamadas basadas en stdio, entonces usted no está ahorrando ningún escrituras en disco mediante la creación del archivo de salida en la memoria. Por lo tanto, es mejor que no construyas tu salida en la memoria, sino que la escribas mientras la creas. Por supuesto, si está usando syswrite, olvídelo.

  • Dado que nada es obviamente lento, intente arrojar Devel::SmallProf a su código. He encontrado que es el mejor perfilador de perl para producir esos "¡Oh! ¡Esa es la línea lenta!" ideas

+0

¡Gracias por los consejos! :) – Dave

+0

1. Inicialmente almacené la línea en un hash, pero pensé que la estaba ralentizando, así que traté de minimizar el tamaño de los pares clave-valor a solo la clave y el número de línea para ver si ayudaría. (obviamente no lo hizo) 2. sí, el punto está hecho. Usaré matrices en lugar de concatenar todo en una gran cadena. 3. No se utiliza syswrite, consejo tomado. 4. sí, estudiaremos el uso de SmallProf para el código futuro. – Dave

+0

3.Por cierto, descubrí que si escribo línea por línea con una instrucción print OUT, $ _ en un bucle foreach(), se bloqueará/desconectará mi unidad SSD. Considerando que, si uso una sola impresión OUT $ OUTSTRING; entonces esto funcionará bien. (tal vez el controlador para el disco SSD es malo). Cuando ejecuto el programa en un disco duro rotativo mecánico, entonces no puedo hacer ningún problema. – Dave

0

Guardaría cada registro en un hash cuyas claves son las principales.El valor de una clave primaria dada es una referencia a una matriz de valores CSV, donde undef representa un valor desconocido.

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

Su programa principal se parecerá

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

El módulo Data::Dumper muestra que el hash resultante dadas las entradas simples de su pregunta es

$VAR1 = { 
    '42' => [ 
    'one', 
    'two', 
    'three', 
    'four' 
    ] 
};
1

Si desea fusionar realmente debería fusionarse . Antes que nada, debes ordenar tus datos por clave y luego fusionarlos. Batirás incluso a MySQL en rendimiento. Tengo mucha experiencia con eso.

se puede escribir algo por el estilo:

#!/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; 

salida de redirección para presentar y medir.

Si te gusta más cordura en torno argumentos de clasificación que puede reemplazar al abrir el archivo con la parte

(open my $file1, '-|') || exec('sort', '-n', "-k$sortpos", '-t,', $ARGV[0]); 
(open my $file2, '-|') || exec('sort', '-n', "-k$sortpos", '-t,', $ARGV[1]); 
+0

Este código fue realmente útil, gracias! – Dave

Cuestiones relacionadas