[RESOLU] Parser un répertoire de fichiers DATA pour intégration MySQL

Eléphant du PHP | 103 Messages

01 oct. 2013, 17:03

Bonjour!

De façon journalière sur un répertoire dédié, plusieurs fichiers .DATA (c'est du texte lisible via le bloc-note) de données sont déposés tous du même type :
Fichier fic34154001-2013-09-30.data :
POSTE;DATE;GLOT
34154001;20130929;1168
Je souhaite créer un script PHP exécuté via CRON permettant de parser ces données pour les intégrer dans une base MySQL.

Le premier chiffre est le nom de la table SQL, le second la date, le dernier la valeur.

Une fois bien intégré (confirmation MySQL), on supprime le fichier et on passe au suivant (ou on fait un traitement en lot sachant que ce sera la même base mais des tables différentes). A la moindre erreur, on envois un mail d'alerte.


Bon déjà un tout petit début basique de chez basique :

Code : Tout sélectionner

<?php $data = file_get_contents('fic34154001-2013-09-30.data'); $matches = array(); $tab_result = array(); foreach(explode("\n", $data) as $line) { if(!empty($line)) { preg_match('/(\d+);(\d+);(\d+)/', $line, $matches); print_r($matches); } } ?>
Cela donne :
Array ( ) Array ( [0] => 34154001;20130929;1168 [1] => 34154001 [2] => 20130929 [3] => 1168 )

Grand merci et bonne journée!

Mammouth du PHP | 2278 Messages

01 oct. 2013, 17:09

Il faut passer par mysqli ou PDo (cf manuel:
"This extension is deprecated as of PHP 5.5.0, and is not recommended for writing new code as it will be removed in the future. Instead, either the mysqli or PDO_MySQL extension should be used." )
Vanitas vanitatum et omnia vanitas
Mes derniers livres :
Sauvez les Mots chez BoD,
Tous les chemins mènent à ROM chez BoD

Eléphant du PHP | 103 Messages

01 oct. 2013, 17:16

Tu parle de cette fonction : http://fr2.php.net/manual/fr/book.mysqli.php
Ca c'est pour l'injection SQL, c'est pérenne donc?

Mammouth du PHP | 2278 Messages

01 oct. 2013, 18:44

Ce n'est pas que mysqli soit perenne, c'est que mysql est obsolete :
ette extension est obsolète depuis PHP 5.5.0 et n'est pas recommandée pour écrire vos nouvelles lignes de code, sachant qu'elle sera supprimée dans un futur proche. A la place, soit l'extension mysqli ou PDO_MySQL devrait être utilisée.
Vanitas vanitatum et omnia vanitas
Mes derniers livres :
Sauvez les Mots chez BoD,
Tous les chemins mènent à ROM chez BoD

Avatar du membre
Modérateur PHPfrance
Modérateur PHPfrance | 8758 Messages

01 oct. 2013, 19:36

salut,

ton fichier c'est du csv, il y a des fonctions le parser simplement
est ce qu'il y a toujours l'entête (la 1ère ligne) ?

quand tu décris la ligne 34154001;20130929;1168, tu veux dire que
- la table s'appel : 34154001
- la date est 20130920
- la valeur est 1168 ?

je ne vois pas bien l’intérêt d'une table avec un numéro pour nom c'est pas logique et incompréhensible.

il te manque le nom de la colonne dans laquelle mettre les données.

pour continuer ce que Sirakawa a avancé, avec l'extension mysqli ou PDO tu va pouvoir utiliser une requête préparée pour l'insertion ce qui va te permettre d'accélérer un peu la chose.

attention au temps de traitement des fichiers il ne faut pas que le cron lance deux fois le script en parallèle si tu ne prévois pas le cas si non va y avoir des doublons :)


@+
Il en faut peu pour être heureux ......

Eléphant du PHP | 103 Messages

02 oct. 2013, 10:20

Bonjour et merci pour votre participation!
ton fichier c'est du csv, il y a des fonctions le parser simplement
On me conseille d'utiliser fgetcsv, qu'en pensez vous? http://php.net/manual/fr/function.fgetcsv.php

Code : Tout sélectionner

<?php if (($handle = fopen("fic34154001-2013-09-30.data", "r")) !== FALSE) { while (($data = fgetcsv($handle, 1000, ";")) !== FALSE) { print_r($data); } fclose($handle); } ?>
Cela donne :
Array ( [0] => POSTE [1] => DATE [2] => GLOT ) Array ( [0] => 34154001 [1] => 20130929 [2] => 1168 )
Comment se retrouver les données à la ligne 2, la numérotation du ARRAY n'est pas indiqué, donc si je fais print_r($data[2]); cela indique GLOT1168 !
est ce qu'il y a toujours l'entête (la 1ère ligne) ?
Il y a toujours la première ligne effectivement, mais avec preg_match('/(\d+);(\d+);(\d+)/', $line, $matches); je ne récupère que les chiffres.
quand tu décris la ligne 34154001;20130929;1168, tu veux dire que
- la table s'appel : 34154001
- la date est 20130920
- la valeur est 1168 ?

je ne vois pas bien l’intérêt d'une table avec un numéro pour nom c'est pas logique et incompréhensible.
Je ne décide pas du contenu de ce fichier, néanmoins je peux gérer son intégration, ainsi effectivement le numéro 34154001 est un numéro unique de datalogger.
J'aurais donc plusieurs Dataloggers qui vont déposer leurs données sur ce répertoire, c'est pourquoi j'ai pensé que la base serait par exemple "datalogger" et les tables de differents nom des dataloggers (numéro donc).
Après si il est préférable de tout injecter dans la même table pour des questions de rapidité de traitement et lecture, je ne suis pas contre!
pour continuer ce que Sirakawa a avancé, avec l'extension mysqli ou PDO tu va pouvoir utiliser une requête préparée pour l'insertion ce qui va te permettre d'accélérer un peu la chose.
Je suis en train d'en prendre connaissance, quelle est la plus simple, efficace et répandue des deux?
attention au temps de traitement des fichiers il ne faut pas que le cron lance deux fois le script en parallèle si tu ne prévois pas le cas si non va y avoir des doublons :)
Bonne remarque, comment s'en prémunir autrement que la suppression des fichiers après lecture (parce que sinon, les garder ne me pose pas de souci particulier.

Deja, quelles fonctions vous me recommandez pour ouvrir un répertoire et faire le traitement en lot. Vous me recommandez de traiter les fichiers un par un (j'ouvre, je traite, j'injecte SQL, je supprime), ou un traitement en une seule fois (j'ouvre les fichiers un par un, je traite les fichiers un par un, j'injecte en SQL en une seule fois, je supprime tout le repertoire)?

Merci!
[/list]

Eléphant du PHP | 103 Messages

03 oct. 2013, 09:34

Salut!
Un ptit up!
Merci!!!!

Avatar du membre
Modérateur PHPfrance
Modérateur PHPfrance | 8758 Messages

03 oct. 2013, 20:43

Modération :
Les "up" sont interdits sur PHPFrance.

Si tu n'as pas obtenu de réponse, c'est (au choix) :
- que ta question est mal formulée : reformule-la différemment ;
- que personne ne connaît la réponse ici : faire un "up" ne te donnera pas davantage de résultats ;
- que la réponse demandée exige un travail important que personne ne va faire à ta place ;
- que trop peu de temps s'est écoulé depuis ton précédent message pour qu'un membre ait pu y répondre.

Merci de prendre le temps de lire les règlements.


pour le reste il y a plusieurs solutions.
si tu as plus d'info sur les datalogger, autre le N° de la première colonne (qui est toujours le même ?) tu peux créer une table représentant un datalogger.
par exemple
id int <= le N° de la 1ère colonne
nom
description

dasn tous les cas
et une table "logs" qui va contenir les logs avec une fk vers la première.
cette table comprend les 3 colonnes du fichier

au final le code peu ressembler a cela
<?php
try {
    // connexion
    $pdo = new PDO('mysql:host=localhost;dbname=labase', '', '');
    $stmt = $pdo->prepare('insert into log () values (:poste,str_to_date(:dt,\'Ymd\')),:glot');
    $dir = new DirectoryIterator(dirname(__FILE__) . '/tmpsfolder');
    foreach ($dir as $fileinfo) {
        if (!$fileinfo->isDot() && $fileinfo->isFile() && $fileinfo->getExtension() != 'csv') {
            $tabFile = file($fileinfo->getgetPathname());
            if (count($tabFile) > 0) {
                unset($tabFile[0]);
                foreach ($tabFile as $line) {
                    $csv = str_getcsv($line,';');
                    $stmt->bindValue(':poste',$csv[0], PDO::PARAM_INT);
                    $stmt->bindValue(':dt',$csv[1]);
                    $stmt->bindValue(':glot',$csv[2], PDO::PARAM_INT);
                    $stmt->execute();
                }
            }
        }
    }
} catch (Exception $e) {
    echo $e->getMessage() . '<br />' . $e->getTraceAsString();
}
a tester, attention au temps d’exécution.
il peux être préférable de déplacer le fichier avant de l'utiliser pour éviter qu'il soit utilisé par deux exécution concurrente ;)

@+
Il en faut peu pour être heureux ......

Eléphant du PHP | 103 Messages

03 oct. 2013, 21:32

Modération :
Les "up" sont interdits sur PHPFrance.

Désolé!

Ouaou, ce code me semble excellemment propre mais bien au dessus de mes compétences !

si tu as plus d'info sur les datalogger, autre le N° de la première colonne (qui est toujours le même ?) tu peux créer une table représentant un datalogger.
par exemple
id int <= le N° de la 1ère colonne
nom
description

Effectivement, la première ligne sera toujours le même numéro, comme le nom du fichier sous cette forme "fic34154001-2013-10-01", on pourrais tout à fait se contenter de la date et de la valeur dans une table portant le nom du datalogger, en l’occurrence ici une table nommée 34154001.

Code : Tout sélectionner

$pdo = new PDO('mysql:host=localhost;dbname=labase', '', '');
Je suppose qu'il s'agit de ('mysql:host=127.0.0.1;dbname=labase', 'username', 'password')

Code : Tout sélectionner

$stmt = $pdo->prepare('insert into log () values (:poste,str_to_date(:dt,\'Ymd\')),:glot');
Dans base "labase" intégration dans table "logs"

Code : Tout sélectionner

$dir = new DirectoryIterator(dirname(__FILE__) . '/tmpsfolder');
dossier dans lequel se trouve les fichiers .DATA

Code : Tout sélectionner

if (!$fileinfo->isDot() && $fileinfo->isFile() && $fileinfo->getExtension() != 'csv') {
Type de fichier à rechercher? Dans mon cas ce n'est pas "csv" mais "data.

Rectification de ton erreur getgetPathname() en getPathname() et création de la base "labase" dans MYSQL et table "logs" avec 3 colonnes (poste/dt/glot en INT), le code suivant tourne mais laisse une page blanche sans aucun message d'erreur sans que cela n'injecte quoique ce soit dans MYSQL :

Code : Tout sélectionner

<?php try { // connexion $pdo = new PDO('mysql:host=127.0.0.1;dbname=labase', 'root', ''); $stmt = $pdo->prepare('insert into logs () values (:poste,str_to_date(:dt,\'Ymd\')),:glot'); $dir = new DirectoryIterator(dirname(__FILE__) . '/tmpsfolder'); foreach ($dir as $fileinfo) { if (!$fileinfo->isDot() && $fileinfo->isFile() && $fileinfo->getExtension() != 'data') { $tabFile = file($fileinfo->getPathname()); if (count($tabFile) > 0) { unset($tabFile[0]); foreach ($tabFile as $line) { $csv = str_getcsv($line,';'); $stmt->bindValue(':poste',$csv[0], PDO::PARAM_INT); $stmt->bindValue(':dt',$csv[1]); $stmt->bindValue(':glot',$csv[2], PDO::PARAM_INT); $stmt->execute(); } } } } } catch (Exception $e) { echo $e->getMessage() . '<br />' . $e->getTraceAsString(); } ?>
Désolé si je fais des erreurs grossières, j'éditerais le post en fonction de mes avancées!

Merci!!

Avatar du membre
Modérateur PHPfrance
Modérateur PHPfrance | 8758 Messages

04 oct. 2013, 07:03

Créer une table différentes par logger pourquoi mais ce n'est pas forcément utile et surtout le numéro n'est pas parlant du tous.
Mais il est vrai que tu pex tes bien lister les numéros de logger et repartir dans plusieurs tables mais côté modèle c'est moyen.

Pour le reste tu as presque tous bon ;)

La méthode prépare permet de precompiler la requête sur le serveur et ensuite on ne fai que l'exécuter en lui fournissant les paramètres.
L'avantage c'est un gain de performance indéniable presque l'on utilise une requête dans une boucle (comme ici).

Pour le reste effectivement le code n'affiche rien ;)
Vu que c'est pour un bat y ce n'est pas trop grave.
Tu peu aussi remplacer l'affichage dans le catch par l'insertion de l'erreur dans un log.

La méthode execute execute la requête préparée avec les info fournit par les bindValue

Dans ton cas je suppose que tu n'as pas de répertoire tmpsfolder a la racine du script (au même niveau) avec les fichier data dedans ?
L'emplacement du répertoire n'est pas important il simplement que puisse lire ce qu'il y a dedans.
La partie avec la constante __FILE__ c'est pour dit de retourner le répertoire (path complet) dans lequel se trouve le fichier courant.

@+
Il en faut peu pour être heureux ......

Eléphant du PHP | 103 Messages

04 oct. 2013, 11:36

Créer une table différentes par logger pourquoi mais ce n'est pas forcément utile et surtout le numéro n'est pas parlant du tous.
Mais il est vrai que tu pex tes bien lister les numéros de logger et repartir dans plusieurs tables mais côté modèle c'est moyen.
Après réflexion, c'est vraiment ce qu'il me faut, une base "dataloggers" avec diverses tables ayants pour nom les identifiants comme "34154001" (tordu peut être mais bien plus simple pour moi!) avec pour données 2 colonnes uniquement "Date" et "Valeur".
Pour le reste effectivement le code n'affiche rien ;)
Vu que c'est pour un bat y ce n'est pas trop grave.
Tu peu aussi remplacer l'affichage dans le catch par l'insertion de l'erreur dans un log.
Les erreurs ne s'affichent que pour les soucis MySQL, mais la je ne comprends pas, page blanche (donc pas d'erreur MySQL) mais rien dans la base :
Soit il ne trouve pas les fichiers avec DirectoryIterator(dirname(__FILE__) . '\tmpsfolder') pourtant le scipt se trouve dans un dossier ou il y a le dossier tmpsfolder avec les fichiers .data - PEU PROBABLE SELON MES TESTS.
Soit il ne peut insérer dans la table, et d'ailleurs j'ai essayé de supprimer la table, le script affiche toujours page blanche! Je ne comprends pas le Catch n'affiche rien!

Comment afficher toutes les erreurs (EasyPHP est réglé par défaut pour afficher toutes les erreurs) afin de résoudre le souci?

Merci!

Avatar du membre
Modérateur PHPfrance
Modérateur PHPfrance | 8758 Messages

05 oct. 2013, 01:39

une version fonctionnelle
<?php
try {
    // connexion
    $pdo = new PDO('mysql:host=localhost;dbname=josse34', 'josse34', 'josse34');
    $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
    $pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_OBJ);
    $pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
    $pdo->setAttribute(\PDO::ATTR_CASE, \PDO::CASE_LOWER);

    $dir = new DirectoryIterator(dirname(__FILE__) . '/tmpsfolder');
    foreach ($dir as $fileinfo) {
        if (!$fileinfo->isDot() && $fileinfo->isFile() && $fileinfo->getExtension() == 'data') {
            echo 'parse du fichier : '.$fileinfo->getFilename().'<br />';
            $tabFile = file($fileinfo->getPathname());
            preg_match('/^fic(\d+)-(\d+)-(\d+)-(\d+).data$/',$fileinfo->getFilename(),$match);
            $table = $match[1];
            $stmt = $pdo->prepare('INSERT INTO `'.$table.'` (poste,dtlog, glot) VALUES (:poste,str_to_date(:dt,\'%Y%m%d\'),:glot)');
            if (count($tabFile) > 0) {
                unset($tabFile[0]);
                $i = 0;
                foreach ($tabFile as $line) {
                    $csv = str_getcsv($line, ';');
                    $stmt->bindValue(':poste', $csv[0], PDO::PARAM_INT);
                    $stmt->bindValue(':dt', $csv[1]);
                    $stmt->bindValue(':glot', $csv[2], PDO::PARAM_INT);
                    $stmt->execute();
                    echo 'insertion de la ligne '.$i.'<br />';
                    $i++;
                }
            }
        }
    }
} catch (Exception $e) {
    echo $e->getMessage() . '<br />' . $e->getTraceAsString();
}
@+
Il en faut peu pour être heureux ......

Eléphant du PHP | 103 Messages

05 oct. 2013, 19:39

Ce n'est pas une version fonctionnelle, c'est une version MERVEILLEUSE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! =D>

Incroyable, pour toi cela semble naturel mais pour obtenir le même résultat, j'aurais pondu un code 4 fois plus long, moche et lent en 3 semaines! Grand merci!

Je risque d'avoir jusqu'à 600 fichiers à traiter, ça passe sans aucun souci?!
il peux être préférable de déplacer le fichier avant de l'utiliser pour éviter qu'il soit utilisé par deux exécution concurrente
Vu que ces fichiers ne prennent que extrêmement peu de place et qu'il peut être pratique de les avoir sous le coude en cas de souci pour les réinjecter dans le traitement, comment déplacer le fichier lu durant le traitement (dossier temp par exemple) et le déplacer une seconde fois dans un dossier type archive?

Oh, et puis je vais en demander surement un peu beaucoup mais est il possible de tester la table avant pour voir si une ligne avec la même date existe déjà et dans ce cas remplacer la valeur?!

Merci!
:D

Avatar du membre
Modérateur PHPfrance
Modérateur PHPfrance | 8758 Messages

06 oct. 2013, 13:23

Yop,

Pour le déplacement il faut utiliser la fonction rename
C'est la première chose a faire dans le 1er foreach (juste après le if) et ensuite il faut utiliser le fichier temporaire ;)

Et ensuite pareil mais du temporaire vers l'archive.

Pour le volume ce n'est pas forcément un problème, ce qui peux en être un c'est la taille du fichier.
Avec ce système tu charge le fichier entièrement en mémoire et tu est limité en mémoire utilisable par ton script (memory limit dans un phpinfo) et si tu dépasse cette taille il faudra changer ton fusil d'épaule et le lire au fur et a mesure comme tu l'as fait au début ;)

@+
Il en faut peu pour être heureux ......

Eléphant du PHP | 103 Messages

06 oct. 2013, 18:55

Slu!
Pour le déplacement il faut utiliser la fonction rename
C'est la première chose a faire dans le 1er foreach (juste après le if) et ensuite il faut utiliser le fichier temporaire ;)
Pour ce qui est du déplacement du dossier "AVANT" à "PENDANT" durant le traitement, si je fais cela :
$dir = new DirectoryIterator(dirname(__FILE__) . '\AVANT');  
    foreach ($dir as $fileinfo) {
        if (!$fileinfo->isDot() && $fileinfo->isFile() && $fileinfo->getExtension() == 'data') {
            rename("./AVANT/$fileinfo", "./PENDANT/$fileinfo");
            echo 'parse du fichier : '.$fileinfo->getFilename().'<br />';
            $tabFile = file($fileinfo->getPathname());
            preg_match('/^fic(\d+)-(\d+)-(\d+)-(\d+).data$/',$fileinfo->getFilename(),$match);
            $table = $match[1];
            $stmt = $pdo->prepare('INSERT INTO `'.$table.'` (poste,dtlog, glot) VALUES (:poste,str_to_date(:dt,\'%Y%m%d\'),:glot)');
Ca déplace le fichier mais il ne le trouve plus pour le FOREACH et du coup, ne le traite pas SQL ni le déplace de "PENDANT" à "APRES" via rename("./PENDANT/$fileinfo", "./APRES/$fileinfo"); !!!
Warning: file(C:\Program Files (x86)\EasyPHP-DevServer-13.1VC11\data\localweb\scripts\AVANT\fic34154001-2013-10-03.data): failed to open stream: No such file or directory in C:\Program Files (x86)\EasyPHP-DevServer-13.1VC11\data\localweb\scripts\testparserdata4.php on line 15
Je ne parviens pas à utiliser le fichier déplacé.

Un fichier représente 40,3 octets en moyenne, soit un maxima théorique durant de traitement de 600 fichiers * 40,3 octets = 25Ko, largement soutenable!!!

Oh, et puis est il possible de tester la table avant pour voir si une ligne avec la même date existe déjà et dans ce cas remplacer la valeur?!?!?!?!
Peut etre un truc du genre à intercaler avant le $stmt->execute(); d'injection :
$count = $dbh->prepare('SELECT COUNT(*) FROM `'.$table.'` WHERE dt = :dt');
$count->bindValue('dt', $_POST['dt'], PDO::PARAM_STR);
$count->execute();
if ($count->fetchColumn()) {
    //ON METS A JOUR LA VALEUR, MAIS COMMENT???
} else {
   //ON INJECTE NORMALEMENT CAR LA LIGNE N'EXISTE PAS, avec $stmt->execute();
}
Merci!!