Regexp en PHP, le mémo indispensable

Tout ce qu'il faut savoir sur les expressions régulières

28 décembre 2014

Les expressions rationnelles, aussi appelées expressions régulières et abrégées regex ou regexp, sont des outils extrêmement utiles pour rechercher, segmenter et travailler une chaîne de caractères. Il existe de nombreux moteurs de regexp (Perl, POSIX, Java, .NET, Javascript, XML path etc...) et chacun possède ses spécificités, que ce soit des fonctionnalités absentes ou une interprétation différente du même pattern.

Bien que ce qui sera présenté ci-dessous soit compatible avec la plupart des moteurs, cet article se concentrera sur l'extension PCRE (Perl Compatible Regular Expressions) de PHP. Pour suivre l'article, vous devrez donc utiliser les fonctions commençant par preg (preg_match, preg_replace etc...), nous utiliserons le slash "/" pour délimiter la regexp : preg_match(/$pattern/, $subject, $output).

Les bases

Cette partie sera relativement courte et traitera, sans entrer dans le détail, des bases pour écrire des regexp.

RegexExplicationsExemple
^Début de la chaine./^foo/ : commence par foo
$Fin de la chaine./bar$/ : termine par bar
[ ]Un ensemble de possibilités appelé "classe"./[abc]/ : soit un a, un b ou un c
[^ ]Un ensemble de caractères interdits./[^abc]/ : ne soit pas un a, ni un b, ni un c
?Présence du caractère 0 ou 1 fois./as?/ : "a" ou "as" car le "s" est rendu optionnel
*Présence du caractère 0 ou X fois./as*/ : "a", "as", "asssss".
+Présence du caractère obligatoire, pouvant se répéter X fois./as+/ : "as", "asssss".
{ }Un nombre de répétitions./as{2}}/ : "ass". /as{2, 4}}/ : "ass", "asss", "assss". /as{2,}}/ : "assssssss".
[0-9] ou \dUn chiffre. "d" : digit./\d+/ : 0, 1, 2, 3
[A-Za-z0-9_] ou \wUn chiffre, une majuscule, une minuscule ou un underscore. "w" : word./\w+/ : Informatix
\s ou [ \t\r\n\f]Un espace, une tabulation, une fin de ligne, une fin de page. "s" : space./a\sb/ : a b
\D, \W et \SEn général mettre une classe en majuscule sert à l'inverser. \D = [^\d] = tout ce qui n'est pas un chiffre./[\s\S]/ : tous les caractères. Ce qui est un espace ou ce qui n'est pas un espace.
.N'importe quel caractère à l'exception des sauts de lignes. Pour trouver n'importe quel caractère il y a plusieurs possibilités : utiliser [\s\S] ou mettre l'option "single-line mode" (s)./le .ien/ : trouve "le tien", "le sien", "le mien" etc...
( )Faire un groupe. Par défaut un groupe est capturant, vous pourrez utiliser la valeur entre parenthèses plus tard dans la regexp ou en sortie de preg_match par exemple.preg_match('/a(.+)e/', 'abcde', $output) : $output[1] = 'bcd'
[:alnum:], [:digit:], [:alpha:], [:lower:], [:upper:] etc...Classes au format POSIX pour matcher respectivement : un alphanumérique, un numérique, un alphabétique, une minuscule, une majuscule./([[:alpha:]]+)/ = /([a-zA-Z]+)/

Les opérateurs fainéants

Par défaut les opérateurs "*" et "+" sont gourmands (greedy), cela signifie qu'ils essaieront de capturer le maximum de choses. A l'inverse, le concept de fainéantise consiste à ne capturer que le minimum possible. En ajoutant un "?" derrière "*" et "+" on rend ces opérateurs fainéants (lazy). Exemple pour capturer une balise XML :

RegexSujetExplications
<(.+)><b>Informatix</b>Capture gourmande : b>Informatix</b. Ce n'est pas ce que l'on veut.
<(.+?)><b>Informatix</b>Capture fainéante : b
<([^>]+)><b>Informatix</b>Parfois il vaut mieux réfléchir à une solution alternative. Il est ainsi plus rapide de capturer tous les caractères qui ne sont pas ">" suivi de ">" que de faire appel à la fainéantise.

Trouver un mot précédé ou suivi d'un autre

RegexExplications
foo(?=bar)Assertion avant positive (positive lookahead) : trouve foo qui est suivi de bar.
foo(?!bar)Assertion avant négative (negative lookahead) : trouve foo qui n'est pas suivi de bar.
(?<=foo)barAssertion arrière positive (positive lookbehind) : trouve bar qui est précédé de foo.
(?<!foo)barAssertion arrière négative (negative lookbehind) : trouve bar qui n'est pas précédé de foo.

Attention avec les assertions arrières, les moteurs ont un peu de mal si elles sont compliquées. Si vous pouvez tourner votre regexp différemment, faites-le ! Le contenu matchant l'assertion positive n'est pas capturé, si vous souhaitez le faire il faut mettre des parenthèses dans l'assertion. Exemple : /(foo(?=bar))/ capturera "foo" alors que /(foo(?=(bar)))/ capturera "foo" et "bar".

Un exemple un peu plus poussé pour trouver une balise script qui ne contient pas d'attribut src : /(<script(?!.*?src=(['"]).*?\2)[^>]*>)/. Cette regexp cherche une balise script qui n'est pas suivie de : rien ou plusieurs caractères suivis de src="(rien ou plusieurs caractères)" ou src='(rien ou plusieurs caractères)' suivi de rien ou plusieurs caractères qui ne sont pas ">" suivis de ">".

Les groupes et la récursivité

Précédemment nous avons vu qu'il suffisait d'entourer de parenthèses un ensemble pour en faire un groupe capturant. Dans cette section nous allons voir comment nommer ce groupe, le rendre non capturant et le rappeler dans la suite de la regexp. Comme vous pourrez le constater, en ce qui concerne les groupes, il existe de nombreuses syntaxes pour faire la même chose.

DescriptionExplication / Regex
Faire un groupe non capturant.(?:.+)
Nommer un groupe pour pouvoir le rappeler dans la regex ou dans le tableau de sortie de preg_match.(?'un_nom'.*) ou (?P<un_nom>.*).
Rappeler la capture exacte d'un groupe précédent : la référence arrière (backreference).(?P=un_nom), \k'un_nom', \k{un_nom} ou s'il n'est pas nommé \1, \g1. Exemple : /<(?'balise'[bs])[^>]*>.*<\/\k'balise'>/ matchera <b>informatix</b> mais pas <b>informatix</s>.
Rappeler la regexp d'un groupe précédemment défini (subroutine).(?P>un_nom), (?&un_nom) , \g'un_nom' ou s'il n'est pas nommé \g'1', \g<1>, (?1). Exemple : /(?'test'[ab])(?&test)c/ match aac, abc, bbc, bac car la regexp est équivalente à /(?'test'[ab])[ab]c/.
Définir une subroutine au début de la regex avec le mot clé DEFINE./(?(DEFINE)(?'age'\d{1,3} ans))^Age: \g'age'$/ on défini la subroutine "age" pour l'utiliser par la suite. Cette regex match "Age: 25 ans".
Rappeler l'intégralité de la regex : récursivité.(?R), (?0), \g<0>. Exemple : /a-(?R)?z/ match a-z, a-a-zz, a-a-a-zzz etc...
Remettre la capture globale à zéro.\K. Exemple : /(ab\Kc)/, $output[0] = "c" et $output[1] = "abc".
Différentes branches de capture (branch reset groups). Exemple : nous souhaitons parser du JSON {"foo": "bar"} ou {"foo": 42}. Nous voulons la clé dans $output[1] et la valeur dans $output[2]./{"([^"]+)": (?|(\d+)|"([^"]+)")}/ nous avons trois groupes de capture mais grâce à la syntaxe (?|(a)|(b)) les deux derniers groupes correspondent à $output[1]. Attention, si vous nommez les groupes, toutes les alternatives, bien que pouvant ne pas avoir le même nombre de groupes, doivent avoir le même enchainement de noms.

Les groupes atomiques et les opérateurs possessifs

Les opérateurs classiques "*", "+", "?", "{2, 3}" peuvent parfois être lourds en terme de traitement, surtout s'ils sont profondément imbriqués dans des groupes. Prenons un exemple simple : /<.+>/ sur "<img>". Le moteur trouve "<" directement. Pour lui, ".+" match "img>". En essayant de trouver ">" il se rend compte que ça ne colle pas, il fait donc un retour arrière d'un caractère (backtrack). Puisque ".+" vaut maintenant "img", il arrive finalement à trouver ">".

Maintenant faisons la même chose avec la regexp /<[^>]+>/ et la chaîne "<img". Le moteur va essayer de faire correspondre "[^>]+" à "img" avant de se rendre compte qu'il ne trouve pas ">". Il essaiera ensuite un backtrack : "[^>]+" vaut "im" et il constate que "g" ne match pas ">". De nouveau, le moteur fait un retour arrière pour qu'à la fin ça ne corresponde pas. Si le moteur était assez intelligent, il se serait rendu compte qu'avec la regex "[^>]+" il n'a pas pû louper de ">" et qu'il était donc inutile de faire tous ces coûteux retours en arrière.

Les opérateurs possessifs servent justement à interdire les retours arrières, ils allégent donc le traitement. Ce sont les opérateurs habituels auxquels nous collons un "+" à la suite. Attention, il faut bien réfléchir avant de les utiliser ! Si nous reprenons l'exemple /<.+>/ que nous transformons en /<.++>/, comme avant le moteur va échouer à trouver ">" au premier essai. Avec ".+", nous avons vu ci-dessus qu'au second essai, la regexp matchait. Avec ".++" il n'y a pas de retour arrière donc pas de second essai : le moteur renverra un échec !

Les groupes atomiques sont utilisés pour les mêmes raisons de gains de performance, d'ailleurs les opérateurs possessifs sont une facilité d'écriture pour des groupes atomiques simples. Ainsi ".++" est l'équivalent du groupe atomique (?>.+). Dans un groupe atomique, si le pattern contenu dans le groupe match, le moteur passe à la suite sans donner la possibilité, en cas d'échec, de revenir sur ce pattern et d'évaluer d'autres alternatives. Ainsi /(?>foobar|foobarbaz)\b/ appliquée à "foobarbaz" renvoie un échec. En effet, "foobar" est bien contenu dans "foobarbaz" mais ensuite "\b" est introuvable, le moteur ne retourne pas en arrière et indique un échec.

Autres

DescriptionExplication / Regex
Les frontières de mots pour trouver un mot exacte (word boundaries).\b équivalent de (^\w|\w$|\W\w|\w\W). Exemple : preg_replace ('/\bart\b/', 'REMPLACEMENT' , "Cet article c'est de l'art") : "Cet article c'est de l'REMPLACEMENT".
Ne pas avoir besoin d'échapper les caractères spéciaux.Tout ce qui se trouve entre \Q et \E sera interprété comme du texte brut et ne sera pas évalué comme une regexp. Exemple : \Q*\d+*\E match littéralement *\d+* et non un nombre.
Faire une condition.La syntaxe générale d'une structure conditionnelle : (?(condition) alors|sinon). Exemple, tester si un groupe existe : /alpha(num)?(?(1)[[:alnum:]]|[[:alpha:]])+/. Dans cet exemple on cherche un alnum si le groupe contenant "num" existe, sinon on cherche un alpha. Il est bien sûr possible de remplacer le 1 par le nom d'un groupe.
Traiter de l'unicode.\X est l'équivalent unicode du point. \x{1234} correspond au caractère unicode U+1234. Unicode dispose de ses propres classes, par exemple : \p{Lowercase_Letter} pour matcher un caractère minuscule ou \p{Arabic} pour un caractère Arabe.
Rendre certaines parties de la regex insensibles à la casse/(?i)insensible(?-i)sensible/
Ne pas tenir compte des espaces dans les tokens./(?x) \d +/ = /\d+/
Mettre des commentaires.(?x) a aussi la particularité d'ignorer sur chaque ligne ce qui se trouve après le caractère #, ce qui est très pratique pour mettre des commentaires. Sinon, placer (?#commentaire) n'importe où dans une regexp fait très bien le travail aussi.

Vous pourrez trouver toutes les informations détaillées sur chacun des points abordés sur le site : regular-expressions.info.

Si vous trouvez des fonctionnalités oubliées, n'hésitez pas à poster un commentaire.

A bientôt !

Par
Créateur et administrateur.

Dans la même catégorie

La fonction isset et la valeur null
Les différentes façons de fusionner deux tableaux en PHP
Le lazy load en PHP
La priorité des opérateurs en PHP

Commentaire(s)