parser très gros fichier texte

Mammouth du PHP | 19672 Messages

04 juil. 2006, 11:26

Salut tout le monde,
Une fois n'est pas coutume, j'ai une question.

Je dois parser des fichiers de données que je récupère au format txt. ce sont à priori des dump de base, mais je n'ai accès qu'à ces fichiers. les données sont structurées en lignes, chaque données séparées par une tabulation. Exemple :

Code : Tout sélectionner

FR 10 75948 0 RE65010 16 F S7 FR 10 75949 0 RE65011 16 F S7 FR 10 81588 0 RE65016 16 G S7 FR 10 75950 0 RE65012 16 G S7 FR 10 81589 0 RE65017 16 H S7 FR 10 79504 0 RE65014 * J S7
Je me suis fait une petite classe pour sortir ces données en tableau dont voici un extrait:
<?php
//...
    private function lirefichier()
    {
        $fiche = $this->repertoire . $this->fichier;
        if(file_exists($fiche))
        {
            $lignes = file($fiche);
            foreach($lignes as $num_ligne => $ligne)
            {
                $this->tableau[$num_ligne] = explode("\t", rtrim($ligne));
            }
        }
    }
//...
?>
Ca fonctionne parfaitement mais...

Le problème est que certains fichiers sont particulièrement lourd, j'en ai notament un de 11.9Mo et un autre de 32Mo. Le timeout explose et les ressources système sont sollicitées à fond.

J'ai essayé en augmentant le timeout à 600 secondes et en augmentant la mémoire allouée au script de 8 à 128Mo pour tester. Même là, ça rame à plein tube.

La manipulation se fera de toutes façons en local, donc les paramètres de configurations sont à mon entière discrétion et les données seront transférées vers une abse MySQL (à priori MySQL 5.0.xx), mais je cherche une manière d'accélérer un brin le processus : est-ce que l'un d'entre vous aurait une idée à me suggérer.
Modifié en dernier par Cyrano le 06 juil. 2006, 09:31, modifié 2 fois.
Codez en pensant que celui qui maintiendra votre code est un psychopathe qui connait votre adresse :axe:

Administrateur PHPfrance
Administrateur PHPfrance | 11457 Messages

04 juil. 2006, 11:51

Une suggestion, peut-être naïve...

As-tu la possibilité de découper ton fichier en sous-fichiers
afin que leur taille, plus raisonnable, permette l'exécution de ta fonction
sans qu'un TimeOut vienne te sortir à chaque fois ?

Mammouth du PHP | 1311 Messages

04 juil. 2006, 11:51

salut
tu ne peut pas le faire directement avec mysql et load data in file
en specifaint les delimiteur de tes champs
LOAD DATA [LOW_PRIORITY | CONCURRENT] [LOCAL] INFILE 'file_name.txt'
[REPLACE | IGNORE]
INTO TABLE tbl_name
[FIELDS
[TERMINATED BY '\t']
[[OPTIONALLY] ENCLOSED BY '']
[ESCAPED BY '\\' ]
]
[LINES
[STARTING BY '']
[TERMINATED BY '\n']
]
[IGNORE number LINES]
[(col_name,...)]

Administrateur PHPfrance
Administrateur PHPfrance | 11457 Messages

04 juil. 2006, 11:55

Le LOAD DATA va-t-il résoudre le problème du volume important de données à traiter ?
Ne risque-t-il pas de partir en sucette sur un TimeOut ou autre erreur ?

Mammouth du PHP | 1311 Messages

04 juil. 2006, 11:57

A vrai dire je n'en sais rien

Administrateur PHPfrance
Administrateur PHPfrance | 11457 Messages

04 juil. 2006, 11:59

A vrai dire je n'en sais rien
Le problème, c'est que... moi non plus. :oops: :lol:

Modérateur PHPfrance
Modérateur PHPfrance | 6373 Messages

04 juil. 2006, 12:02

Un LOAD DATA est bien plus performant qu'une manipulation par PHP pour charger directement des données dans MySQL, donc si c'est le but ça serait une bonne solution je pense :)
Modifié en dernier par ouckileou le 04 juil. 2006, 12:02, modifié 1 fois.

Avatar du membre
Administrateur PHPfrance
Administrateur PHPfrance | 13231 Messages

04 juil. 2006, 12:02

Le LOAD DATA INFILE est optimisé pour transférer des données à grande vitesse, il est donc très probable que l'exécution de cette requete puis d'une autre requete de sélection soit beaucoup plus rapide que le parsage du fichier direct.

Maintenant, il reste à voir si l'utilisation d'une bdd tampon est possible et/ou envisageable :-k
Connaître son ignorance est la meilleure part de la connaissance
Pour un code lisible : n'hésitez pas à sauter des lignes et indenter

twitter - site perso - Github - Zend Certified Engineer

ViPHP
ViPHP | 1380 Messages

04 juil. 2006, 13:15

salut
tu ne peut pas le faire directement avec mysql et load data in file
en specifaint les delimiteur de tes champs
+1
ripat

Administrateur PHPfrance
Administrateur PHPfrance | 3131 Messages

04 juil. 2006, 13:35

C'est pourquoi faire tes données ? Si tu n'es pas obligé de les traiter en bloc un usage de fopen()/fgets() sera plus performant que file() au niveau occupation mémoire et vitesse.

Administrateur PHPfrance
Administrateur PHPfrance | 3088 Messages

04 juil. 2006, 13:46

Je confirme : LOAD DATA est super rapide donc si ton fichier est correctement formaté c'est la solution vers laquelle t'orienter si tu souhaites simplement importe son contenu sans appliquer de transformations à la volée. Enfin, même ça c'est possible dans les versions récentes si mes souvenirs sont bons, mais là tout de suite je le déconseillerais un peu.

Sinon, le bout de script que tu as posté possède le gros défaut de ne pas être "scalable". La quantité de mémoire et le temps CPU utiliséssont directement proportionnels à la taille des fichiers à cause de file() et de la mise en tableau du résultat. Pour les gros fichiers, il vaut mieux utiliser quelque chose comme fread() et ne stocker qu'une petites partie des enregistrements en mémoire tampon. Si le but recherché est l'import des données alors j'essaierais plutôt quelque chose comme
// Tampon d'enregistrements à sauvegarder
$insert = array();
$i = 0;

$filepath = 'lefichier.txt';
$fp = fopen($filepath, 'r');

while ($row = fscanf($fp, "%s\t%s\t%s\n"))
{
	$insert[] = "('" . $db->escape_string($row[0]) . "','" . $db->escape_string($row[1]) . "','" . $db->escape_string($row[2]) . "')";

	if (++$i == 100)
	{
		// On insère les enregistrements 100 par 100
		$sql = 'INSERT DELAYED INTO table (foo, bar, baz) VALUES ' . implode(',', $insert);
		$db->query($sql);

		$insert = array();
		$i = 0;
	}
}

if ($i)
{
	// On insère les enregistrements restants
	$sql = 'INSERT DELAYED INTO table (foo, bar, baz) VALUES ' . implode(',', $insert);
	$db->query($sql);

	unset($insert);
}
Attention, si l'import plante au milieu du te retrouveras avec une demi-table, donc il te faudra peut-être mettre le tout dans une transaction :) (auquel cas, le "DELAYED" devient inutile)

Mammouth du PHP | 19672 Messages

04 juil. 2006, 14:08

Merci bien pour toutes ces réponses, je viens tout juste de revenir pour jeter un coup d'oeil et la solution de LOAD DATA INFILE était dans les options auxquelles j'ai pensé aussi. Le seul problème est que je devrai dans ce cas avoir une base intermédiaire parce que je n'ai pas besoin obligatoirement de toutes les données, la base intermédiaire étant la copie de celle d'où sortent ces données et sur laquelle je n'ai absolument aucun acces.

D'autant qu'en plus, si j'ai évoqué des fichiers de 32Mo, c'est le plus gros que j'ai actuellement dans un jeu d'essai : je m'attends à pire dans les fichier que va nous envoyer le fournisseur à qui on achète ces données.

@ Albat : couper les fichiers serait à la rigueur envisageable, mais je vais devoir commencer par les récupérer par des fonctions ftp. Si la taille est trop importante et que même avec un load data infile ça plante tout, ce sera peut-être une partie de la solution.
Merci tout me monde, je mets pas ça en résolu pour l'instant, je testerai avant et on verra.

++
Codez en pensant que celui qui maintiendra votre code est un psychopathe qui connait votre adresse :axe:

Mammouth du PHP | 19672 Messages

04 juil. 2006, 19:43

Bon, ben c'est bien ce que je craignais et ça va exclure totalement ma première solution : j'ai eu des infos et le plus gros fichier pèse aux environs de 670Mo. Selon le fournisseur, il faut environ 30mn pour le charger dans une base Oracle, j'ai hâte de voir ça dans une base MySQL... :-k

Quoiqu'il en soit, je vais faire des tests demain avec un LOAD DATA INFILE en mettant un timeout à une heure : on verra bien, je vous raconterai peut-être mes aventures si j'ai des soucis ;)
Codez en pensant que celui qui maintiendra votre code est un psychopathe qui connait votre adresse :axe:

Administrateur PHPfrance
Administrateur PHPfrance | 3088 Messages

04 juil. 2006, 22:31

Si tu n'as pas besoin de transactions, LOAD DATA sur MyISAM est très très très rapide. Je ne saurais pas dire pour InnoDB, mais je doute que ce soit lent.

N'oublie pas de jeter un œil du côté des conseils pour améliorer les performances d'INSERT. Dans ton cas, le seul conseil qui s'applique est celui de désactiver puis réactiver les index, sauf si tu as une clé primaire sur la table. Dans ce cas, laisse les index et augmente le plus possible le cache d'index par key_buffer_size. (c'est parce que MySQL est obligé de vérifier les contraintes d'unicité donc tu ne peux pas désactiver les indexes, qui sont d'ailleurs très sollicités)

Tiens-nous au courant ;)

Mammouth du PHP | 19672 Messages

04 juil. 2006, 22:37

Pour résumer, on achète une base complète dont on va utiliser une bonne partie pour créer un produit qui sera ensuite vendu. J'aurai chaque mois une version à jour de la base complete. Mes fichiers seront des fichiers textes tels que je les ai décris en donnant un exemple dans mon message de départ : le LOAD DATA INFILE avec l'option REPLACE devrait être très simple. Pour ce qui est des index, j'ai pas mal de tables avec des clés composites, mais effectivement, l'idée de desactiver les index devrait notablement accélerer le processus. Faudra peut-être que je fasse quelques bench pour décider de la manière. Je vous raconterai, notre propre base n'est pas encore complètement finalisée de toutes façons.
Codez en pensant que celui qui maintiendra votre code est un psychopathe qui connait votre adresse :axe: