Mumuse avec les Regex
Par Clochix le jeudi 3 septembre 2009, 12:55 - les petits tutos à toto - Lien permanent
Un des mérites de PHP 5.3, à mes yeux, est de rendre obsolètes les
expressions régulières POSIX. En effet, PHP disposait jusqu'à
présent de deux bibliothèques d'expressions rationnelles, l'une inspirée de POSIX, l'autre de PERL. Ces deux
bibliothèques proposaient des fonctions similaires, mais en utilisant chacune
une syntaxe spécifique. C'est très bien d'avoir le choix, mais inconstant comme
je suis, je n'avais jamais réussi à choisir l'une ou l'autre et passais mon
temps à mélanger les syntaxes. C'est à présent fini, on peut dire adieu à
ereg, split et compagnie[1], et relire le manuel des expressions PCRE,
qui contient de nombreux trésors. Ces astuces n'ont sont pas, puisque tout est
dans le manuel, je me contente de vous en rappeler l'existence.
Notes
[1] commencez déjà à faire la chasse dans vos programmes, pour éviter les messages annonçant leur dépréciation lorsque vous passerez à PHP 5.3
Les expressions régulières sont un domaine où il est très simple de faire des erreurs. Si vous avez le courage de lire celles que je présente ici, et si vous y découvrez des bourdes, merci de me corriger.
Délimiteurs
Un masque est entouré de délimiteurs. On a coutume d'utiliser des
slash : /(toto)+/, mais en fait n'importe quel caractère non
alphanumérique peut servir de délimiteur. Ainsi, si on a besoin de rechercher
un slash dans une chaîne, on pourra soit le protéger d'un anti-slash, soit
utiliser d'autres délimiteurs : /.+\/.+/ ou
%.+/.+%
Les sous-masques
Les sous-masques, délimités par des parenthèses, ont plusieurs rôles:
- délimiter une expression qui devra être présente un nombre donné de fois:
- de zéro à n avec
* - zéro ou une fois avec
? - au moins une fois avec
+; - un nombre exact de fois avec
{};
- de zéro à n avec
- délimiter des alternatives :
(toto|titi|tata); - capturer des sous-chaînes, par exemple avec
preg_match;
Par défaut, chaque sous-masque est capturant, c'est à dire qu'il sera renvoyé dans les résultats. Imaginons, au hasard, que l'on veuille analyser une chaîne, composée d'un identifiant, éventuellement suivi d'un serveur (précédé de '@') éventuellement suivi d'un nom de ressource, précédé de '/'. On pourrait utiliser une expression du genre[1]:
$reg = '%([^@]+)(@([^/]+)(/(.+))?)?%'
On a ici cinq masques, les uns délimitant des expressions que l'on veut
capturer, les autres servant juste à déterminer la présence de ces expressions.
Ainsi preg_match($reg, 'toto@titi.org/tata', $res) retourne un
tableau de six éléments : la chaîne totale correspondant au masque, et le
contenu de chacun des sous-masques:
0 => toto@titi.org/tata 1 => toto 2 => @titi.org/tata 3 => titi.org 4 => /tata 5 => tata
On peut heureusement dire au moteur d'expression de ne pas capturer certains
sous-masques. Il suffit de les faire débuter par ?: :
$reg = '%([^@]+)(?:@([^/]+)(?:/(.+))?)?%';
ne retournera que quatre éléments : la chaîne totale, et les expressions capturées.
Cerise sur le gatal, on peut nommer les expressions capturées pour y accéder
non à partir de leur position dans le masque, mais par leur nom. Il suffit pour
cela de débuter le sous-masque par ?P<nom>. Plus de
re-numérotation des résultats si on rajoute des sous-masques, et une expression
un peu plus lisible, AMHA.
$reg = '%(?P<login>[^@]+)(?:@(?P<server>[^/]+)(?:/(?P<resource>.+))?)?%'; preg_match($reg, 'toto@titi.org/tata', $res); $login = $res['login']; $server = ($res['server']?$res['server']:'default'); $resource = ($res['resource']?$res['resource']:'default');
Pour rendre plus lisibles vos expressions, vous pouvez aussi y ajouter des
commentaires, encadrés de (?# et ).
Références arrières
Donner un nom aux sous-masques pour également s'avérer très utile lorsqu'on
utilise des références arrières, c'est à dire lorsqu'on utilise le résultat
d'un sous masque précédent. On peut faire cela soit avec un antislash suivi du
numéro du sous-masque, soit en écrivant (?P=name) (depuis PHP
5.2.4, d'autres
syntaxes sont disponibles mais celle-ci me semble la plus simple à
retenir).
Par exemple pour capturer des adresses mail de la forme "Toto Titi <toto.titi@tata>", les deux syntaxes suivantes sont équivalentes ;
preg_match('/(\S+)\s+(\S+)\s+<(?i)\1\.(?i)\2@[^>]+>/', "Toto Titi <toto.titi@tata>", $res);
preg_match('/(?P<first>\S+)\s+(?P<last>\S+)\s+<(?i)(?P=first)\.(?i)(?P=last)@[^>]+>/', "Toto Titi <toto.titi@tata>", $res);
(j'utilise ici en plus une option interne, (?i) indiquant que
le sous-masque suivant doit être insensible à la casse. Les chaînes "Toto Titi
<Toto.Titi@tata>", "Toto Titi <toto.titi@tata>" ou "Toto Titi
<TOTO.TITI@tata>" vérifieront donc toutes le masque.
Masques conditionnels
Il est possible de choisir d'utiliser un masque uniquement si une condition est remplie, voire d'en utiliser un autre si elle n'est pas remplie. La syntaxe générale est la suivante:
(?(condition)masque)(?(condition)masque|masque sinon)
La condition elle même peut être de trois types :
- une référence arrière;
- une assertion;
- un appel récursif. Dans ce cas, la syntaxe est de la forme
(?(Rn)masque)avec n le numéro du sous-masque à appeler récursivement. On peut aussi appeler récursivement les sous-masques par leur nom avec(R&name). Pour ne pas agrâver mon mal de crâne, je ne traiterai pas de récursivité ici, allez regarder la doc si ça vous intéresse.
Dans le cas d'une référence arrière, la condition sera vraie si la dite référence a capturé quelque chose. L'exemple le plus classique est la capture de balises fermantes si une balise ouvrante était présente. Pour capturer une adresse mail, éventuellement encadrée de < et > on pourra utiliser
/(<)?[^>\s]+(?(1)>)/: le(?(1)>)signifie qu'on ne capture le ">" final que s'il y en avait un au début- ou
/(?P<OPEN><)?[^>\s]+(?(<OPEN>)>)/
Les assertions elles sont des masques non capturant, c'est à dire qu'elle utilisent la même syntaxe que les masques, mais ne consomment pas les caractères qu'elles testent. On distingue cinq types d'assertions :
- les assertions "simples" : par exemple
^sera vraie si le caractère suivant est le premier de la ligne. On utilise aussi$pour la fin de ligne,\bpour une limite de mot (début ou fin),\B,\A,\Z,\z; - les assertions vrai si les caractères suivants remplissent la
condition :
(?=). Par exemple\w+(?=;)ramènera les mots suivis d'un point virgule, mais celui-ci ne sera pas inclus; - les assertions fausses si les caractères suivants remplissent la
condition :
(?!). Par exemple/\w+\b(?!;)/pour les mots qui ne sont pas suivis d'un point-virgule. Notez l'utilisation du\bpour marquer la limite de mot; - les assertions vrai si les caractères précédents remplissent la
condition :
(?<=):/(?<=-)\d+/trouve les nombres négatifs et ramène leur valeur absolue; - les assertions fausses si les caractères précédents remplissent la
condition :
(?<!)./(?<!-)\b\d+/trouve les nombres positifs (avec encore une fois l'utilisation de\b, sinon l'expression ramène les chiffres non précédés de -, et non les nombres;
Et on en arrive donc à nos masques conditionnels. Imaginons que l'on veuille tester une chaîne composée d'un mot, une date et un mot, séparés par ':', sachant que la date peut être au format JJ.MM.AAAA ou AAAA.MM.JJ . On pourra utiliser l'expression suivante :
/\S+:((?=\d{4})\d{4}\.\d{2}\.\d{2}|\d{2}\.\d{2}\.\d{4}):\S+/
Elle est bien de la forme ((?condition)oui|non), avec comme
condition que la deuxième partie de la chaîne commence par quatre chiffres
suivis d'un point. Si c'est le cas on utilise le masque
d{4}\.\d{2}\.\d{2}, sinon \d{2}\.\d{2}\.\d{4}.
Correction suite au commentaire de TiTerm : mon exemple est
bidon, la condition ne sert à rien puisque le moteur peut choisir de lui-même
entre les 2 alternatives. c'est à dire que
/\S+:(\d{4}\.\d{2}\.\d{2}|\d{2}\.\d{2}\.\d{4}):\S+/ fera la même
chose. Bon, je sèche sur un exemple fonctionnel.
Voilà, je laisse pour l'instant de côté les masques récursifs et la possibilité de définir des masques pour une utilisation ultérieure. Il est utile de savoir que ça existe, mais j'avoue ne pas assez en maîtriser l'usage pour m'y aventurer.
Note : ce billet était sponsorisé par l'aspirine Lama Lalatêt.
Notes
[1] composer un billet sur les expressions régulières dans
un éditeur utilisant des expressions régulières pour formater le texte, c'est
du bonheur 
Commentaires
Juste une remarque pour l'exemple final,
Amha, l'assertion positive ?= ne sert à rien. Vu que pour les dates tu mets des alternatives, si la première ne match pas, la seconde sera automatiquement testé
Merci TiTerm, tu as tout à fait raison, j'ai rajouté un mot dans le billet. A trop chercher des exemples j'ai perdu tout recul. Je suis prenneur d'un exemple "réel" d'utilisation des conditions.
Tout d'abord, toute mes félicitations pour l'effort qu'a du te demander la rédaction de ce billet ;.
Ensuite, je me permet de recommander le livre de référence à mes yeux pour tout ce qui concerne les regex : Mastering regular expression, aux éditions O'reilly (http://oreilly.com/catalog/97805960...).
En plus du fait de décrire la syntaxe des regexs, il explique le fonctionnement du parser, ce qui permet de comprendre énormément de choses et d'optimiser son code.Seul bémol, le tube d'aspro n'est pas fourni avec, et il y a tout intêret à en acheter un ou deux à la pharmacie du coin avant d'en entamer la lecture ;).
Pour compléter, je recommande l'outil RegexBuddy (http://www.regexbuddy.com/)
qui pour moi, est probablement le meilleur du genre.
L'editeur propose aussi un site sur les regex qui est très clair et relativement complet.
http://www.regular-expressions.info...
Je n'ai pas d'exemple réel pour ?=
Pour un exemple réel, c'est pas évident et je risque de trébucher de la même façon que toi:). Je pense qu'il faut bien faire valoir le fait que l'assertion test mais ne consomme pas de caractère contrairement aux parentheses même non capturantes
Supposons que nous voulions récupérer la partie lettrée d'une référence type abc123-45
Soit on fait:
[a-z]+(?=\d+-\d+) et ce qui match sera uniquement abc si ce abc est suivi d'un nombre-nombre, en retour du preg_match, il n'y aura que abc dans le match global [0]
Soit on fait:
([a-z]+)(?:\d+-\d+) et dans le retour, il y aura
abc123-45 dans [0] et abc dans [1]
La seconde syntaxe a bien matché la totalité de la référence et retourné les sous partie capturée quand la première syntaxe ne retourne que la partie qui nous intéresse sans capture