¿Cómo analizo eficientemente un archivo CSV en Perl?

Estoy trabajando en un proyecto que implica analizar un gran archivo con formato csv en Perl y estoy buscando hacer las cosas más eficientes.

Mi enfoque ha sido split() el archivo por líneas primero, y luego split() cada línea nuevamente 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 eficaz en el tiempo para analizar un gran archivo CSV utilizando 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 parallel processing, aunque podría funcionar de manera efectiva.

editar

Solo puede incluir herramientas integradas que se envían con Perl 5.8. Por razones burocráticas, no puedo usar ningún módulo de terceros (incluso si está alojado en cpan)

otra edición

Supongamos que nuestra solución solo puede tratar los datos del archivo una vez que está completamente cargado en la memoria.

otra edición

Me di cuenta de lo estúpida que es esta pregunta. Lo siento por hacerte perder el tiempo. Votando para cerrar.

La forma 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.

Aproximadamente lo más rápido que obtendrá para pure-Perl es leer el archivo línea por línea y luego dividir ingenuamente 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; } 

Esto fallará si algún campo contiene comas incrustadas. Un enfoque más robusto (pero más lento) sería usar Text :: ParseWords. Para hacer eso, reemplace la split con esto:

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

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

Úselo así:

 while(my $line = ) { my @cells = csvsplit($line); # or csvsplit($line, $my_custom_seperator) } 

Como otras personas mencionaron, la forma correcta de hacerlo es con Text :: CSV , y con el extremo posterior Text::CSV_XS (para la lectura MÁS Text::CSV_PP ) o el Text::CSV_PP (si no puede comstackr el módulo XS ) .

Si puede obtener código adicional localmente (por ejemplo, sus propios módulos personales), puede tomar Text::CSV_PP y colocarlo en algún lugar localmente, luego acceder a él a través de la solución alternativa de use lib :

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

Además, si no hay otra alternativa que tener todo el archivo leído en la memoria y (supongo) almacenado en un escalar, igual puede leerlo como un identificador de archivo, abriendo un identificador al 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 lea a través de la interfaz Text :: 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 solo se copian un poco a la vez fuera del escalar.

Puede hacerlo de una vez 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 () { @csv = split /,/ # now parse the csv however you want. } 

Sin embargo, 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 código de golf, el algoritmo general es leer todo el archivo o parte del archivo en un búfer.

Iterar byte por byte a través del buffer hasta encontrar un delimeter csv, o una nueva línea.

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

Eso es. 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.

Suponiendo que tiene su archivo CSV cargado en la variable $csv y que no necesita texto en esta variable después de haberlo analizado correctamente:

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

Si necesita tener $csv intacto:

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

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) de Text::CSV , puede obtener el código fuente en CPAN y copiar / pegar el código en su proyecto …