Aller au menu - Aller au contenu

Icône Les expressions régulières (Partie 2/2)

Mise à jour : 03/04/2010
Difficulté : Difficile Difficile Creative Commons BY-NC-SA
165 249 visites depuis 7 jours, dont 2 775 sur ce chapitre, classé 3/795
Voici donc la suite (et fin) de notre aventure avec les expressions régulières :)
Le mot d'ordre de ce chapitre est : pratiquer. Hormis quelques points que nous allons aborder au début, vous connaissez le principal sur les regex mais il vous manque le plus important : la pratique !

Dans la seconde moitié de ce chapitre, nous allons donc construire ensemble des Regex, pour que vous voyiez comment il faut procéder pour arriver enfin à écrire cette $%@#$#% de Regex ! :lol:
Ecrire un bout de Regex comme on l'a fait jusqu'ici, c'est une chose, mais créer une Regex complète vous allez voir que c'est une toute autre paire de manches ! ^^
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

Une histoire de métacaractères

Pour commencer, et avant d'aller plus loin, il me semble important d'ajouter un nouveau mot à votre vocabulaire : métacaractères.
Ce n'est pas une insulte de programmeur, mais un mot qui signifie tout simplement "caractères spéciaux". Ce sont des caractères pas comme les autres qui ont un rôle ou un sens particulier.

Alerte mon Général ! Les métacaractères s'échappent !



Dans le langage PCRE (des Regex), les métacaractères qu'il faut connaître sont les suivants :

# ! ^ $ ( ) [ ] { } ? + * . \ |


Il faut bien les retenir. Pour la plupart d'entre eux, vous les connaissez déjà.
Ainsi, le dollar "$" est un caractère spécial parce qu'il permet d'indiquer une fin de chaîne.
De même pour l'accent circonflexe, le dièse, les parenthèses, les crochets, les accolades et les symboles "? + *" : nous les avons tous utilisés dans le chapitre précédent, souvenez-vous.
Pour le point "." et l'antislash "\", vous ne les connaissez pas mais vous n'allez pas tarder à les apprendre.

Bon, ce sont des caractères spéciaux et chacun d'eux signifie quelque chose de précis. Et alors ?


Et alors, le problème vous tombe dessus le jour où vous voulez chercher par exemple "Quoi ?" dans une chaîne.
Comment écririez-vous la Regex ? Comme ça ?

#Quoi ?#

Eh non surtout pas ! Le point d'interrogation, vous le savez, sert à dire que la lettre juste avant est facultative (elle peut apparaître 0 ou 1 fois). Ici, l'espace devant le point d'interrogation serait donc facultatif, mais ce n'est pas ce qu'on veut faire !

Alors, comment faire pour faire comprendre qu'on recherche "Quoi ?" alors que le point d'interrogation a déjà une signification ?
Il va falloir l'échapper. Cela signifie que vous devez placer en fait un antislash "\" devant un caractère spécial. Ainsi, la bonne regex serait :

#Quoi \?#

Ici, l'antislash sert à dire que le point d'interrogation juste après n'est pas un symbole spécial, mais bel et bien une lettre comme une autre !

C'est la même chose pour tous les autres métacaractères que je vous ai montrés plus haut (# ! ^ $ ( ) [ ] { } ? + * . \) : il faut mettre un antislash devant si vous voulez les utiliser dans votre recherche.
Vous remarquerez que pour utiliser un antislash il faut... un antislash devant ! Comme ceci : \\.


Bien tordu tout ça non ?
Pourtant, ce que vous devez retenir est simple : si vous voulez utiliser un caractère spécial dans votre recherche, il faut placer un antislash devant. Point barre.
Je vous donne quelques exemples d'utilisation, ça devrait bien vous faire rentrer ça dans la tête :

Chaîne Regex Résultat
Je suis impatient ! #impatient \!# VRAI
Je suis (très) fatigué #\(très\) fatigué# VRAI
J'ai sommeil... #sommeil\.\.\.# VRAI
Le smiley :-\ #:-\\# VRAI



Le clas cas des classes



Vous m'excuserez si j'ai dérapé sur ce sous-titre, mais je vous mets au défi quand même d'arriver à dire 10 fois très rapidement "Le cas des classes" sans erreur :p

De quoi parlait-on déjà ? :euh:
Ah oui, les expressions régulières c'est vrai. ^^
Bon, si je vous ennuie un peu là (je vous comprends), il faudra m'excuser mais je n'ai pas le choix. Il est obligatoire que vous sachiez ce genre de choses si vous voulez vraiment utiliser les Regex.

Il reste une dernière petite chose à voir (encore un cas particulier) : c'est à propos des classes de caractères.
Jusqu'ici, vous avez mis des lettres et des chiffres entre les crochets, par exemple :
#[a-z0-9]#
Oui mais, vous vous en doutez, vous avez le droit de mettre d'autres caractères, comme les accents (mais dans ce cas il faut les énumérer un à un). Par exemple : [a-zéèàêâùïüë] etc...

Jusque-là, tout va bien. Mais si vous voulez lister aussi des caractères spéciaux mmh ? Par exemple un point d'interrogation (au hasard :p). Eh bien là, ça ne compte pas ! Pas besoin de l'échapper, à l'intérieur de crochets les métacaractères... ne comptent plus !
Ainsi, cette regex marche très bien :
#[a-z?+*{}]#
Elle signifie qu'on a le droit de mettre une lettre, un point d'interrogation, un signe + etc...

3 cas particuliers cependant :

  • "#" (dièse) : il sert toujours à indiquer la fin de la Regex. Vous DEVEZ mettre un antislash devant même dans une classe de caractères pour l'utiliser.
  • "]" (crochet fermant) : normalement, le crochet fermant indique la fin de la classe. Si vous voulez vous en servir comme d'un caractère que vous recherchez, il faut là aussi mettre un antislash devant.
  • "-" (tiret) : encore un cas un peu particulier. Le tiret, vous le savez, sert à définir un intervalle de classe (comme [a-z]). Et si vous voulez ajouter le tiret dans la liste des caractères possibles ? Eh bien il suffit de le mettre soit au début de la classe, soit à la fin. Par exemple : [a-z0-9-] permet de chercher une lettre, un chiffre ou un tiret.

Les classes abrégées

La bonne nouvelle, c'est que vous êtes maintenant prêts à faire quasiment toutes les Regex que vous voulez. :)
La mauvaise, c'est que je viens de dire "quasiment". :p

Oh rassurez-vous, ça ne sera pas long et vous ne sentirez aucune douleur (à ce stade, on ne ressent plus la douleur de toute façon).
Je souhaite juste vous montrer ce qu'on appelle les classes abrégées, et que moi j'appelle les raccourcis (ce mot me parle un peu plus).

Certains de ces raccourcis ne vous seront pas indispensables, mais comme vous risquez de les rencontrer un jour ou l'autre, je ne voudrais pas que vous soyiez surpris et que vous croyiez que je vous ai caché des choses. ;)

Voici ce qu'il faut retenir :

Raccourci Signification
\d Indique un chiffre. Ca revient exactement à taper [0-9]
\D Indique ce qui n'est PAS un chiffre. Ca revient à taper [^0-9]
\w Indique un caractère alphanumérique ou un tiret de soulignement. Cela correspond à taper [a-zA-Z0-9_]
\W Indique ce qui n'est PAS un caractère alphanumérique ou un tiret de soulignement. Ca revient à taper [^a-zA-Z0-9_]
\t Indique une tabulation
\n Indique une nouvelle ligne
\r Indique un retour chariot
\s Indique un espace blanc (correspond à \t \n \r)
\S Indique ce qui n'est PAS un espace blanc (\t \n \r)
. Le point indique n'importe quel caractère ! Il autorise donc tout !


Il s'agit de lettres normales, mais quand on place un antislash devant, on leur donne une signification spéciale.
C'est l'inverse de ce qu'on faisait tout à l'heure : on utilisait un antislash devant les métacaractères pour leur enlever leur signification spéciale.

Pour le point, il existe une exception : il indique tout sauf les Entrées (\n).
Pour faire en sorte que le point indique tout, même les entrées, vous devrez utiliser l'option "s" de PCRE. Exemple :
#[0-9]-.#s


Allez, cette fois vous en savez assez, on va pouvoir passer à la pratique ! :D

Construire une Regex complète

Vous allez enfin comprendre pourquoi vous en avez bavé tout le long ! :p
Cette fois, nous allons toucher du concret à travers des exemples qui vous seront sûrement utiles. Nous allons construire de grosses Regex ensemble, pour que vous compreniez la méthode. Ensuite, vous serez tout à fait capable d'inventer vos Regex et de vous en servir pour vos scripts PHP ! :)

Un numéro de téléphone



Pour cette première vraie Regex, nous allons essayer de voir si une variable (rentrée par un visiteur via un formulaire par exemple) correspond bien à un numéro de téléphone.
Je vais me baser sur les numéros de téléphone français, alors il faudra m'excuser si vous n'êtes pas français et que vous ne connaissez pas. L'avantage, c'est que vous pourrez ensuite vous exercer à écrire cette Regex pour les numéros de téléphone de votre pays. ;)

Pour rappel (et pour ceux qui ne savent pas donc), un numéro de téléphone français comporte 10 chiffres. Par exemple : "01 53 78 99 99". Il faut respecter les règles suivantes :

  • Le premier chiffre est TOUJOURS un 0
  • Le second chiffre va de 1 à 9 (1 pour la région parisienne... 6 et 7 pour les téléphones portables, 8 pour les numéros spéciaux, et 9 pour les numéros VOIP).
  • Ensuite viennent les 8 chiffres restants (ils peuvent aller de 0 à 9 sans problème)


Pour commencer, et pour faire simple, on va supposer que l'utilisateur rentre le numéro de téléphone sans mettre d'espace ni quoi que ce soit (mais on complique juste après, et vous verrez que c'est là le véritable intérêt des Regex).
Ainsi, le numéro de téléphone doit ressembler à ça : "0153789999". Comment écrire une Regex qui corresponde à un numéro de téléphone comme celui-ci ?

Voici comment je procède, dans l'ordre, pour construire cette Regex :
  1. Primo, on veut qu'il y ait UNIQUEMENT le numéro de téléphone. On va donc commencer par mettre les symboles ^ et $ pour indiquer un début et une fin de chaîne :
    #^$#
  2. Continuons. On sait que le premier caractère est forcément un 0. On tape donc :
    #^0$#
  3. Le 0 est suivi par un nombre allant de 1 à 9. Il faut donc utiliser la classe [1-9], qui signifie "Un nombre de 1 à 9"
    #^0[1-9]$#
  4. Ensuite, viennent les 8 chiffres restants, pouvant aller de 0 à 9. Il nous suffit donc d'écrire [0-9]{8} pour indiquer que l'on veut 8 chiffres. Au final, ça nous donne cette Regex :
    #^0[1-9][0-9]{8}$#


Et c'est tout ! :D

Bon, je vois que vous êtes en forme, alors ne nous arrêtons pas en si bon chemin et améliorons cette Regex. :)
Maintenant, on va supposer que la personne peut taper un espace tous les 2 chiffres (comme c'est courant de le faire en France), mais aussi un point ou un tiret. Notre Regex devra donc accepter les numéros de téléphone suivants :

  • 0153789999
  • 01 53 78 99 99
  • 01-53-78-99-99
  • 01.53.78.99.99
  • 0153 78 99 99
  • 0153.78 99-99
  • etc...


Et c'est là qu'est toute la puissance des Regex !
Les possibilités sont très nombreuses, et pourtant vous avez juste besoin d'écrire la Regex qui correspond. :D

On reprend la création de notre Regex donc :

  1. Primo, le 0 puis le chiffre de 1 à 9. Ca, ça ne change pas :
    #^0[1-9]$#
  2. Après ces 2 premiers chiffres, il peut y avoir soit un espace, soit un tiret, soit un point, soit rien du tout (si les chiffres sont attachés). On va donc utiliser la classe [-. ] (tiret, point, espace).
    Mais comment faire pour dire que le point (ou le tiret, ou l'espace) n'est pas obligatoire ? Avec le point d'interrogation bien sûr !
    Ca nous donne :
    #^0[1-9][-. ]?$#
  3. Après le premier tiret (ou point, ou espace, ou rien), on a les 2 chiffres suivants. On doit donc rajouter [0-9] à notre Regex.
    #^0[1-9][-. ]?[0-9]{2}$#
  4. Et maintenant réfléchissez. Il y a moyen de terminer rapidement : on a juste besoin de dire que "[-. ]?[0-9]{2}" doit être répété 4 fois, et notre Regex est terminée ! On va se servir des parenthèses pour entourer le tout, et placer un {4} juste après pour indiquer que tout ça doit se répéter 4 fois. Ce qui nous donne finalement :
    #^0[1-9]([-. ]?[0-9]{2}){4}$#

Vous pouvez l'encadrer en gros en poster dans votre chambre : c'est votre première VRAIE Regex !!! :D

#^0[1-9]([-. ]?[0-9]{2}){4}$#

Voici un petit script que j'ai fait rapidement, pour que vous puissiez tester toute la puissance des Regex :

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<p>
<?php
if (isset($_POST['telephone']))
{
    $_POST['telephone'] = htmlspecialchars($_POST['telephone']); // On rend inoffensives les balises HTML que le visiteur a pu rentrer

    if (preg_match("#^0[1-9]([-. ]?[0-9]{2}){4}$#", $_POST['telephone']))
    {
        echo 'Le ' . $_POST['telephone'] . ' est un numéro <strong>valide</strong> !';
    }
    else
    {
        echo 'Le ' . $_POST['telephone'] . ' n\'est pas valide, recommencez !';
    }
}
?>
</p>

<form method="post">
<p>
    <label for="telephone">Votre téléphone ?</label> <input id="telephone" name="telephone" /><br />
    <input type="submit" value="Vérifier le numéro" />
</p>
</form>




Vous pouvez essayer tous les numéros de téléphone que vous voulez, avec des espaces au milieu, ou pas si ça vous chante : la Regex gère tous les cas. :D

Vous auriez pu aussi utiliser le raccourci \d pour indiquer un chiffre dans votre Regex :
#^0\d([-. ]?\d{2}){4}$#
Personnellement, je trouve que mettre [0-9] est quand même plus clair. ;)


Une adresse e-mail



Ca serait dommage de s'arrêter sur une si bonne lancée. ^^
Je vais donc vous présenter un deuxième exemple, qui vous sera certainement utile : tester si l'adresse e-mail est valide.

Alors, avant de commencer quoi que ce soit, et pour qu'on soit bien d'accord, je vais rappeler comment est construite une adresse e-mail :

  1. On a tout d'abord le pseudonyme (au minimum une lettre, mais c'est plutôt rare). Il peut y avoir des lettres minuscules (pas de majuscules), des chiffres, des points, des tirets et des underscores "_".
  2. Il y a ensuite un arobase : @
  3. Ensuite il y a le nom de domaine. Pour ce nom, même règle que pour le pseudonyme : que des minuscules, des chiffres, des tirets, des points et des underscores. La seule différence, vous ne pouviez pas forcément deviner, c'est qu'il y a au moins 2 caractères (par exemple, "a.com" n'existe pas, mais "aa.com" oui).
  4. Enfin, il y a l'extension (comme ".fr"). Cette extension comporte un point, suivi de 2 à 4 lettres (minuscules). En effet, il y a ".es", ".de", mais aussi ".com", ".net", ."org", ".info" etc...


L'adresse e-mail peut donc ressembler à : j.dupont_2@wanadoo.fr

Construisons la Regex :
  1. Primo, comme tout à l'heure, on ne veut QUE l'adresse e-mail, donc on va demander à ce que ça soit un début et une fin de chaîne :
    #^$#
  2. Ensuite, on a des lettres, chiffres, tirets, points, underscores, au moins une fois. On utilise donc la classe [a-z0-9._-] à la suite de laquelle on rajoute le signe + pour demander à ce qu'il y en ait au moins un :
    #^[a-z0-9._-]+$#
  3. Vient ensuite l'arobase (là c'est pas compliqué, on a juste à taper le caractère) :
    #^[a-z0-9._-]+@$#
  4. Puis, encore une suite de lettres, chiffres, points, tirets au moins 2 fois. On tape donc {2,} pour dire "2 fois ou plus" :
    #^[a-z0-9._-]+@[a-z0-9._-]{2,}$#
  5. Ensuite vient le point (de ".fr" par exemple). Comme je vous l'ai dit plus haut, c'est un caractère spécial qui sert à indiquer "n'importe quel caractère" (même des accents). Or, ici, on veut enlever sa signification au point pour dire que l'on veut le symbole point dans notre Regex. On va donc mettre un antislash devant :
    #^[a-z0-9._-]+@[a-z0-9._-]{2,}\.$#
  6. Enfin, pour terminer, il nous faut 2 à 4 lettres. Ce sont forcément des lettres minuscules, et cette fois pas de chiffres ou de tiret etc... On écrit donc :
    #^[a-z0-9._-]+@[a-z0-9._-]{2,}\.[a-z]{2,4}$#


Et voilà encore une nouvelle Regex de bouclée ! :D

#^[a-z0-9._-]+@[a-z0-9._-]{2,}\.[a-z]{2,4}$#

Vous sentez que vous commencez à parler chinois, non ? ;)

Allez, je suis en forme et de bonne humeur, je vous donne le script PHP pour tester cette Regex :

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<p>
<?php
if (isset($_POST['mail']))
{
$_POST['mail'] = htmlspecialchars($_POST['mail']); // On rend inoffensives les balises HTML que le visiteur a pu rentrer

    if (preg_match("#^[a-z0-9._-]+@[a-z0-9._-]{2,}\.[a-z]{2,4}$#", $_POST['mail']))
    {
        echo 'L\'adresse ' . $_POST['mail'] . ' est <strong>valide</strong> !';
    }
    else
    {
        echo 'L\'adresse ' . $_POST['mail'] . ' n\'est pas valide, recommencez !';
    }
}
?>
</p>

<form method="post">
<p>
    <label for="mail">Votre mail ?</label> <input id="mail" name="mail" /><br />
    <input type="submit" value="Vérifier le mail" />
</p>
</form>



Testez donc des adresses comme :
  • the_cypher@hotmail.com
  • business_consultants@free4work.info
  • mega-killer.le-retour@super-site.fr.st
  • etc etc...


Alors, ça vous plaît ? ;)
Je reconnais que ça paraît être un truc de malade quand on lit une Regex la première fois. J'imagine la tête que vous avez dû faire lorsque je vous en ai montré une dans l'introduction du chapitre précédent. ^^

Mais bon, vous voyez le progrès ?! On vient ensemble d'écrire un de ces fameux trucs imbuvables, et je ne pense pas que beaucoup d'entre vous pensaient y arriver en lisant le chapitre précédent !
Pourtant nous y voilà, nous avons réussi à écrire 2 Regex complètes. Je ne vais pas vous faire travailler sur une troisième, vous avez je pense compris le principe et vous savez vous débrouiller comme des grands.

Je veux juste vous montrer une dernière petite chose avant de passer à la dernière notion importante que nous aborderons (Capture et remplacement).

Des Regex... avec MySQL !



Comme quoi, vous allez vraiment être heureux d'en avoir un peu bavé pour arriver jusqu'ici. :p
Eh oui, grrrande nouvelle : MySQL comprend les Regex !

Et ça, bah c'est tout bénèf pour vous : vous venez d'apprendre à écrire des Regex, vous n'avez presque rien de plus à savoir pour vous en servir avec MySQL.
Il faut savoir cependant que MySQL ne comprend que les Regex en langage POSIX, et pas PCRE comme on a appris.

Salaud ! Tu nous as fait apprendre PCRE parce que c'est plus rapide, et on peut même pas s'en servir avec MySQL ???

Meuh non, calmez-vous voyons. ;)
Je vous ai appris PCRE parce que c'était beaucoup plus rapide ET que c'était pratiquement pareil que POSIX.

Alors, vous avez juste besoin de retenir ceci pour faire une Regex POSIX :
  • Il n'y a pas de délimiteur ni d'options. Votre Regex n'est donc pas entourée de dièses
  • Il n'y a pas de classes abrégées comme on l'a vu plus haut, donc pas de \d etc... En revanche, vous pouvez toujours utiliser le point pour dire : "n'importe quel caractère".

Le mieux, bien entendu, c'est toujours un bon exemple. Supposons que vous ayez stocké les IP de vos visiteurs dans une table "visiteurs" et que vous voulez les noms des visiteurs dont l'ip commence par "84.254" :

SELECT nom FROM visiteurs WHERE ip REGEXP '^84\.254(\.[0-9]{1,3}){2}$'

Cela signifie : Sélectionne tous les noms de la table visiteurs où l'ip commence par "84.254" et se termine par 2 autres nombres de 1 à 3 chiffres (ex : 84.254.6.177).

Toute la puissance des Regex dans une requête MySQL pour faire une recherche très précise... Ca ne se refuse pas. ;)
Je ne m'étends pas plus dessus, je sais que vous saurez vous débrouiller si jamais cela vous est utile.

Passons maintenant à la dernière notion importante avec les Regex : "Capture et remplacement" !

Capture et remplacement

Je vous avais dit au début de ces 2 chapitres consacrés aux Regex qu'elles servaient à faire une recherche puissante (ça on vient de le voir, à travers l'exemple du téléphone et du mail), mais aussi à faire une recherche et un remplacement.
Cela va nous permettre par exemple de faire la chose suivante :
  1. Chercher s'il y a des adresses e-mail dans un message laissé par un visiteur.
  2. Modifier automatiquement son message pour mettre un lien <a href="mailto:blabla@truc.com"> devant chaque adresse, ce qui rendra les e-mails cliquables !

Avec cette technique, on peut faire pareil pour rendre les liens http:// automatiquement cliquables eux aussi. On peut aussi, vous allez voir, créer notre propre langage simplifié pour le visiteur, comme le fameux bbCode utilisé sur la plupart des forums ([b][/b] pour mettre en gras, ça vous dit quelque chose ? ;) ).

Les parenthèses capturantes



Tout ce que nous allons voir maintenant tourne autour des parenthèses. Vous vous êtes déjà servi d'elles pour entourer une partie de votre Regex et dire qu'elle devait se répéter 4 fois par exemple (comme on l'a fait pour le numéro de téléphone).
Eh bien ça, c'est la première utilité des parenthèses, mais elles peuvent aussi servir à autre chose !

Nous allons travailler avec la fonction preg_replace à partir de maintenant.
C'est avec cette fonction que nous allons pouvoir réaliser ce qu'on appelle une "capture" de chaîne.

Ce qu'il faut savoir, c'est qu'à chaque fois que vous utilisez des parenthèses, cela crée une "variable" contenant ce qu'elles entourent.
Je m'explique avec une Regex :

#\[b\](.+)\[/b\]#

Vous ne devriez pas avoir trop de mal à la déchiffrer : elle signifie "Chercher dans la chaîne un [b], suivi d'un ou plusieurs caractères (le point permet de dire "n'importe lesquels"), suivis d'un [/b]".

J'ai été obligé de mettre des antislash "\" devant les crochets pour ne pas que PHP les confonde avec des classes de caractères (comme [a-z])


Normalement, si vous réfléchissez 2 secondes, vous devez vous dire que les parenthèses ne sont pas obligatoires ici. Et c'est vrai que pour faire juste une recherche, les parenthèses sont effectivement inutiles. Mais pour faire un remplacement, cela va être très pratique !

En effet, retenez bien ceci : à chaque fois qu'il y a une parenthèse, cela crée une variable appelée $1 (pour la première parenthèse), $2 pour la seconde etc...
On va se servir ensuite de ces variables pour modifier la chaîne (faire un remplacement).

Sur la Regex que je vous ai montrée plus haut, il y a une seule parenthèse vous êtes d'accord ? Donc, il y aura juste une variable $1, qui contiendra ce qui se trouve entre le [b] et le [/b]. Et grâce à ça, on sait ce qu'on va mettre en gras. :D

Bon, la théorie de tout ça est délicate à expliquer, alors je vais vous montrer de suite comment on fait pour mettre en gras tous les mots compris entre des [b][/b] :

Code : PHP
1
2
3
<?php
$texte = preg_replace('#\[b\](.+)\[/b\]#i', '<strong>$1</strong>', $texte);
?>


Voici comment s'utilise la fonction preg_replace :

  1. On lui donne en premier paramètre la Regex. Rien de particulier, comme vous pouvez le constater, à part qu'il faut bien garder en tête que chaque parenthèse va créer une variable ($1, $2 etc...)
    Ici, j'ai rajouté l'option "i" pour que le code fonctionne aussi avec des majuscules ([B][/B])
  2. Ensuite, et c'est là qu'est la nouveauté, on indique le texte de remplacement : "<strong>$1</strong>" (je vous rappelle que <strong> permet de mettre en gras en HTML).
    Entre les balises HTML, j'ai mis $1. Cela signifie que ce qui se trouve dans la parenthèse capturante (entre [b] et [/b]) sera en fait entouré des balises <strong> !
  3. Enfin, dernier paramètre, c'est le texte dans lequel on fait notre recherche / remplacement (ça vous connaissez déjà).

La fonction preg_replace renvoie le résultat après avoir fait les remplacements.

Si je schématise le fonctionnement, ça donne ça :

Image utilisateur


Il y a quelques règles à respecter que vous allez devoir apprendre :

  • Si vous avez plusieurs parenthèses, pour savoir le numéro d'une parenthèse il suffit de les compter dans l'ordre de gauche à droite. Par exemple :
    #(anti)co(nsti)(tu(tion)nelle)ment#
    Il y a 4 parenthèses dans cette regex (donc $1, $2, $3 et $4). La parenthèse numéro 3 ($3) contient "tutionnelle", et la parenthèse $4 contient "tion"

    N'oubliez pas que c'est l'ordre dans lequel les parenthèses sont ouvertes qui est important.

  • Vous pouvez utiliser jusqu'à 99 parenthèses capturantes dans une Regex (ça vous laisse de la marge ). Ca va donc jusqu'à $99
  • Une variable $0 est toujours créée, elle contient toute la Regex. Sur le même exemple que tout à l'heure :
    #(anti)co(nsti)(tu(tion)nelle)ment#
    ... $0 contient "anticonstitutionnellement".
  • Si, par hasard, vous ne voulez pas qu'une parenthèse soit capturante (pour vous faciliter les comptes, ou parce que vous avez beaucoup beaucoup de parenthèses), il faut qu'elle commence par un point d'interrogation suivi d'un deux points ":". Par exemple :
    #(anti)co(?:nsti)(tu(tion)nelle)ment#
    La seconde parenthèse n'est pas capturante. Il ne nous reste que 3 variables (4 si on compte $0) :
    1. $0 : anticonstitutionnellement
    2. $1 : anti
    3. $2 : tutionnelle
    4. $3 : tion

Voilà si vous avez compris ça, vous avez tout compris, bravo ! :)

Créez votre bbCode



Maintenant, on peut passer à la pratique et apprendre à se servir des parenthèses capturantes.

Nous allons réaliser ce qu'on appelle un parser (prononcez "parseur").
Le parser va servir à transformer le texte rédigé par un visiteur (pour un message sur un forum, ou sur votre livre d'or, ou même sur votre mini-chat !), en un texte inoffensif (sans balise HTML grâce à htmlspecialchars) mais qui accepte aussi du bbCode !
On ne va pas faire tous les bbCode qui existent (trop long), mais pour s'entraîner déjà ceux-ci suffiront :

  • [b][/b] : pour mettre du texte en gras.
  • [i][/i] : pour mettre du texte en italique.
  • [color=red][/color] : pour colorer le texte (il faudra laisser le choix entre plusieurs couleurs).


Et nous ferons en sorte de remplacer aussi automatiquement les URL (http://) par des liens cliquables. :)

Commençons par [b] et [i] (c'est la même chose).
Vous avez déjà vu le code pour [b], et c'est en effet presque le bon. Il y a un problème toutefois : il manque des options. Pour que ça marche, on va avoir besoin d'utiliser 3 options :
  • i : pour accepter les majuscules comme les minuscules ([B] et [b])
  • s : pour que le "point" fonctionne aussi pour les retours à la ligne (pour que le texte puisse être en gras sur plusieurs lignes)
  • U : le U majuscule est une option que vous ne connaissez pas, qui signifie "Ungreedy" ("pas gourmand"). Je vous passe les explications un peu complexes sur son fonctionnement, mais sachez que, grosso modo, ça ne marcherait pas correctement s'il y avait plusieurs [b] dans votre texte. Exemple :
    "Ce texte est [b]important[/b], il faut me [b]comprendre[/b] !"
    ... sans l'option Ungreedy, la Regex aurait voulu mettre en gras tout ce qu'il y a entre le premier [b] et le dernier [/b] (c'est-à-dire "important[/b], il faut me [b]comprendre";).
    En utilisant l'option "U", la Regex s'arrêtera au premier [/b], et c'est ce qu'on veut ;)

Voici donc le code correct pour mettre en gras et italique avec le bbCode :

Code : PHP
1
2
3
4
<?php
$texte = preg_replace('#\[b\](.+)\[/b\]#isU', '<strong>$1</strong>', $texte);
$texte = preg_replace('#\[i\](.+)\[/i\]#isU', '<em>$1</em>', $texte);
?>


Comme vous pouvez le voir, c'est quasiment pareil pour [b] et [i] (à part que la balise HTML qu'on utilise est <em>).

Donc là, si vous avez suivi jusqu'ici, ça ne doit pas trop vous surprendre.
Passons maintenant à un cas un peu plus complexe : celui de la balise [color=truc]. On va laisser le choix entre plusieurs couleurs avec le symbole "|" (OU), et on va utiliser 2 parenthèses capturantes :

  1. La première pour récupérer la couleur qui a été choisie (en anglais, comme ça on n'aura pas besoin de le changer pour le code HTML).
  2. La seconde pour récupérer le texte entre [color=truc] et [/color] (pareil que pour gras et italique).


Voici le résultat :

Code : PHP
1
2
3
<?php
$texte = preg_replace('#\[color=(red|green|blue|yellow|purple|olive)\](.+)\[/color\]#isU', '<span style="color:$1">$2</span>', $texte);
?>


Ainsi, si on tape [color=blue]texte[/color], ça écrira texte en bleu. Vous pouvez essayer avec les autres couleurs aussi ! ^^

Allez dernière étape, et après je vous laisse essayer.
Je veux que les liens "http://" soient automatiquement transformés en liens cliquables. Essayez d'écrire la regex, vous en êtes tout à fait capables !

Voici la solution :

Code : PHP
1
2
3
<?php
$texte = preg_replace('#http://[a-z0-9._/-]+#i', '<a href="$0">$0</a>', $texte);
?>


Dans le texte de remplacement, j'ai utilisé $0 qui, si vous vous souvenez bien, prend tout le texte reconnu par la Regex (donc ici toute l'url).
Il n'y a pas les options "s" et "U" car on ne fait jamais de retour à la ligne au milieu d'une URL et, le mode "Ungreedy" ne sert pas ici (essayez avec U, vous verrez que le lien s'arrête à la première lettre !)


Vous remarquerez que j'ai fait simple pour cette Regex. C'est vrai, j'aurais pu la faire plus complexe et plus précise, mais je n'ai pas envie de vous embrouiller avec ça, et surtout je veux que vous l'amélioriez vous-mêmes.
En effet, la Regex marche très bien pour http://www.siteduzero.com/images/super_image2.jpg, mais elle ne marche pas s'il y a des variables en paramètres dans l'url, comme par exemple :
http://www.siteduzero.com/index.php?page=3&skin=blue

Je vous laisse le soin d'améliorer la Regex, ça vous fera un peu de travail ;)

Vous savez quoi ? Vous avez peut-être mal à la tête, mais dites-vous que moi j'ai mal à la tête ET mal aux doigts ! :D

Mais je vais fournir quand même un dernier petit effort, voici l'heure du cadeau bonus !

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php
if (isset($_POST['texte']))
{
    $texte = stripslashes($_POST['texte']); // On enlève les slash qui se seraient ajoutés automatiquement
    $texte = htmlspecialchars($texte); // On rend inoffensives les balises HTML que le visiteur a pu rentrer
    $texte = nl2br($texte); // On crée des <br /> pour conserver les retours à la ligne
    
    // On fait passer notre texte à la moulinette des Regex
    $texte = preg_replace('#\[b\](.+)\[/b\]#isU', '<strong>$1</strong>', $texte);
    $texte = preg_replace('#\[i\](.+)\[/i\]#isU', '<em>$1</em>', $texte);
    $texte = preg_replace('#\[color=(red|green|blue|yellow|purple|olive)\](.+)\[/color\]#isU', '<span style="color:$1">$2</span>', $texte);
    $texte = preg_replace('#http://[a-z0-9._/-]+#i', '<a href="$0">$0</a>', $texte);

    // Et on affiche le résultat. Admirez ! :D
    echo $texte . '<br /><hr />';
}
?>

<p>
    Bienvenue dans le parser du Site du Zéro !<br />
    Nous avons écrit ce parser ensemble, j'espère que vous saurez apprécier de voir que tout ce que vous avez appris va vous être très utile !
</p>

<p>Amusez-vous à utiliser du bbCode. Tapez par exemple :</p>

<blockquote style="font-size:0.8em">
<p>
    Je suis un gros [b]Zéro[/b], et pourtant j'ai [i]tout appris[/i] sur http://www.siteduzero.com<br />
    Je vous [b][color=green]recommande[/color][/b] d'aller sur ce site, vous pourrez apprendre à faire ça [i][color=purple]vous aussi[/color][/i] !
</p>
</blockquote>

<form method="post">
<p>
    <label for="texte">Votre message ?</label><br />
    <textarea id="texte" name="texte" cols="50" rows="8"></textarea><br />
    <input type="submit" value="Montre-moi toute la puissance des Regex" />
</p>
</form>




Pfiou !
Eh bah si avec ça vous me pondez pas un super site, je ne peux plus rien pour vous. ;)

Avant de terminer, comme j'ai peur que vous vous ennuyiez, je vous donne quelques idées de Regex que vous pourriez rajouter au parser :

  • Je vous l'ai déjà dit plus haut, mais il serait très appréciable que les URL cliquables fonctionnent aussi pour des URL avec des variables comme :
    http://www.siteduzero.com/index.php?page=3&skin=blue
  • Vous devriez aussi parser les adresses e-mail, en faisant un lien "mailto:" dessus !
  • Il serait bien de compléter le bbCode avec [u], [img] etc...
    Mais puisqu'on y est, pourquoi refaire du bbCode ? Après tout, si vous êtes allergiques aux crochets, que pour vous [b] ne veut rien dire, vous n'avez qu'à inventer le code : {gras} {/gras} :p
  • Et, si faire des Regex vous plaît, je peux vous proposer un dernier défi qui devrait vous occuper un petit moment : écrire une fonction qui colore automatiquement le code HTML !
    Vous donnez à la fonction le code HTML, elle en fait un htmlspecialchars, puis elle rajoute des <span style="color:..."> pour colorer par exemple en bleu les noms des balises, en vert les attributs, en rouge ce qui est entre guillemets, etc.


Bon courage !
Vous en aurez besoin !

Q.C.M.

Lequel de ces caractères n'est pas un métacaractère ?
Pourquoi cette Regex va planter ?
Code : Autre
1
#[1-5a-e:-.]+#
Quelle est l'option qui permet de faire en sorte que le point compte aussi les Entrées (\n) ?
Quelle classe correspond à \W ?
Laquelle de ces Regex va permettre de reconnaître un mot de passe de 4 à 8 lettres, acceptant uniquement des minuscules, des chiffres et des tirets mais à condition que le mot de passe commence et se termine par une lettre ? (niark, je suis diabolique ^^ )
Que contient $3 dans cette petite Regex ?
Code : Autre
1
#(Je (suis) (?:un) (Zér))0#
Quelle Regex capturera les dates au format international AAAA-MM-JJ pour obtenir 3 variables $1, $2 et $3 qui serviront ensuite à transformer la date au format français JJ/MM/AAAA ? Petite difficulté : il faut aussi accepter les années à 2 chiffres (AA-MM-JJ doit marcher !)

Statistiques de réponses au QCM

Après des chapitres comme ceux-ci, je pense que ce petit cadeau vous fera le plus grand bien. ^^

Image utilisateur


Je n'ai pas grand chose à ajouter, si ce n'est que je suis exténué mais heureux, parce que c'était vraiment un des chapitres les plus difficiles du cours et nous y sommes venus à bout ensemble. :D
Chapitre précédent Sommaire Chapitre suivant

Partager

111 commentaires pour "Les expressions régulières (Partie 2/2)"
Note moyenne : 3.61 / 4 (2409 votes)
Pseudo Commentaire
Hors ligne k3nz0 # Posté le 07/06/2011 à 02:18:12

Voici la regex proposé dans le tutoriel qui regarde si l'adresse mail correspond à une adresse valide ou pas :
#^[a-z0-9._-]+@[a-z0-9._-]{2,}\.[a-z]{2,4}$#
Alors, je viens signaler un petit truc ...
après le @ nous avons le nom de domaine du site (qui peut être hotmail, yahoo ... ou le nom de domaine du propriétaire du site ... ) Or comme vous le savez il est impossible de créer un nom de domaine comportant des underscores. Donc ici [a-z0-9._-]{2,}, le "_" est inutile [a-z0-9.-]{2,} ...
La regex finale que je propose :
#^[a-z0-9._-]+@[a-z0-9.-]{2,}\.[a-z]{2,4}$#

k3nz0x3a
-- For security reasons, I do not exist --
-- Pour des raisons de sécurité, je n'existe pas --
 
Hors ligne chooky # Posté le 21/06/2011 à 20:01:08

Avis : Très bon

Après m'avoir introduit, formé et façonné au PHP il y a maintenant 7 ans. C'est encore toi Mathéo qui m'a enfin fait franchir le pas des regex sur lesquelles j'ai toujours fait un blocage.
Je te remercie pour la clarté, le côté ludique et la qualité de tes tutoriels. Tu es sincèrement très grand.
Bonne chance à toi et merci pour tout ce que tu peux apporter. Originaire d'Avignon et résidant dans les antilles depuis 11 ans, ce serait une joie que de te payer une mousse durant mon retour dans la région pour les vacances d'été.
Hors ligne GTK # Posté le 03/11/2011 à 13:03:16
Assembleur de Pixel
Avatar
Flux RSS

Bonjour ! Excellent tuto !
Cependant, j'ai deux questions :

- Premièrement, comme Orquato, je n'ai pas bien compris l'exemple... Je pensais avoir compris la deuxième parenthèse que cite Orquato mais j'ai un doute maintenant...

- Ensuite, dans le second chapitre, tu parles des cas de classe. Tu cites la #, le ] et le - comme des exceptions, mais tu ne parles pas de ^. Se caractère n'a pas de besoin de \ s'il ne se trouve pas au tout début de l'accolade ?
 
Hors ligne candide # Posté le 09/12/2011 à 23:24:49
"In C ode we trust"
Avatar

Très bon tuto et certainement très précieux pour ceux qui découvrent les regexp, en particulier les explications pas à pas de construction de regexp.

Bien que ce ne soit probablement pas utile, le tuto pourrait être approfondi en parlant par exemple de l'avididité des quantificateurs, des lookahead et lookbehind (en français, les motifs prospectifs et rétrospectifs), de l'algorithme de traitement (rétrogradation, backtracking), l'ancrage \b, etc.

Cela manque aussi de références (livres en français ou en anglais, des outils de capture de regexp en ligne ou à installer, des pointeurs vers des sites, par exemple la documentation en ligne de php sur les regexp qui semble bien faite).


Je n'ai pas vu d'erreurs ; j'ai trouvé curieux que l'on parle à deux reprises de \n comme les Entrées (qui fait penser à entrées/sorties alors qu'il ne s'agit que d'un saut de ligne). Au passage, \n n'est pas une classe abrégée, de même que le point (cf. le tableau dans le § Les classes abrégées). Il me semble incorrect de dire que l'on doive impérativement échapper un crochet fermant dans une classe de caractères (il suffit de la placer au début de la classe, essayer ICI)

  • Make it work, Make it well, Make it fast, Make it short, Make it fun
  • Comment insérer les balises de code dans un message ? : Screencast ou Texte
  • Le rasoir d'Occam : Entities should not be multiplied beyond necessity.
  • KISS !
  • Le gâteau sous la cerise.
  • (Lien vers fichier pdf) La norme du langage C 99
  • scanf et printf : les colifichets de la programmation bling bling

 
Hors ligne phdphd # Posté le 13/12/2011 à 12:30:08

Une petite suggestion de mise à jour concernant la partie qui traite des regex de remplacement : utiliser "paire de parenthèses". Exemple :


Il faut bien garder en tête que chaque parenthèse va créer une variable ($1, $2 etc...) >>> il faut bien garder en tête que chaque paire de parenthèses va créer une variable ($1, $2 etc...)


#(anti)co(nsti)(tu(tion)nelle)ment#
Il y a 4 parenthèses dans cette regex (donc $1, $2, $3 et $4). La parenthèse numéro 3 ($3) contient "tutionnelle", et la parenthèse $4 contient "tion" >>> #(anti)co(nsti)(tu(tion)nelle)ment#
Il y a 4 paire de parenthèses dans cette regex (donc $1, $2, $3 et $4). La paire de parenthèses numéro 3 ($3) contient "tutionnelle", et la paire de parenthèses $4 contient "tion"

Voir tous les commentaires