Traitement de gros fichiers CSV

Traitement de gros fichiers CSV - PHP - Programmation

Marsh Posté le 12-08-2015 à 13:13:36    

Bonjour,
 
Je me permets d'ouvrir ce sujet car je dois traiter de gros fichiers CSV (1go par fichier pour environ 450 millions de lignes et y a 23 fichiers comme ça), la lecture ligne par ligne du fichier par PHP prend 200s (ce qui est raisonnable) mais le stockage de ces données dans de nouveaux fichiers prend lui énormément de temps (là ça fait 1h que ça tourne et je n'ai crée qu'une heure et y en a plus de 30 par fichier).  
 
En gros voici mon code :
 
Je lis chaque ligne du fichier csv avec fgetcsv, chaque ligne contient comme donnée l'heure / la latitude / longitude / et la valeur, ensuite je crée un dossier par couple latitude / longitude (avec un dossier par latitude arrondie pour amoindrir le nombre de dossiers) et un fichier par heure, le fichier par heure contenant toutes les données associées à cette heure-ci. J'utilise la commande echo pour écrire les données dans les fichiers / ajouter les données à ceux-ci.
 
J'avais essayé de stocker tout ça dans MySQL mais c'est encore bien plus long.
 
Vous avez une idée de la technique à employer pour traiter un tel volume de données ? y a-t-il un langage approprié pour ça ?
 
Je vous remercie d'avance pour votre aide.
 
Bonne journée à vous

Reply

Marsh Posté le 12-08-2015 à 13:13:36   

Reply

Marsh Posté le 17-08-2015 à 16:30:54    

salut,
clairement, je ne sais pas si PHP est le meilleur langage que ça ... J'aurais bien dit du shell ou du PowerShell (dépend de ton environnement), au moins tu ne serais pas tributaire d'une série de variables comme celles d'Apache (taille max mémoire, etc.)
surtout que créer des dossiers, en shell ça se fait très bien. Des requêtes MySQL ça doit pouvoir se faire (au besoin un appel d'un PHP). Idem avec PowerShell.

Reply

Marsh Posté le 17-08-2015 à 16:47:01    

Bonjour,
 
Oui PHP n'est pas adapté, c'est clair.  
 
J'ai donc testé le sh, ça marche bien mais ça met quand même du temps, je pense que le mieux serait de traiter en C++, je ferai un code prochainement et je le posterai sur le forum (si d'autres sont intéressés).
 
J'ai aussi commencé à tester Python, l'occasion d'apprendre un nouveau langage.
 
La solution que j'ai adopté, en attendant de traiter de gros fichiers csv en C, c'est de traiter à la volée les données initiales en découpant le fichier en plein de petits fichiers, c'est très rapide, aussi rapide que si je stockais ces données dans des fichiers en dur ou dans une BDD.

Reply

Marsh Posté le 17-08-2015 à 22:34:46    

salut,
 
D'après la doc, fgetcsv lit la totalité du fichier puis charge les données en mémoire, donc la mémoire contient au moins 1 Go de données après l'appel à fgetcsv .
Quand on travaille avec de gros fichiers, le mieux est d'ouvrir le fichier avec fopen et récupérer les lignes avec fgets voir fread. Ca permet de traiter des fichiers de plusieurs Go avec moins de 50 Mo. Ce qui revient à traiter le fichier à la volée comme dit plus haut, et donc de commencer à traiter le fichier dès la première seconde.
 
Comment sont écrit les fichiers ? Si le code utilisé ressemble à exec("echo toto > aaa" ); je comprend que ça prenne beaucoup de temps. A chaque appel de commande, php doit redémarrer un /bin/sh qui lui même démarre /bin/echo (création processus x2, allocation mémoire, réservation pid etc.) ce qui prend plus de temps que faire un fopen, fwrite, fclose. (ou en plus court file_put_contents)
 
Pour étayer mes propos :
 

Code :
  1. $ time echo '<?php for ($i=0;$i<1000;$i++) { exec("echo a > bb" ); }'|php
  2. real 0m2.703s
  3. user 0m0.199s
  4. sys 0m2.295s
  5. $ time echo '<?php for ($i=0;$i<1000;$i++) { file_put_contents("bb", "a" ); }'|php
  6. real 0m0.377s
  7. user 0m0.012s
  8. sys 0m0.197s
  9. $ time echo '<?php for ($i=0;$i<1000;$i++) { }'|php
  10. real 0m0.029s
  11. user 0m0.019s
  12. sys 0m0.008s
  13. $ time echo '<?php for ($i=0;$i<1000;$i++) { $f = fopen("bb", "w" ); fclose($f); }'|php
  14. real 0m0.073s
  15. user 0m0.020s
  16. sys 0m0.044s
  17. $ time echo '<?php for ($i=0;$i<1000;$i++) { $f = fopen("bb", "w" ); fwrite($f, "a" ); fclose($f); }'|php
  18. real 0m0.867s
  19. user 0m0.043s
  20. sys 0m0.174s


 
php en mode shell (autrement dit php-cli) n'est pas tributaire d'Apache.
 
Cette tâche est basique, et il n'y a pas de langage plus adapté que d'autre, tous se valent.


Message édité par czh le 18-08-2015 à 00:11:45
Reply

Marsh Posté le 18-08-2015 à 22:43:42    

:hello: !
 
Si tu veux importer tes données dans une BdD, il me semble que chaque SGBDR propose sa solution : tu as la commande LOAD DATA INFILE pour MySQL, SQL*Loader pour Oracle,...
 
 :jap:


---------------
And in the end, the love you take is equal to the love you make
Reply

Marsh Posté le 25-08-2015 à 10:53:48    

Perso, j'ai été amené à faire un mix entre php, mysql et un binaire en C pour faire un gros traitement (analyse sémantique de tickets puis calcul des taux de corrélation entre eux afin d'identifier, pour un ticket donné, ceux similaires).
 
Je travaille sur un ensemble restreint d'environ 5000 tickets et 3500 termes, donc une matrice de 5000x3500. Je devais calculer le produit de la transposée de la matrice par elle-même. J'ai codé toute la partie d'analyse textuelle en php avec stockage de la matrice dans Mysql (une simple table de 3 champs : ligne, colonne, valeur pesant environ 160 Mo). Après, je faisais, via le script php, un export de la table en CSV (environ 300 Mo) avec une requête SQL puis le script php appelait un programme écrit en C pour faire le calcul matriciel. Enfin, le script php récupérait le csv généré et l'importait dans Mysql via une requête SQL.
 
A noter que j'avais aussi testé avec le calcul du produit matriciel via une requête SQL. En gros, avec le programme en C, ça met moins de 3 min; avec Mysql, environ 20 min. Avec PHP, c'est même pas la peine, ça se chiffre en heures :(
 
PHP n'étant pas compilé, il n'est pas adapté à de gros traitements. Pour ça, le mieux reste le C.


---------------
Astres, outil de help-desk GPL : http://sourceforge.net/projects/astres, ICARE, gestion de conf : http://sourceforge.net/projects/icare, Outil Planeta Calandreta : https://framalibre.org/content/planeta-calandreta
Reply

Marsh Posté le 25-08-2015 à 18:30:55    

Oui, enfin, pour faire ce qu'il veut, j'utiliserais Perl ou Python. C'est le genre de langage adapté à ce genre de traitement (c'est pas pour rien qu'ils sont pas mal utilisé en traitement des données génétiques)
 
 
Grosso modo en perl ça ressemblerait à
 
#!/usr/local/bin/perl -w
use strict;
use warnings;
use autodie;
 
my $filename = '...';
open (my $data_fh, '<', $filename);
while ( <$data_fh> ) {
    if (/^...... un pattern correspondant a la structure des lignes..$/o) {
        # on a matché les groupes
        my ($heure, $latitude, $longitude, $valeur) = ($1, $2, $3, $4);
        $latitude = arrondir($latitude); # routine arrondir a écrire
        mkdir $latitude unless (-d $latitude);
        mkdir $latitude.'/'.$longitude unless (-d $latitude.'/'.$longitude);
        open ( my $out_fh, '>>', $latitude.'/'.$longitude.'/'.$heure);
        print $out_fh $valeur, "\n"; # ou ce qui conviendra
        close ( $out_fh );
    }
}
close ($data_fh);
 
Et les interfaces DBI pour faire communiquer du code perl avec une BDD, c'est un truc qui marche très bien.
 
Et si on dispose de plus de 1Go de mémoire pour le programme, ce qui n'est pas si rare de nos jours, on peut d'abord
- lire en mémoire tout le fichier dans une structure type hash complexe,%data:
  quand on a lu  ($heure, $latitude, $longitude, $valeur), on va faire
  push @{$data{$latitude}{$longitude}{$heure}}, $valeur
- parcourir le hash pour créer les répertoires et écrire dans les fichiers (ça permet de n'ouvrir qu'une fois chaque fichier dans lequel on va écrire)
Bref ce sera optimal en temps, puisqu'on ne fait que le nombre minimal possible d'appels au système pour créer et ouvrir des fichiers.
Le code ressemblerait à ceci:
 
#!/usr/local/bin/perl -w
use strict;
use warnings;
use autodie;
 
my $filename = '...';
open (my $data_fh, '<', $filename);
my %data;
while ( <$data_fh> ) {
    if (/^...... un pattern correspondant a la structure des lignes..$/o) {
        # on a matché les groupes
        my ($heure, $latitude, $longitude, $valeur) = ($1, $2, $3, $4);  
        # note un code optimal utilisera des "named capture groups" directement pour éviter 4 assignations par ligne lue
        $latitude = arrondir($latitude); # routine arrondir a écrire
        if ( $data{$latitude}{$longitude}{$heure} ) {
            push @{$data{$latitude}{$longitude}{$heure}}, $valeur;
        }
        else {
            $data{$latitude}{$longitude}{$heure} = [$valeur];
        }
    }
}
close ($data_fh);  
 
foreach my $latitude (keys %data) {
    mkdir $latitude unless (-d $latitude);
    foreach my $longitude (keys %{$data{$latitude}}) {
        mkdir $latitude.'/'.$longitude unless (-d $latitude.'/'.$longitude);
        foreach my $heure (keys %{$data{$latitude}{$longitude}}) {
            open ( my $out_fh, '>', $latitude.'/'.$longitude.'/'.$heure);
            foreach my $valeur (@{$data{$latitude}{$longitude}{$heure}}) {
                print $out_fh $valeur, "\n"; # ou ce qui conviendra
            }
            close ( $out_fh );
        }
    }
}
 
 
Le code  
        if ( $data{$latitude}{$longitude}{$heure} ) {
            push @{$data{$latitude}{$longitude}{$heure}}, $valeur;
        }
        else {
            $data{$latitude}{$longitude}{$heure} = [$valeur];
        }
peut peut-être être remplacé par
       $data{$latitude}{$longitude}{$heure} //= [];
       push @{$data{$latitude}{$longitude}{$heure}}, $valeur;
c'est a tester pour voir si c'est plus rapide.
 
Au prix d'un flag, on peut encore optimiser: quand on crée un répertoire, inutile de tester l'existence des sous répertoires ou fichiers, et les tests sur le file system sont probablement plus couteux en temps d'exécution que le test d'un flag.
 
A+,


Message édité par gilou le 25-08-2015 à 22:12:56

---------------
There's more than what can be linked! --    Iyashikei Anime Forever!    --  AngularJS c'est un framework d'engulé!  --
Reply

Sujets relatifs:

Leave a Replay

Make sure you enter the(*)required information where indicate.HTML code is not allowed