2010-06-17 13 views
19

Estoy trabajando en un proyecto que implica el análisis de un gran archivo con formato csv en Perl y estoy buscando hacer las cosas más eficientes.¿Cómo analizo eficientemente un archivo CSV en Perl?

Mi enfoque ha sido split() el archivo por líneas primero, y luego split() cada línea otra vez por comas para obtener los campos. Pero esto es poco óptimo ya que se requieren al menos dos pases en los datos. (una vez para dividir por líneas, luego una vez más para cada línea). Este es un archivo muy grande, por lo que reducir el procesamiento a la mitad sería una mejora significativa para toda la aplicación.

Mi pregunta es, ¿cuál es el medio más eficiente en el tiempo para analizar un archivo CSV grande usando solo herramientas integradas?

nota: Cada línea tiene un número variable de tokens, por lo que no podemos simplemente ignorar las líneas y dividirlas solo por comas. También podemos suponer que los campos contendrán únicamente datos ascii alfanuméricos (sin caracteres especiales u otros trucos). Además, no quiero entrar en procesamiento paralelo, aunque podría funcionar de manera efectiva.

edición

Sólo puede implicar herramientas integradas que se incluyen con Perl 5.8. Por motivos burocráticos, no puedo usar cualquiera de los módulos de terceros (incluso si Alojado en CPAN)

otra edición

Vamos a suponer que nuestra solución sólo se le permite hacer frente a los datos del archivo una vez que se carga completamente en la memoria.

otra edición

simplemente agarré lo estúpida esta pregunta es. Lo siento por hacerte perder el tiempo. Votando para cerrar.

+4

algún motivo necesita sólo herramientas integradas (estoy suponiendo que no hay derechos de administrador). De lo contrario, intente utilizar el módulo perl 'Text :: CSV'. Hace que el análisis CSV sea mucho más fácil: http://search.cpan.org/~erangel/Text-CSV/CSV.pm –

+5

¿Por qué leer todo el archivo y 'split()' por líneas? Si acaba de abrir el archivo y utiliza la expresión '', puede repetir las líneas para que solo tenga que almacenar una línea por vez en la memoria. – mob

+1

@Mike algunos módulos perl de cpan no requieren ninguna compilación y se pueden utilizar sin el derecho del administrador ... si hay uno de tipo, ¿seguiría en la lista de la muestra que necesitaba? – Prix

Respuesta

42

La manera correcta de hacerlo - por un orden de magnitud - es usar Text::CSV_XS. Será mucho más rápido y mucho más sólido que cualquier cosa que puedas hacer por tu cuenta. Si está decidido a utilizar solo la funcionalidad central, tiene un par de opciones que dependen de la velocidad frente a la solidez.

sobre el más rápido que obtendrá por pura-Perl es leer el archivo línea por línea y luego ingenuamente dividir los datos:

my $file = 'somefile.csv'; 
my @data; 
open(my $fh, '<', $file) or die "Can't read file '$file' [$!]\n"; 
while (my $line = <$fh>) { 
    chomp $line; 
    my @fields = split(/,/, $line); 
    push @data, \@fields; 
} 

Este fallará si cualquiera de los campos contienen comas incrustadas. Un enfoque más robusto (pero más lento) sería usar Text :: ParseWords. Para ello, reemplace la split con esto:

my @fields = Text::ParseWords::parse_line(',', 0, $line); 
+0

Cuando dice un enfoque más lento, ¿quiere decir que este módulo tiene problemas de rendimiento conocidos o es solo un poco más lento? – MikeKulls

+2

@MikeKulls: No lo llamaría un problema de rendimiento per se. Es una consecuencia de hacer un análisis real en lugar de asumir ciegamente que cada coma es un separador de campo. Dicho esto, no es "un poco más lento". En un punto de referencia simple, una 'división 'simple era 10-20 veces más rápida que' parse_line'. –

+0

Supongo que es más lento también porque está escrito en perl en lugar de C para la función de división. En teoría, debería ser posible obtener un rendimiento más cercano a la función de división, por ejemplo, quizás 2-3 veces más lento. – MikeKulls

2

Puede hacerlo en una sola pasada si lee el archivo línea por línea. No hay necesidad de leer todo en la memoria a la vez.

#(no error handling here!)  
open FILE, $filename 
while (<FILE>) { 
    @csv = split /,/ 

    # now parse the csv however you want. 

} 

No estoy seguro si esto es significativamente más eficiente, Perl es bastante rápido en el procesamiento de cadenas.

NECESITA REFERIR SU IMPORTACIÓN para ver qué está causando la desaceleración. Si, por ejemplo, está haciendo una inserción de db que toma el 85% del tiempo, esta optimización no funcionará.

Editar

Aunque esto se siente como campo de código, el algoritmo general es leer todo el archivo o parte de la FIE en un búfer.

Iterar byte por byte a través del búfer hasta encontrar un delímetro csv o una nueva línea.

  • Cuando encuentre un delimitador, incremente el conteo de sus columnas.
  • Cuando encuentre una línea nueva, incremente el número de filas.
  • Si tocas el final de tu búfer, lee más datos del archivo y repite.

Eso es todo. Pero leer un archivo grande en la memoria realmente no es la mejor manera, vea mi respuesta original de la manera normal en que se hace.

+0

gracias por la respuesta. por favor vea ediciones – Mike

+0

Desde perl 5.8, cuando el archivo está en la memoria (digamos, en una variable llamada '$ escalar'), usted todavía puede usar el iterador filehandle con' open (FILE, "<", \ $ escalar) ' – mob

8

Como otras personas han mencionado, la forma correcta de hacerlo es con Text::CSV, y, o bien al final Text::CSV_XS posterior (para la lectura más rápida) o Text::CSV_PP extremo posterior (si no puede compilar el módulo XS).

Si se le permite obtener el código adicional localmente (por ejemplo, sus propios módulos personales) que podría tomar Text::CSV_PP y ponerlo en algún lugar a nivel local, luego acceder a ella a través de la solución use lib:

use lib '/path/to/my/perllib'; 
use Text::CSV_PP; 

Adicionalmente , si no hay alternativa a tener todo el archivo lee en la memoria y (supongo) almacenado en un escalar, todavía se puede leer como un identificador de archivo, mediante la apertura de un identificador para el escalar:

my $data = stupid_required_interface_that_reads_the_entire_giant_file(); 

open my $text_handle, '<', \$data 
    or die "Failed to open the handle: $!"; 

y luego leer a través de la interfaz de texto :: CSV:

my $csv = Text::CSV->new ({ binary => 1 }) 
      or die "Cannot use CSV: ".Text::CSV->error_diag(); 
while (my $row = $csv->getline($text_handle)) { 
    ... 
} 

o la división sub-óptima en comas:

while (my $line = <$text_handle>) { 
    my @csv = split /,/, $line; 
    ... # regular work as before. 
} 

Con este método, los datos sólo se copia un poco a la vez fuera del escalar

+8

Y la segunda forma más correcta de hacerlo es crear el módulo 'Mike :: Text :: CSV', copiar el código fuente de' Text :: CSV' en él, y agregar un descargo de responsabilidad sobre cómo fue "inspirado" por el código abierto de texto :: módulo CSV. – mob

+0

¡Me gusta! Me gusta mucho. –

+0

@RobertP, ¿cómo se llama $? al final de la cláusula abierta ... morir? – olala

1

Asumiendo que usted tiene su archivo CSV cargado en $csv variable y que no necesita el texto de esta variable después de analizar correctamente que:

my $result=[[]]; 
while($csv=~s/(.*?)([,\n]|$)//s) { 
    push @{$result->[-1]}, $1; 
    push @$result, [] if $2 eq "\n"; 
    last unless $2; 
} 

Si es necesario tener $csv intacta:

local $_; 
my $result=[[]]; 
foreach($csv=~/(?:(?<=[,\n])|^)(.*?)(?:,|(\n)|$)/gs) { 
    next unless defined $_; 
    if($_ eq "\n") { 
     push @$result, []; } 
    else { 
     push @{$result->[-1]}, $_; } 
} 
+0

Aparte de rellenar sus líneas de recuento de códigos, ¿de qué manera es mejor que 'división '? – mob

+0

@modrule Si usa 'split', necesita usarlo dos veces, por lo que los datos se leerán dos veces, mi solución solo lee los datos una vez. // Pero esto solo es cierto si los datos ya están cargados. – ZyX

0

Respondiendo dentro de las restricciones impuestas por la pregunta, aún puede cortar la primera división sorbiendo su archivo de entrada en una matriz en lugar de un escalar:

open(my $fh, '<', $input_file_path) or die; 
my @all_lines = <$fh>; 
for my $line (@all_lines) { 
    chomp $line; 
    my @fields = split ',', $line; 
    process_fields(@fields); 
} 

E incluso si no puede instalar (la versión pura de Perl) Text::CSV, puede obtener su código fuente en CPAN y copiar/pegar el código en su proyecto ...

14

Aquí hay una versión que también respeta las comillas (por ejemplo,foo,bar,"baz,quux",123 -> "foo", "bar", "baz,quux", "123").

sub csvsplit { 
     my $line = shift; 
     my $sep = (shift or ','); 

     return() unless $line; 

     my @cells; 
     $line =~ s/\r?\n$//; 

     my $re = qr/(?:^|$sep)(?:"([^"]*)"|([^$sep]*))/; 

     while($line =~ /$re/g) { 
       my $value = defined $1 ? $1 : $2; 
       push @cells, (defined $value ? $value : ''); 
     } 

     return @cells; 
} 

utilizar de esta manera:

while(my $line = <FILE>) { 
    my @cells = csvsplit($line); # or csvsplit($line, $my_custom_seperator) 
} 
+1

No mueva su propia rutina de análisis CSV. Es fácil hacer las cosas mal y es difícil hacerlo bien, y puede morderte DURO. Utilice Text :: CSV como se menciona en otros carteles. – MichielB

+2

No diría que nunca hacés tu propia. ¿Qué pasa si escribes uno que es mejor que las soluciones existentes de alguna manera? – MikeKulls