Les Regexps en PHP

Les expressions régulières

Avant Propos

Texte de vulgarisation sur les expressions régulières en PHP4
Ce que j'ai dit reste valable en PHP5, mais vous devez commencer par vous renseigner sur les fonctions de filtres.

Introduction

Mise en appétit

Il arrive fréquemment (surtout lors du développement web) à un programmeur de devoir créer un programme qui traite des chaînes de caractères. Lors de ce traitement, les expressions régulières peuvent lui être d'un grand secours... Quand on fait un formulaire d'inscription, on vérifie toujours plusieurs choses : le pseudo doit avoir au minimum 6 caractères, et au maximum 16, le password doit avoir au minimum 10 caractères et au maximum 16, on ne doit pas accepter quelqu'un qui met "paul" dans la case date de naissance, mais on doit accepter quelqu'un qui met : deux chiffres, un séparateur (soit "-", soit "/" soit un espace), puis deux chiffres, puis quatre chiffres, les plus pointilleux veilleront à ce que le premier chiffre de l'année soit "1" ou "2". Avec des fonctions de traitements de chaînes classiques, vous aurez bien du mal à faire toutes ces vérifications. C'est la que les expressions régulières interviennent. On peut faire tout cela évidement avec des fonctions classiques, mais les programmes seraient bien plus lents, bien plus compliqués à comprendre, bien plus gros... Les expressions régulières accélèrent les programmes, les rendent plus performants et plus lisibles, vous facilitent la programmation.

Vous avez certainement déjà utilisé et vu des expressions régulières : lorsque vous cherchez un fichier, vous tapez *.doc par exemple pour un fichier Word, et bien c'est une forme d'expression régulière ; et lors de l'enregistrement, vous voyez en bas, le filtre des fichiers affichés : *.doc Fichier Words par exemple.

Où s'appliquent-elles ?

On peut les utiliser pour parler des chaînes, des fichiers (de configuration par exemple), pour faire de la coloration syntaxique, du BBcode dans un forum, pour vérifier le bon format d'un numéro de téléphone, pour convertir une date française en date américaine, en clair, on peut les trouver partout...

Quels langages ?

Je n'énumérerai pas tous les langages où l'on peut les trouver. Je citerai le Perl, qui a connu un tel succès grâce à cette fonction qui permettait aux .cgi d'être rapides et faciles à comprendre. On les trouve en PHP, où elles permettent aux pages d'êtres plus rapides, plus lisibles, plus facilement modifiable (même si les débutants ne s'attaquent pas aux expressions régulières facilement), et en Javascript, où elles servent pour la simulation de BBcode en temps réel, par exemple. Nous nous attarderons sur le PHP et le Javascript (duo gagnant du net), mais sachez que les expressions régulières existent dans bien d'autres langages.

Vocabulaire

On parle d'expressions régulières, mais on parle aussi d'expressions rationnelles, ces deux groupes nominaux sont équivalents, ils désignent la même chose. Pour désigner une expression régulière, on parle souvent sur les forums de Regexp, et de Pattern. Le Pattern est la chaîne qui est appliquée par la fonction sur une autre chaîne (c'est l'expression régulière).

On parle souvent de sensibilité à la casse, ce terme désigne tout simplement la différence minuscule / majuscule... Si l'expression est sensible à la casse, elle fera la différence entre les minuscules et les majuscules, et elle aura i dans ses options.

Bases

Pour utiliser les expressions régulières, il faut connaître le code des caractères invisibles (pour ceux qui ne savaient pas que ça existe, lisez la table des caractères ASCII, et cherchez à écrire le caractère 13 ou 32 en mettant de l'encre sur une feuille... 13 correspond au retour à la ligne, et 32 à l'espace).

\n retour à la ligne
\r retour de chariot
\t tabulation
\xhh caractère hexadécimal de code hh
\ddd caractère en octale de code ddd
\040 espace
\011 tabulation
\d tout caractère décimal
\D tout caractère non décimal
\s tout caractère blanc
\S tout caractère non blanc
\w tout caractère de mot
\W tout caractère qui ne peut pas être dans un mot
\b toute limite de mot
\B tout caractère qui n'est pas une limite de mot

Vif du sujet

Comment les utiliser

Pour commencer, nous devons chercher une liste de fonctions intéressantes à utiliser avec ces expressions : quelles sont les fonctions associées ? Il en existe une grande quantité. En Javascript, il y a même un objet nommé ExpReg. Nous ne le présenterons pas dans cette page, ce n'est pas le but (de plus, on peut s'en passer, et cet objet n'est pas vraiment performant...). Nous montrerons des fonctions qui permettent d'utiliser les expressions régulières en PHP et en Javascript, mais ne présenterons dans un premier temps que des expressions régulières pour PHP (Il ne faut pas oublier que le PHP est un dérivé du Perl : PHP est le diminutif de PHP Hypertext Preprocessor... Dans le libre, on aime bien les acronymes... PHP est né en 1995 par Rasmus Lerdorf avec PHP/FI constitué de scripts Perl, PHP voulait dire Personal Home Page Tools et vu le grossissement du projet, PHP a été réécrit en C). Donc, comme PHP est un dérivé du Perl, les expressions régulières y sont présentes, et pour un langage serveur, mieux vaut savoir les utiliser. Le Perl n'est pas facile d'accès, alors parler d'expressions régulières en Perl sans en avoir parlé en PHP peut être déroutant.

Fonction Javascript PHP
teste si le motif est présent test ereg
remplace le motif par une chaîne replace ereg_replace
fractionne selon le motif (comme explode, mais avec une expression régulière au lieu d'une chaîne) split preg_split
recherche toutes les chaînes match preg_match_all ou preg_match

Il en existe bien plus, mais le but de cette page n'est pas de lister les fonctions, nous voulons montrer la puissance des expressions régulières et vous montrer comment en faire. Pour plus de détails : http://fr.php.net/preg_replace

Mise en condition

Pour expliquer les expressions régulières, rien ne vaut un bon exemple :

Simon veut faire un forum, mais un bon forum : il veut proposer plein d'option à ses utilisateurs. Martin est le premier utilisateur, il peut choisir son pseudo, son password, son adresse e-mail, son adresse MSN Messenger (si adresse il y a), son code postal, sa date de naissance, l'adresse de son site web, l'adresse de son site web préféré... Simon veut que tout soit parfait, donc, pas question d'autoriser un utilisateur de mauvais goût à mettre "biduletruc" comme date de naissance. Il doit tout vérifier...

Principe de bases

Une expression régulière se présente en deux parties : un corps et des options. Le corps est délimité par deux signes divisés, et les options viennent ensuite : "/corps/options". Les options sont assez simples :

  • on écrit "m" pour négliger les retours à la ligne,
  • "i" pour négliger les minuscules majuscules (on appelle ça aussi faire attention à la casse)
  • "s" permet de faire en sorte que le point remplace n'importe quel caractère y compris le retour à la ligne
  • "x" permet d'ignorer les espaces.

Il en existe d'autres, mais ils sont moins courants : pour plus de détails.
Remarque: en Javascript, il faut toujours mettre g comme option.

On détaillera plus le corps de l'expression, mais à travers quelques exemples pour être plus pratique que théorique, pour que les moins travailleurs s'ennuient le moins possible...

Le pseudo est composé de caractères alphanumériques et du tiret "_", sa longueur est d'au minimum 8 caractères et au maximum 12. L'expression régulière correspondant à la vérification sera : "/^[a-z0-9_]{8,12}$/i". Dans une expression régulière, le caractère ^ correspond au début de la chaîne de caractères traitée quand il est placé en début d'expression (c'est le cas ici). Ensuite, les crochets correspondent à des intervalles et à des choix : [a-z0-9_] correspond à un caractère qui sera soit une lettre entre a et z (enfin une lettre quoi...), soit un chiffre entre 0 et 9 (enfin un chiffre quoi...), soit le caractère "_". Pour les accolades, elles définissent le nombre de fois que le groupe précédent est répété. Ici, elles indiquent que le caractère entre crochets peut être répété entre 8 et 12 fois, ce qui veut dire que cette expression désigne une suite de 8 à 12 caractères qui peuvent être des caractères alphanumériques, ou un tiret. Ensuite, il y a le dollar. Lorsqu'il est placé en fin d'expression, il représente la fin de la chaîne de caractères traitée.

Le password doit avoir au minimum 10 caractères. A part ça, il a les mêmes caractéristiques que le pseudo : il est composé de caractères alphanumériques, et de tirets. Son expression régulière est : "/^[a-z0-9_]{10,}$/i". Lorsqu'un nombre et une virgule sont entre accolades, ils indiquent un nombre minimum de répétitions du dernier ensemble. Ici, on aura 10 caractères au minimum.
Si on avait voulu un password de dix caractères exactement, on aurait choisit : "/^[a-z0-9_]{10}$/i".

Pour la date de naissance : "/^[0-9]{2}[\/\\\- ][0-9]{2}[\/\\\- ][0-9]{4}$/". Ici, on a deux chiffres ("[0-9]{2}") suivit d'un séparateur au choix entre "/", "-", "/", et un espace, ("[\/\\\- ]") suivit d'un nombre à deux chiffres, puis d'un séparateur, puis d'un nombre à quatre chiffres. Il existe dans les expressions régulières des caractères dits "spéciaux". Lorsque l'on veut les voir dans nos expressions, on doit utiliser le caractère "\" pour qu'ils ne soient pas interprétés. Ces caractères spéciaux sont : " ^ . [ ] ( ) $ | * + ? { } \ / ". Ce sont ceux dont on se sert pour construire une expression régulière, on peut les comparer aux mots clefs d'un langage, ou aux " que l'on écharpe avec \ dans une chaîne en C++ ou en php quand la chaîne a été commencée par ".

Pour un code postal, c'est un peu plus compliqué car il est soit écrit, soit sous la forme : deux chiffres, un espace puis trois chiffres; soit sous la forme cinq chiffres. Nous écrirons donc : "/^[0-9]{2} ?[0-9]{3}$/". Le point d'interrogation signifie que le groupe précédent (ici, c'est un caractère : l'espace) est facultatif : soit il n'apparaît pas, soit il apparaît une fois (c'est un équivalent de "{0,1}").

Pour son adresse e-mail, "/^[0-9a-z-A-Z_\.\-\/\+\{\}]{2,45}@[0-9a-z-A-Z_]{3,20}\.[0-9a-z-A-Z_]{2,3}$/". Notez que si on ajoute la sensibilité à la casse (si on enlève le i à la fin,) on est obligé d'ajouter A-Z dans les crochets pour ne pas modifier les résultats. Ici, l'adresse mail est sous cette forme : une chaîne composée par deux caractères alphanumériques, des tirets et des points d'entre 2 et 45 caractères, une arobase, une chaîne composée de caractères alphanumériques et de tirets, entre 3 et 20 caractères, un point, et une chaîne composée de caractères alphanumériques et de tirets d'entre deux et trois caractères... Ce style de chaîne est utilisé par les moteurs de spams pour trouver des adresses e-mails sur le net, mais ils utilisent des expressions régulières plus complexes encore (ils commencent par remplacer "at" par "@" [...]).

Pour une url internet, "/^http:\/\/([^\s]*)$/i" on a une chaîne composée de http:// puis de caractères sans espaces. Lorsque l'on met [^...] cela signifie que la chaîne sera composée de tout sauf ..., donc, ici, la suite de la chaîne est composée de tout sauf "\s" et \s correspond à un blanc (un espace, une tabulation, un retour à la ligne...). Pour créer un moteur de recherche, on peut se servir de ce genre d'expression régulière.
Pour son BBcode, les fonctions seront des fonctions de remplacements. Le miracle des expression régulières est ici : on peut copier en mémoire une partie de la chaîne remplacée pour la coller dans la partie de remplacement. Pour copier, on met la partie à sauvegarder entre parenthèses, et on la recolle en mettant \1 pour la première chaîne copiée dans la chaîne qui remplace :

$var=preg_replace('/[Centre](.*?)[/Centre]/i', '<center>\1</center>', $var);
$var=preg_replace('/http://([^s]*)/i', '<a href="http://\1" target="blank">http://\1</a>', $var);

Ici, on remplace le texte centré par les balises html, et les liens par les balises html correspondantes. On peut très bien utiliser plusieurs parenthèses pour récupérer plusieurs chaînes, un exemple qui permet de changer la couleur dans un forum :

$var=preg_replace('/[Couleur #([0-9A-F]{6})](.*?)[/Couleur]/i', '<span style="color:#\1;">\2</span>',$var);

Sur ce principe, on peut faire une collection d'expressions et de boutons pour faire un textarea et un tas de boutons à coté, faire un moteur de BBcode avec plein d'options. Pour ce que l'on peut faire avec les expressions régulières, je laisse votre imagination prendre le relai.

Les équivalences

Il existe des bouts d'expressions équivalentes : un petit exemple : "/[a-zA-Z]/" et "/[a-z]/i". C'est toujours intéressant de connaître ces équivalences :

[[:lower:]]
[a-z]
[[:upper:]]
[A-Z]
[[:alpha:]]
[a-zA-Z]
[[:alphanum:]]
[a-zA-Z0-9]
[[:digit:]]
[0-9]
[[:xdigit:]]
[a-fA-F0-9]
[[:space:]]
[\t\v\f]
[[:blank:]]
[\x09]
[[:punct:]]
[!-/:-@[-'{-~]

Il en existe d'autres, mais ils ne servent quasiment jamais... ":punct:" et "[!-/:-@[-'{-~]" servent à afficher les caractères de ponctuation. Quand on programme, on n'a pas forcément besoin de connaître toutes les équivalences, mais on a parfois besoin de caractères de ponctuation, de tout types d'espaces (":blank:").

Evidemment, dans une expression insensible à la casse, :lower:, :upper: et :alpha: sont aussi équivalents. Il est aisé d'écrire plusieurs expressions régulières équivalentes : "/[A-Z]/i", "/[a-z]/i", "/[a-zA-Z]/", "/:lower:/i", "/:upper:/i", "/:alpha:/", ou encore "/:alpha:/i", on peut donc écrire de plusieurs façons, une suite de lettres sans accents..

Exemple d'applications de fonctions : traitement de commandes bash

Un certain nombre de personnes utilisent un serveur perso pour héberger leur propre site web (cela leur permet de mettre des .cgi, des .pl, des serveurs, et éviter au maximum la publicité). Pour la maintenance de leur machine, les sessions telnet et ssh existent, mais elles restent en mode console, ce qui peut rebuter les puristes (dont je fais parti, ne nous m'éprenons pas), bien que la session ssh soit irremplaçable pour faire des actions de maintenance. Cependant, nous pouvons utiliser un fichier php pour afficher des logs, l'état de la mémoire (ram et HD). Nous ne nous attarderons pas sur les commandes bash utilisées, la page présentée ici est utilisable pour un serveur linux. Pour tout détails, cherchez dans les howto, ou dans les pages man (man free). Pour les accros de la portabilité, il suffit de changer la commande pour obtenir un fichier pour windows.

Pour commencer, nous allons nous attarder sur la commande "df", avec l'option "-h" pour avoir les valeurs à échelle "humaine". La commande "df" permet d'afficher la mémoire utilisée et libre sur le disque dur. Pour exécuter la commande, nous aurions pu utiliser "system()", mais system ne renvoie aucune valeur, nous ne pourrions pas avoir ce qu'affiche la console. La commande "exec()" est donc à utiliser.

exec('df -h',$tab);

Ici, nous récupérons donc dans $tab chaque ligne renvoyée par la commande. Ensuite, il faut afficher sous forme d'un tableau les valeurs intéressantes : la première ligne décrit les valeurs : total, used ... monted on... Ensuite, les partitions du disque dur sont décrites deux lignes par deux lignes : la première donne le nom du disque dur, la seconde donne les valeurs associées à la mémoire. Les étiquettes de la première ligne sont séparées par des espaces, mais il faut prendre en compte "Monted on" qui ne correspond qu'à une étiquette, donc, une seule colonne. On utilise donc str_replace pour recoller "Monted" à "on", et ensuite, preg_replace pour remplacer chaque mot (un mot est un ensemble de choses qui ne contient pas d'espaces) par une colonne (entre les balises <td>).

echo 'df -h :<br />
<table width="100%" border="1">';
echo '<tr>'.preg_replace('/(S+)/','<td>\1</td>',str_replace('Mounted on','Mounted_on',$tab[0])).'</tr>';

Nous allons donc maintenant afficher les valeurs. Nous commençons par lire la seconde ligne car la première contenait les étiquettes, puis nous nous décalons de deux lignes par deux lignes pour lire en dans $tab[$i] le nom de la partition, et $tab[$i+1] les valeurs qui qualifient la mémoire : un nombre, un point, un nombre, et une unité (un mot ou un pourcentage...), soit "/([0-9]+.?[0-9]+[\w%]+)/i".

for ($i=1;$i<count($tab);$i+=2){
    echo '<tr><td>'.$tab[$i].'</td>';
    $tab[$i+1]=preg_replace('/([0-9]+.?[0-9]+[w%]+)/i','<td>\1</td>',$tab[$i+1]);
    $tab[$i+1]=preg_replace('/s([a-zA-Z/0-9]+)$/i','<td>\1</td>',$tab[$i+1]);
    echo $tab[$i+1].'</tr>';
}
echo '<table>';

Voici le code correspondant pour la commande free :

$tab=array();
exec('free',$tab);
echo 'free :<br />
<table width="100%" border="1">';
echo '<tr><td></td>'.preg_replace('/(S+)/','<td>\1</td>',$tab[0]).'</tr>';
for ($i=1;$i<count($tab);$i++){
    echo '<tr>'.preg_replace('/s([0-9]+)/','<td>\1</td>',preg_replace('/^(.*):/','<td>\1</td>',$tab[$i])).'</tr>';
}
echo '</table>';

Voici le résultat sur ma machine personnelle (Intel Céléron 2.4 Ghtz, 512 Mo de RAM, avec un système Linux Mandrake 9.2). Pour free, les valeurs sont en Ko.

Filesystem Size Used Avail Use% Mounted_on
/dev/ide/host0/bus0/target0/lun0/part2 19G 3.0G 15G 18% /
/dev/ide/host0/bus0/target0/lun0/part1 20G 5.9G 14G 30% /mnt/windows

Free:

Total Used Free Shared Buffers Cached
Mem 514280 348952 165328 0 10084 174968
-/+ buffers/cache 163900 350380
Swap 811240 0 811240

On peut aisément utiliser ce principe pour afficher des fichiers logs, proposer une coloration syntaxique. Je n'ai pas présenté l'affichage des fichiers logs car sur une très grande partie des systèmes linux, ils sont protégés en lecture pour un utilisateur ayant des droits aussi restreints que PHP. Mais nous pouvons afficher des CSS : nous ne ferons dans ce fichier PHP que présenter les expressions régulières, le CSS est ici plutôt négligé. Nous commençons par mettre le fichier dans une variable :

<?php
$css=implode(file('site.css'));

Puis, nous remplaçons les sauts de lignes par des "
" pour l'affichage dans une page web. Pour cela, la fonction nl2br fait parfaitement son travail :

$css=nl2br($css);

Ensuite, nous traitons l'expression avec des expressions régulières pour apporter la coloration syntaxique : les parties entre { et } seront colorées en bleu, et les attributs seront colorés en rouges, les attributs sont entre : et ;.

$css=preg_replace('/({.*?})/i','<font color="#0077FF">\1</font>',$css);
$css=preg_replace('/(:s?S*?s?;)/i','<span style="color:#FF0000;">\1</span>',$css);

Puis, nous affichons le CSS. Je ne détaille pas cette partie.

echo $css;
?>

On pourrait ajouter les finitions, mettre d'une certaine couleur les unités, les points virgules, les accolades et les deux points, mais ce n'est pas le but de cette page puisque nous ferions cela sans utiliser les expressions régulières.

Les parseurs

Il existe un bon nombre de parseurs : un parseur XML est implémenté en PHP, Tidy est un parseur HTML libre fonctionnant sous PHP, mais nous pouvons faire nous même nos parseurs (soit pour réinventer la roue, soit pour ajouter des fonctionnalités, soit pour une satisfaction personnelle...). Ici, nous allons présenter une classe en PHP qui permet de parser du CSS. Donc, la classe se présente ainsi :

class Css{
    function Css($url){
        $this->url=$url;
        $this->text=implode(file($url));
    }

Le constructeur ouvre le fichier CSS à partir de son URL, place le texte contenu dans le fichier dans la variable $this->text. Pour parser, le plus simple est d'utiliser les split (éclate). Pour ne pas perdre de données, nous utilisons l'option "PREG_SPLIT_DELIM_CAPTURE" (sauf pour les séparations). Tout d'abord, nous cassons la chaîne avec les parties entre accolades, ce qui fait qu'une ligne sur deux sera une balise (ou une class), et une ligne sur deux ses propriétés. Ensuite, nous cassons une case sur deux de ce tableau : les cases qui correspondent aux propriétés des balises. Nous séparons ici les propriétés et les valeurs en cassant avec les deux points et les points virgules. Ici, nous ne gardons pas les cases vides grâce à l'option : "PREG_SPLIT_NO_EMPTY".

    function parse(){
        $this->parse = preg_split('/{(.*?)}/', $this->text, -1, PREG_SPLIT_DELIM_CAPTURE);
        foreach ($this->parse as $a=>$b){
            if ($a%2==1){
                $this->parse[$a]=preg_split('/([.: ])/', $b, -1, PREG_SPLIT_NO_EMPTY );
            }
        }
    }
    var $parse;
    var $url;
    var $text;
}

On écrit l'objet

$a=new Css('site.css');
$a->parse();
</php>
Nous allons afficher le résultat sous forme classique :
<code php>
print_r($a->parse);

Elargissement au Javascript

En règle générale, pour utiliser une expression régulière en Javascript, il faut la transformer très légèrement : ajouter un g à la fin (simple curiosité du langage...). Pour les utiliser, c'est légèrement différent. Javascript étant un langage bien plus orienté objet que PHP, les expressions régulières s'utilisent comme des méthodes sur l'objet string :

var str="LOL LOl lol Lol LoL lOl, lOL";
window.alert(str.replace(/lol/gi,'Lol'));

On obtiendra une boite d'alerte avec "Lol Lol Lol Lol Lol Lol Lol" de marqué dessus. Remarquons aussi que le premier paramètre est l'expression régulière, et que contrairement au PHP, elle n'est pas entre guillemets. On peut utiliser eval pour lancer une expression régulière à partir d'une variable, ou bien l'objet RegExp.

Conclusion

Grâce aux expressions régulières, on peut faire plein de choses, on peut traiter ou vérifier le bon format de chaînes de caractères, on peut récupérer des informations sur des pages internets (on peut par exemple trouver tout les liens externes, tout les liens internes, toutes les adresses emails, toutes les URLs, l'adresse du flux RSS correspondant à la page, les commentaires, les copyrights déposés sur les scripts...). Les expressions régulières ont fait la gloire du Perl, les maîtriser est un atout vraiment intéressant, et les utiliser fera de vos programmes des programmes attractifs et des pages web visitées...

Pour plus d'explications : http://fr.php.net/manual/fr/reference.pcre.pattern.syntax.php

Ce document intitulé « Les Regexps en PHP » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.