web-dev-qa-db-fra.com

Comment analyser efficacement un fichier CSV en Perl?

Je travaille sur un projet qui implique l'analyse d'un gros fichier au format csv en Perl et cherche à rendre les choses plus efficaces.

Mon approche a consisté à split() le fichier d'abord par lignes, puis split() à nouveau chaque ligne par des virgules pour obtenir les champs. Mais ce n'est pas optimal car au moins deux passes sur les données sont nécessaires. (une fois pour diviser par lignes, puis à nouveau pour chaque ligne). Il s'agit d'un fichier très volumineux, donc réduire le traitement de moitié serait une amélioration significative pour l'ensemble de l'application.

Ma question est la suivante: quel est le moyen le plus efficace pour analyser un gros fichier CSV en utilisant uniquement des outils intégrés?

remarque: Chaque ligne a un nombre variable de jetons, nous ne pouvons donc pas simplement ignorer les lignes et les séparer par des virgules uniquement. Nous pouvons également supposer que les champs ne contiendront que des données alphanumériques ascii (pas de caractères spéciaux ou d'autres astuces). De plus, je ne veux pas entrer dans le traitement parallèle, bien que cela puisse fonctionner efficacement.

modifier

Il ne peut impliquer que des outils intégrés fournis avec Perl 5.8. Pour des raisons bureaucratiques, je ne peux utiliser aucun module tiers (même s'il est hébergé sur cpan)

une autre modification

Supposons que notre solution ne soit autorisée à traiter les données du fichier qu'une fois qu'elles sont entièrement chargées en mémoire.

encore une autre modification

Je viens de comprendre à quel point cette question est stupide. Désolée de vous avoir fait perdre votre temps. Voter pour clore.

26
Mike

La bonne façon de le faire - par ordre de grandeur - est d'utiliser Text :: CSV_XS . Il sera beaucoup plus rapide et beaucoup plus robuste que tout ce que vous êtes susceptible de faire vous-même. Si vous êtes déterminé à n'utiliser que les fonctionnalités de base, vous avez quelques options en fonction de la vitesse par rapport à la robustesse.

Le plus rapide que vous obtiendrez pour pure-Perl consiste à lire le fichier ligne par ligne, puis à diviser naïvement les données:

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

Cela échouera si des champs contiennent des virgules incorporées. Une approche plus robuste (mais plus lente) consisterait à utiliser Text :: ParseWords. Pour ce faire, remplacez le split par ceci:

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

Voici une version qui respecte également les guillemets (par exemple 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;
}

Utilisez-le comme ceci:

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

Comme d'autres personnes l'ont mentionné, la bonne façon de procéder est d'utiliser Text :: CSV , et soit le Text::CSV_XS back-end (pour une lecture PLUS RAPIDE) ou Text::CSV_PP back-end (si vous ne pouvez pas compiler le module XS).

Si vous êtes autorisé à obtenir du code supplémentaire localement (par exemple, vos propres modules personnels), vous pouvez prendre Text::CSV_PP et placez-le quelque part localement, puis accédez-y via le use lib solution de contournement:

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

De plus, s'il n'y a pas d'alternative à la lecture du fichier entier en mémoire et (je suppose) stocké dans un scalaire, vous pouvez toujours le lire comme un descripteur de fichier, en ouvrant un descripteur au scalaire:

my $data = stupid_required_interface_that_reads_the_entire_giant_file();

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

Et puis lisez via l'interface 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)) {
    ...
}

ou la division sous-optimale sur des virgules:

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

Avec cette méthode, les données ne sont copiées que peu à la fois hors du scalaire.

9
Robert P

Vous pouvez le faire en un seul passage si vous lisez le fichier ligne par ligne. Il n'est pas nécessaire de lire le tout en mémoire à la fois.

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

     # now parse the csv however you want.

}

Je ne sais pas vraiment si c'est beaucoup plus efficace, Perl est assez rapide pour le traitement des chaînes.

VOUS DEVEZ BANCER VOTRE IMPORTATION pour voir ce qui cause le ralentissement. Si, par exemple, vous effectuez une insertion de base de données qui prend 85% du temps, cette optimisation ne fonctionnera pas.

Éditer

Bien que cela ressemble à du golf de code, l'algorithme général consiste à lire le fichier entier ou une partie de la fie dans un tampon.

Itérer octet par octet dans le tampon jusqu'à ce que vous trouviez un délimiteur csv ou une nouvelle ligne.

  • Lorsque vous trouvez un délimiteur, augmentez le nombre de colonnes.
  • Lorsque vous trouvez une nouvelle ligne, augmentez le nombre de lignes.
  • Si vous atteignez la fin de votre tampon, lisez plus de données du fichier et répétez.

C'est ça. Mais la lecture d'un gros fichier en mémoire n'est vraiment pas la meilleure façon, voir ma réponse d'origine pour la façon normale de procéder.

2
Byron Whitlock

En supposant que votre fichier CSV soit chargé dans $csv variable et que vous n'avez pas besoin de texte dans cette variable après l'avoir analysée avec succès:

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

Si vous devez avoir $csv intact:

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

En répondant aux contraintes imposées par la question, vous pouvez toujours couper le premier fractionnement en transformant votre fichier d'entrée en tableau plutôt qu'en scalaire:

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

Et même si vous ne pouvez pas installer (la version pure-Perl de) Text::CSV, vous pourrez peut-être vous en sortir en récupérant son code source sur CPAN et en copiant/collant le code dans votre projet ...

1
Dave Sherohman