preg_replace : plusieurs remplacements dans une ligne

Mammouth du PHP | 19672 Messages

07 juil. 2006, 11:21

Salut tout le monde,
j'ai une difficulté à construire une expression régulière (PCRE).

Je pars de la chaine suivante :

Code : Tout sélectionner

FR 10 1 3823066 3823067 EUR 0.00 0.00 19.60 0 1 0 0
La même humainement lisible donnerait :

Code : Tout sélectionner

FR\t10\t1\t3823066\t3823067\tEUR\t0.00\t0.00\t19.60\t\t\t0\t1\t0\t0\n
Je veux obtenir :

Code : Tout sélectionner

FR\t10\t1\t3823066\t3823067\tEUR\t0.00\t0.00\t19.60\tNULL\tNULL\t0\t1\t0\t0\n
En clair, insérer "NULL" entre deux tabulations consécutives. Je n'arrive à en insérer qu'un seul. Je suis parti du masque basique suivant :

Code : Tout sélectionner

#(\t)(\t)#
en replaçant par :

Code : Tout sélectionner

$1NULL$2
J'ai essayé bien des variantes sans succès : une idée ?
Codez en pensant que celui qui maintiendra votre code est un psychopathe qui connait votre adresse :axe:

Mammouth du PHP | 1311 Messages

07 juil. 2006, 11:47

j'ai essayé ceci sous regcoach
(?<=\\t)(?=\\t)
ca a l'air de fonctionné avec l'option g

Mammouth du PHP | 19672 Messages

07 juil. 2006, 11:53

Génial, ça roule :)

Par contre, l'option "g" ne semble pas exister, ça me génère une erreur.
Codez en pensant que celui qui maintiendra votre code est un psychopathe qui connait votre adresse :axe:

Mammouth du PHP | 1311 Messages

07 juil. 2006, 12:36

pour l'option g, je parlait d'une option de regcoach

Mammouth du PHP | 983 Messages

07 juil. 2006, 13:40

Pourquoi ne pas utiliser strtr() , qui est beaucoup plus rapide qu'une PCRE ?

Il suffirait de remplacer "\t\t" par "\tNULL\t"

ViPHP
ViPHP | 1380 Messages

07 juil. 2006, 14:19

Cyrano, y-a-t'il un lien avec ton post sur l'insertion d'un très gros fichier dans MySQL avec LOAD DATA INFILE?

Si oui, deux remarques:
  • Si tu veux forcer MySQL à insérer un [NULL], tu dois plutôt mettre \N comme valeur de champ dans le fichier plat. Par exemple:

    Code : Tout sélectionner

    FR 10 1 0.00 0.00 19.60 \N 0 1 0 0
    When reading data with LOAD DATA INFILE, empty or missing columns are updated with ''. If you want a NULL value in a column, you should use \N in the data file. The literal word “NULL” may also be used under some circumstances. See Section 13.2.5, “LOAD DATA INFILE Syntax”.
    http://dev.mysql.com/doc/refman/5.1/en/ ... -null.html
  • Ensuite, que feras-tu pour une ligne se terminant par \t? Tu devras dans ce cas soit faire deux passes avec str_replace ou une seule avec preg_replace mais....
  • Si tu dois traiter un gros fichier plat, il vaudrait mieux utiliser les outils Linux. Je pense avoir vu dans ton post cité plus haut que tes chemins de fichiers sont des chemins Linux. Regarde du côté de sed. A mon avis, beaucoup plus rapide que le traitement en PHP pour de très gros fichiers. Exemple d'une solution sed one-liner:

    Code : Tout sélectionner

    sed -r 's/\t(\t|$)/\t\\N\1/g' cyrano.txt >> cyrano.new
    sed utilise la syntaxe des regex (POSIX étendue). s/masque/valeur_de_remplacement/option (ici g pour remplacer toutes les occurrences). Si ton fichier est vraiment énooorme, considerer l'utilisation de awk, sans doute plus rapide (pas le même moteur regex).
ripat

Mammouth du PHP | 19672 Messages

07 juil. 2006, 14:49

Il y a effectivement un lien direct avec mon précédent post. J'avais également noté à propos de '\N' plustôt que 'NULL', en fait, ce qui m'intéressait était surtout l'insertion entre toutes les paires de tabulations consécutives.

Chacune de mes lignes se termine de toutes façons par une tabulation, donc une donnée absente dans la dernière colonne donnnera lieu à deux tabulations consécutives, donc pas de soucis de ce coté là.

En fait ce qui s'est passé, c'est que les fichiers tels que je les récupère sont formatés de façon très basique ce qui me pose des problème pour les injecter dans une base MySQL. Le chargement dans une base de transit où tous els champs sont de type CHAR ou VARCHAR fonctionne très bien, mais là où la donnée est absente, mysql m'insère une chaine vide et non 'NULL' comme je l'espérais : je pensais pouvoir très rapidement refaire une exportation avec les délimiteurs qui vont bien, partant de MySQL vers MySQL, ça aurait dû marcher : Nada, niet ! Je dois donc trouver une autre méthode : je m'oriente donc vers une ré-écriture des fichiers pour une injection directe dans une base ayant des champs convenablement typés.

Pour l'environnement, je suis sous Windows XP Pro, et mon principal problème, c'est un fichier. J'en reçois 31 dont un de 660Mo, les autres allant de 1Ko à 31Mo : ces derniers ne posent pas de problèmes de traitement, mais le gros est plsu rétif. Je n'ai pas d'environnement Linux sous la main (ou alors chez moi sur ma machine perso).

Reste aussi peut-être l'exécution via PHP en ligne de commande rendant le script indépendant du serveur Apache, je cherche en même temps comment faire ça parce que l'insertion d'un '\N' fonctionne bien, sauf sur le gros fichier qui ne dispose pas du délai suffisant, il doit y avoir un time out sur Apache que j'ai pas réglé.
Codez en pensant que celui qui maintiendra votre code est un psychopathe qui connait votre adresse :axe:

ViPHP
ViPHP | 1380 Messages

07 juil. 2006, 15:56

Dans ce cas, je ne vois que sed ou awk. Je pense qu'il existe des binaires pour Windows (pas testé)
http://gnuwin32.sourceforge.net/packages/sed.htm

Pour awk: http://gnuwin32.sourceforge.net/packages/gawk.htm

Je réfléchis à la solution pour (g)awk qui sera, je pense, plus rapide.

Ces deux utilitaires sont vraiment faits pour ce genre de travail.
ripat

Mammouth du PHP | 19672 Messages

07 juil. 2006, 16:10

Je vais fouiller, merci Ripat :)
Codez en pensant que celui qui maintiendra votre code est un psychopathe qui connait votre adresse :axe:

ViPHP
ViPHP | 1380 Messages

07 juil. 2006, 17:54

Tu m'as donné l'occasion de lever un doute sur les différences de perf entre sed et awk. J'ai construit un gros fichier comparable à ta structure sur 7 col:

Code : Tout sélectionner

1989-02-06 002583 M0581/87 890377 36 97.99 0
Séparateur \t

Taille: 16,2 millions de lignes (760,5 Mo)

But: remplacer:
les doubles \t\t par \t\N\t
et \t\n par \t\N\n
(\n = fin de ligne, \N chaîne spéciale pour MySQL).

Résultat sur ma linux box:
  • sed -r 's/\t(\t|$)/\t\\N\1/g' gros_fichier > out

    1 min 24'
  • awk -v FS="\n" '{print gensub(/\t(\t|$)/, "\t\\N\\1", "g", $0)}' gros_fichier > out

    1 min 10'
Je m'attendais franchement à une plus grande différence entre sed et awk sachant que awk utilise un moteur regex qui, en théorie, devrait être plus rapide. Comme quoi, entre la théorie et la pratique...

Mais tout de même, un peu plus d'une minute pour passer à la moulinette un fichier de cette taille.... Pas mal hein?

Qui a le courage (et la patience) pour essayer de faire ça dans PHP pour voir?

Je donne la regex:
$result = preg_replace('#\t(\t|$)#m', "\t\N\\1", $result);
mais pour traiter un fichier de 760 Mo, je préfère ne pas essayer moi même :wink:
Modifié en dernier par Ripat le 07 juil. 2006, 17:57, modifié 1 fois.
ripat

Mammouth du PHP | 19672 Messages

07 juil. 2006, 17:55

Bon, ben j'ai récupéré sed et gawk... et je capte pas comment on s'en sert ni même à quoi ça sert... :?

En parcourant la doc, je constate qu'on peut faire beaucoup de chose, mais je veux pas tout ça, je veux juste insérer une chaine de caractère à un endroit précis dans un fichier. C'est mal barré, je sens que je vais essayer de trouver une autre astuce, mais le traitement sera plus long. Ceci dit, l'idée pour l'instant, c'est d'insérer mes données brutes dans des champs CHAR/VARCHAR, ensuite lancer en ligne de commande des requêtes de mise à jour pour remplacer les chaînes vides par NULL et ensuite, faire un export normal pour avoir les fichiers correctement formatés pour la base définitive.... sacré merdier si je peux me permettre :-k

[Edit]J'ai trouvé un bout de doc en français, j'ai donc testé en copiant/collant une bête ligne de commande après avoir simplement replacé les noms de fichiers indiqués par les miens : pourquoi ne suis-je donc pas surpris que ça plante ? :evil:
La ligne indiquée :

Code : Tout sélectionner

gawk -F'\t' '{OFS="\t"; print $1,$2,$8}' Graphemes.txt >mybase.txt
Je fais la même chose et voilà la réponse :

Code : Tout sélectionner

gawk: cmd. line:1: '{OFS=\t; gawk: cmd. line:1: ^ invalid char ''' in expression
Si quelqu'un connait ce soft, toute aide sera bienvenue. Et du coup, je vire le [Résolu], il n'a plus vraiment lieu d'être...[/Edit]
Codez en pensant que celui qui maintiendra votre code est un psychopathe qui connait votre adresse :axe:

ViPHP
ViPHP | 1380 Messages

07 juil. 2006, 18:27

Et que donne:

Code : Tout sélectionner

awk -v FS="\n" '{print gensub(/\t(\t|$)/, "\t\\\\N\\1", "g", $0)}' gros_fichier > out
Edit:

awk et sed sont vraiment efficaces pour un traitement de fichier. Awk est un peu plus verbeux mais permet beaucoup. Le tuto de référence est: http://www.tldp.org/LDP/abs/html/index.html un vrai livre on line.

sed et awk pratiquent un traitement ligne par ligne. awk permet en plus d'éclater une ligne par champ (explode de PHP). Chaque colonne est placée dans les variables $1 $2 etc... (toute la ligne dans $0).

Mon one-liner plus haut fait ceci:

-v FS ="\n" ---> on fixe la valeur de la variable d'environnement FS (Field Separator) à la fin de ligne. Ce qui revient à dire un seul champ par ligne pour le traitement qui suit. Et ce champ se trouve dans $0 (toute la ligne pour awk). En éditant ce post, je me rends compte que le FS="\n" est superflu. On pourrait simplement faire:

Code : Tout sélectionner

awk '{print gensub(/\t(\t|$)/, "\t\\\\N\\1", "g")}' gros_fichier > out
les accolades marquent le début du code awk proprement dit.

print --> ben, affiche le résultat sur le stdout (le output standard - écran)

gensub(masque, remplacement, scope, source) ici j'ai mis le scope à "g" (la même option en PERL qui veut dire toutes les occurrences - comme preg_match_all). Le masque devrait te sembler famillier, c'est comme en PCRE de PHP. On essaye de matcher soit \t\t, soit \t\n ($ fin de ligne) en capturant la fin (\t ou $). Le remplacement: \t suivi de \N (pour MySQL) suivi de la capture du masque (\t ou \n).

> out redirection de stdout vers un fichier. Tout ce qui venait à l'écran est inscrit dans un fichier de résultat.

Voilà, le tout en une seule ligne!

Edit 2: Manquait deux échappements pour le \N
Modifié en dernier par Ripat le 09 juil. 2006, 20:45, modifié 1 fois.
ripat

Mammouth du PHP | 19672 Messages

07 juil. 2006, 19:58

Merci mille fos Ripat pour le support, je testerai ça lundi matin, là, je n'ai pas les éléments chez moi pour tester, si je trouve quelques minutes, je testerai demain en téléchargeant ce qu'il faut. Je te dirai ce qu'il en est. :)
Codez en pensant que celui qui maintiendra votre code est un psychopathe qui connait votre adresse :axe:

Mammouth du PHP | 19672 Messages

12 juil. 2006, 20:56

Bon, la solution pour ceux que ça intéresse.

D'abord, il faut des outils. Manipuler des gros fichiers (mesurés en dizaines ou en centaines de Mo) ça ne se fait pas au bloc-note. Donc on a deux possibilités. Utiliser SED ou GAWK. Ces outils s'utilisent en ligne de commande. On trouve ces outils sur Sourceforge pour Windows. Pour linux, sachez que ces outils sont généralement déjà installé et directement utilisables dans une console.

Pour mémoire : le fichier à manipuler est un dump de base de données. Les champs sont séparés par des tabulations et les lignes se terminent par un retour de chariot.
L'importation dans une base de données MySQL pose un problème avec certains champs typés lorsque la valeur est absente. Le problème consiste donc à insérer un "\N" entre deux tabulations consécutives. (Ne pas confondre "\N" et "\n" : le premier a une signification bien précise pour MySQL et sera lu comme "NULL" et non comme un retour de chariot)
Voici quatre lignes exemple :

Code : Tout sélectionner

FR 10 1 3823066 3823067 EUR 0.00 0.00 19.60 0 1 0 0 FR 10 2 4265487 4265488 EUR 0.00 0.00 19.60 0 1 0 0 FR 10 3 7677524 7677525 EUR 0.00 0.00 19.60 0 1 0 0 FR 10 4 8296231 8296232 EUR 0.00 0.00 19.60 0 1 0 0
Après avoir installé SED et GAWK, ouvrez une invite de commande Windows (ou une console Linux, ça doit fonctionner aussi). Sous Windows, déplacez vous dans le répertoire bin là où est installé GAWK :

Code : Tout sélectionner

cd C:\Program Files\GnuWin32\bin\
Ensuite, tapez la ligne suivante dans l'invite de commande:

Code : Tout sélectionner

gawk "{ligne=gensub(/\t(\t|$)/, \"\t\\\\N\\1\", \"g\", $0); print gensub(/\t(\t|$)/, \"\t\\\\N\\1\", \"g\", ligne)}" nom_fichier_entree.txt > nom_fichier_sortie.txt
Là où sont les noms des fichiers, vous devez pointer dessus donc en mettant le chemin complet. ce qui signifie par exemple qu'au lieu de "nom_fichier_entree.txt", il faudra inscrire "C:\Temp\fichiers\nom_fichier_entree.txt" et pareil pour le fichier de sortie.

Résultat :

Code : Tout sélectionner

FR 10 1 3823066 3823067 EUR 0.00 0.00 19.60 \N \N 0 1 0 0 \N FR 10 2 4265487 4265488 EUR 0.00 0.00 19.60 \N \N 0 1 0 0 \N FR 10 3 7677524 7677525 EUR 0.00 0.00 19.60 \N \N 0 1 0 0 \N FR 10 4 8296231 8296232 EUR 0.00 0.00 19.60 \N \N 0 1 0 0 \N
Ceci est un extrait d'un fichier qui fait 32Mo, la conversion a pris moins d'une minute. J'ai testé ensuite avec mon ficier de 660Mo : durée de la transformation de 10mn10' : Chapeau et encore merci à Ripat pour son très précieux support :)
Codez en pensant que celui qui maintiendra votre code est un psychopathe qui connait votre adresse :axe:

didou70
Invité n'ayant pas de compte PHPfrance

31 juil. 2006, 12:12

Code : Tout sélectionner

gawk "{ligne=gensub(/\t(\t|$)/, \"\t\\\\N\\1\", \"g\", $0); print gensub(/\t(\t|$)/, \"\t\\\\N\\1\", \"g\", ligne)}" nom_fichier_entree.txt > nom_fichier_sortie.txt
Merci pour cet exemple assez explicite !

Je suis novice, je m'intéresse à gawk pour transcrire un fichier txt
mon problème est le suivant !
Je veux ajouter un ; à la position 4 de toutes les lignes de mon fishier txt
Cette position n'a pas de caractéristique spéciale, donc pas moyen de rechercher un chaîne de caractère.

J'ai pas mal tourné dans les forums, mais je n'ai sans doûte pas eu de chance.

Ma question est simple, est-ce possible de faire cette manipulation avec gawk.