La puissance des REGEX
Pour ceux qui ne savent pas ce que c’est, les expressions régulières, combinées à des fonctions de certains langages (PHP, bash, JavaScript et même HTML5 !) permettent de faire des recherches et de la reconnaissance sur des chaines de caractères. Extraire des numéro de téléphone d’une page web, ou vérifier que l’email que rentré dans un formulaire, ressemble bien à un email… C’est très puissant !
Les regex (ou regexp) intimident, cependant, la théorie n’est pas des plus complexe et il s’agit surtout de pratiquer pour gagner en expérience. Pour la pratique, le plus simple est d’utiliser un éditeur en ligne. Je recommande regex101.com car il supporte plusieurs langages (Python, Go, JavaScript et PHP) et qu’il inclut des petites fiches mémo.
En outre, pour des regex complexes, vous pouvez utiliser regexper. Il s’agit d’un site vous permettant de visualiser le fonctionnement d’une votre regex, vraiment très puissant.
Anatomie d’une REGEX
Une regex est faite pour effectuer des recherches dans les chaînes de caractères… et une regex est elle-même une chaîne de caractère.
Elle possède un délimiteur qui en indique le début et la fin ainsi que des caractères spéciaux. Les caractères spéciaux permettent d’indiquer des comportement prédéfinis. Par exemple désigner un sensemble caractères, indiquer la longueur d’un mot, une longueur variable, indiquer qu’on ne veut que des majuscules, un mot optionnel etc.
Une fois la regex créé, chaque langage de programmation dispose de ses propres fonctions pour les utiliser. Certaines fonctions permettent de contrôler la présence de certains éléments dans une chaîne, de la nettoyer en supprimer certains éléments ou encore d’extraire du texte depuis une chaîne.
Par exemple, la REGEX /[0-9]+ ans/
permet de matcher l’âge dans une chaîne. Si on veut extraîre cette information depuis une chaîne de caractère, voici comment on peut faie en PHP et JavaScript.
<?php
$text = 'Je suis un licornet de 20 ans';
$regex = '/[0-9]+ ans/';
preg_match($regex, $text, $match);
var_dump($match);
// output
array(1) {
[0]=>
string(6) "20 ans"
}
const text = 'Je suis un licornet de 20 ans';
const regex = /[0-9]+ ans/;
const match = text.match(regex);
console.log(match); // ["20 ans"]
L’objectif ici n’est cependant pas de vous apprendre à vous utiliser les regex dans un langage particulier mais de vous expliquer les regex en elles-mêmes.
POSIX et PCRE
Lorsque l’on parle de REGEX, il faut savoir qu’on peut rencontrer différentes variantes. En effet, certains masques ne fonctionneront pas forcément sur toutes les plate-formes et dans tous les langages.
POSIX est un standard qui a cherché à uniformiser les syntaxes et les fonctionnalités des expressions régulières. Les expressions de type POSIX seront plutôt bien supportées dans la console Linux par exemple.
Cependant, leur support étendu (le reste étend commun avec PCRE) est plus restreint, notamment les classes POSIX dont je parle dans l’article. PHP ne supporte par exemple plus la syntaxe POSIX dans ses dernières versions.
PCRE désigne un type de REGEX qui s’appuie sur la syntaxe des REGEX du Perl. C’est la syntaxe la plus largement supportée aujourd’hui, bien que selon les langages et les implémentations, certaines légères différences puissent apparaitre.
Cependant, pas d’inquiétude, tout ce que nous allons voir ici fait partie des standards adoptés par la majorité des langages. Vous n’aurez donc aucun problème pour adapter vos REGEX à vos cas particuliers.
Délimiteurs
#
, %
, /
etc.
Ils servent à délimiter ce qui fait parti de votre expression, de ce qui n’en fait pas parti. C’est donc en dehors des délimiteurs que vous placerez les options PCRE, POSIX n’ayant pas d’options, ni de délimiteurs d’ailleurs. Le choix du délimiteur est totalement libre (dans la mesure où c’est un caractère spécial), cependant, prenez un caractère assez rare. Inutile de tenter d’utiliser un slash “/“ si vous pensez travailler sur des URL…
Les métas-caractères
Ces caractères ont chacun une signification spéciale dans les expressions régulières. C’est notamment eux qui font la force des REGEX.
Signe | Signification | Exemple |
---|---|---|
^ | marque un début chaine | /^music/ (commence par music) |
$ | marque une fin de chaine | /^music$/ (commence et termine par music) |
| | connecteur logique ou | /music | musique/ (music ou musique) |
. | tous les caractères sauf les retour charriot \n (il faut pour ça utiliser l’option s) | /./ (match presque tout) |
\ | caractère d’échappement | /?/ (signifie que le “?” compte ici comme un caractère normal) |
Note : tous les métas-caractères doivent être échappés. Nous ne les avons pas encore abordés, mais les quantificateurs, les parenthèses qui précisent le nombre et les crochets qui marquent les classes de caractères sont aussi des métas-caractères qu’il convient d’échapper. Par ailleurs, l’antislash d’échappement doit aussi être échappé par… lui même.
Quantificateurs
Les quantificateurs permettent de préciser le nombre de fois que l’on autorise un caractère ou une suite de caractères à se répéter.
Signe | Signification | Exemple |
---|---|---|
? | 0 ou 1 fois | /bue?no/ (buno, ou bueno) |
+ | 1 ou plus | /bue+no/ (bueno, bueno, bueeeeeeno…) |
* | 0, 1 ou plus | /bue*no/ (buno, bueno, bueeeeeeno…) |
( ) | permet d’appliquer répétition sur plusieurs signes | /Ay(Ay)*/ ( Ay, AyAy, AyAyAyAyAyAy…) |
{ } | préciser le nombre de répétitions | * /Ay(Ay){3}/ (AyAyAyAy)* /Ay(Ay){1-4}/ (AyAy, AyAyAy […] AyAyAyAyAy)* /Ay(Ay){3,}/ (AyAyAyAy ; AyAyAyAyAy ; etc) |
Vous l’avez peut-être remarqué mais :
{0,1}
revient à utiliser ?{1,}
revient à utiliser+
{0,}
revient à utiliser*
Ce sont des besoins fréquents et c’est bien la raison pour laquelle ces trois raccourcis ont été créé. Vous conviendrez qu’il est plus court d’écrire ?
que {0,1}
!
Les parenthèses permettent ici de grouper des éléments pour leur appliquer une répétition. En plus de cela, elles sont utiles lors de l’usage du |
. Ainsi, elles groupent les éléments sur lesquels porte le ou.
Par conséquent, /Buzut est le (meilleur|plus fort)/
correspond aux phrases “Buzut est le meilleur” et “Buzut est le plus fort”.
En revanche, si on omet les parenthèses, alors la regex /Buzut est le meilleur|plus fort/
correspond soit à “Buzut est le meilleur” soit à “plus fort” ; ce qui n’a rien à voir !
Classes et intervales
Les classes permettent de recherche entre plusieurs caractères différents, elles donnent des alternatives. Les intervales sont des classes un peu spéciales puisqu’elles permettent d’énumérer une certaine palette de chiffre ou de lettres. Par exemple, tous les chiffres de 0 à 5, ou toutes les lettres de a à i, sans les énumérer une par une.
Signe | Signification | Exemple |
---|---|---|
[ ] | classe de caractères | /gr[oai]s/ (gros, gras ou gris) |
[ - ] | intervalle de classe | /n°[0-9]/ (n°1, n°2, […] n°9) |
[^ ] | classe à exclure | /h[^3-9]/ (h1 et h2 uniquement) |
Quelques particularités : dans un classe, le tiret “-“ sert de délimiteur, donc si on veut l’inclure en tant que caractère, on doit le placer en fin de classe (ou au début). Par ailleurs, le crochet fermant “]” délimite aussi la fin de le classe, il faudra donc l’échapper par un antislash.
En revanche, les autres métas-caractères ne comptent pas dans les classes. On ne les échappe pas. Cette classe [0-9a-z?+*{}.]
correspond donc à un chiffre, une lettre, un point d’interrogation, un point, un plus…
Classes abrégées
Les classes abrégées permettent, comme les classes “normales”, d’avoir de nombreuses possibilités. Elles n’apportent rien de plus en fonctionnalité que les classes normales, mais elles permettent d’écrire tout ça bien plus vite, ce sont des raccourcis ! Que diriez vous si vous pouviez taper \w
à la place de [0-9a-zA-Z_]
?
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 ! |
Classes nommées
Il y a un autre type de classes toutes faites, qui permettent d’économiser un paquet de temps. Ce sont des classes nommées et comme les classes abrégées, elles permettent de faire les choses en plus court ! Elles sont relatives aux expressions régulières POSIX.
Avant de les utiliser, attention toutefois au support de POSIX.
Nom de la classe | Description |
---|---|
[:alnum:] | caractères alphanumériques (équivalent à [A-Za-z0-9]) |
[:alpha:] | caractères alphabétiques ([A-Za-z]) |
[:blank:] | caractères blanc (espace, tabulation) |
[:ctrl:] | caractères de contrôle (les premiers du code ASCII) |
[:digit:] | chiffre ([0-9]) |
[:graph:] | caractère d’imprimerie (qui fait une marque sur l’écran en quelque sorte) |
[:print:] | caractère imprimable (qui passe à l’imprimante … tout sauf les caractères de contrôle) |
[:punct:] | caractère de ponctuation |
[:space:] | caractère d’espacement |
[:upper:] | caractère majuscule |
[:xdigit:] | caractère hexadécimal |
Capture et références
Nous l’avons vu, les parenthèses ()
permettent de grouper plusieurs signes afin d’appliquer une condition, une répétition etc. Cependant, les parenthèses possèdent une autre fonction : elles sont capturantes.
Qu’est-ce que cela veut dire ? Ça veut dire qu’une expression mise entre parenthèse est automatiquement placée dans une variable à laquelle ont peut faire référence ailleurs.
On peut faire référence aux expressions capturées à deux endroits :
- Dans la REGEX elle-même, cela s’appelle une backreference ou référence arrière. On peut ainsi sélectionner les palindromes de trois lettres (mots qui se lisent indifféremment de gauche à droite ou de droite à gauche) avec cette expression
/(\w)\w\1/
.(\w)
sélectionne tout caractère alphanumérique et capture ce caractère, suivi d’un autre caractère puis du caractère précédemment capturé (donc l’expression entre parenthèses, soit la première lettre sélectionnée) :
<mark>SOS</mark> je suis fais des <mark>gag</mark> !
Il peut y avoir plusieurs backreferences dans une même expression, la première est indiquée par \1, la seconde \2 et ainsi de suite.
- Dans le résultat retourné par la fonction invoquée. Ici, cela dépend du langage et de la fonction utilisée. Par exemple en Javascript :
const birth = 'Je suis né en 1990 à Lyon';
console.log(birth.match(/^Je suis né en ([0-9]{4})/)); // ["Je suis né en 1990", "1990"]
On obtient d’abord le match global en index 0, puis dans leur sens d’apparition les résultats des groupes capturants.
Comme on ne peut en général capturer que neuf éléments (en Javascript par exemple, vous ne pourrez pas aller au delà de $9), il peut être intéressant de préciser qu’un couple de parenthèse utilisé à des fin de groupement est non capturant. Il faut pour cela placer ?:
juste après la parenthèse ouvrante ex. /(?:[0-9]{4})/
.
Lookahead et lookbehind
Les lookahead et lookbehind sont des types de références un peu spéciales. Elles permettent de matcher un élément en fonction de son contexte.
lookahead veut dire que l’on regarde en avant, donc on pourra sélectionner un élément en fonction de ce qu’il y a, ou n’y a pas, après lui. Le lookahead s’exprime par des parenthèses, comme les groupes de captures, mais on y ajoute la chaîne ?=
. Exemple :
const birthQuent = 'Je suis Quentin et je suis né en 1990 à Lyon';
const birdtRoger = 'Je suis Roger et je suis né en 1978 à Paris';
const regex = /en ([0-9]{4}) (?=à Lyon)/;
console.log(regex.exec(birthQuent)); // ["en 1990 ", "1990"]
console.log(regex.exec(birdtRoger)); // null
Lorsqu’on sélectionne selon ce qu’il n’y a pas, on parle de lookahead négatif. Le principe est le même mais on remplace =?
par =!
. Comme un exemple vaut mille mots :
const birthQuent = 'Je suis Quentin et je suis né en 1990 à Lyon';
const birdtRoger = 'Je suis Roger et je suis né en 1978 à Paris';
const birthSixt = 'Je suis Sixtine et je suis né en 1994 à Bordeaux';
const regex = /en ([0-9]{4}) (?!à Paris)/;
console.log(regex.exec(birthQuent)); // ["en 1990 ", "1990"]
console.log(regex.exec(birdtRoger)); // null
console.log(regex.exec(birthSixt)); // ["en 1994 ", "1994"]
Penchons-nous maintenant sur le lookbehind. Ce dernier fonctionne de la même manière mais il match les expressions qui sont (ou ne sont pas) précédées par ce qu’il y a dans le lookbehind. Les notations sont donc respectivement pour le positif et le négatif (?<=)
et (?<!)
.
const alex = 'Codename 006 – Alec Trevelyan';
const james = 'Codename 007 – James Bond';
const regex = /(?<=007 – )([A-Z][a-z]+ [A-Z][a-z]+)/;
Inutile de vous en dire plus, vous avez parfaitement compris. Attention toutefois, le lookbehind n’est supporté en JavaScript qu’en ES2018. Il n’est donc pas encore vraiment supporté par les navigateurs. Pour langages côté serveur, aucun problème.
Options
Comme expliqué au début de l’article, les regex POSIX n’ont pas d’options, ceci est donc valable pour PCRE uniquement. En outre, tous les langages ne proposent pas forcement les mêmes options. Il vaut donc mieux se référer directement aux documentations de vos langages (PHP et JavaScript).
Conclusion
Peu importe votre domaine de programmation et le langage utilisé, tôt ou tard les regex sont l’outil qu’il vous faut. Fort de ces connaissances, vous pouvez régler à peu près tous les problèmes solvables par des regex. N’oubliez pas également que regex101 possède une bibliothèque de regex prêtes à être utilisées.
Enfin, gardez à l’esprit que de la même manière que du code, on n’obtient pas forcement la bonne solution du premier coup, alors testez !
Commentaires
MagiCrazy dit –
Je trouve les versions POSIX trop obscures pour être utilisées. je préfère donc la bonne vieille syntaxe, bien plus compatible en plus. Par contre, les expressions rationnelles (et non "régulières" qui est un anglicisme éhonté), c'est bien, mais pas avec tous les langages. Perl, Ruby, OK. Mes derniers tests sur PHP sont effroyables (5.1 je crois), Python on n'en parle pas...
Les regexp, quand c'est simplement pour valider certaines entrées utilisateurs, c'est bien, mais quand c'est du gros traitement batch, on a plutôt intérêt à trouver un langage qui est performant !
Sinon /Au\sboulo(t)?/ ^^
Buzut dit –
Ah je savais pas que ça prenait un "t", je connaissais le bouleau (l'arbre) mais l'autre… Je le laisse comme ça, sinon ta blague n'aura plus de sens :)
Désolé pour "régulière" c'est malheureusement entré dans le langage ! Et oui, pour POSIX, ça fait souvent merder les scripts donc je ne l'utilise jamais. Par contre, pourquoi t'as des problèmes avec PHP ? Je trouve que #preg_[a-z]+# fait des merveilles.
Et sinon, pour le langage performant tu penses à quoi ?
MagiCrazy dit –
oui, le boulot (travail) prend un "t" ^^ Sinon je sais pour "régulière", mais, même si ça sert à rien (y'a qu'à voir CCM, SdZ ou Dev.net), je milite ^^ Donc POSIX ça fait merder les scripts dès qu'on les fait tourner sur un système non-POSIX justement, et puis, ça reste très vague et un [:alnum:] est pas forcément plus compréhensible qu'un [A-Za-z0-9] =) Donc avec PHP, ce sont précisément les perfs qui m'ennuient, mais il est vrai qu'après des années dedans (7 années de dev PHP à mon actif), PHP se fait vieux, a désormais l'inertie d'une baleine sur une plage et ne m'apparaît plus comme un langage d'avenir. Ca reste un avis personnel tout ça ^^
Pour les perfs, de ce que j'ai essayé en "langages de script", Perl et Ruby s'en sortent vraiment mieux que PHP ou Python lorsque l'on batche un fichier texte de 2G ! :) J'avoue ne pas utiliser le bash autre que pour du one-line lorsqu'une petite modification s'impose, mais mes besoin suivant un match/replace sont souvent une réorganisation totale du fichier !
Buzut dit –
"PHP se fait vieux, a désormais l’inertie d’une baleine sur une plage et ne m’apparaît plus comme un langage d’avenir"
PHP prend de l'inertie mais il devient plus complet… On ne peut pas tout avoir !
Bizarre pour Python plus lent que Perl, il a pourtant la réputation d'être très rapide !
Quoi qu'il en soit, même si j'aime bien PHP, je m'essayerai surement à Python ou Ruby et à leurs frameworks web respectif. (va savoir pourquoi je suis plus tenté par python)
Jamais tenté le fichier texte de 2G. Chez moi, ce genre de truc en général, c'est renommer quelques fichiers ou changer deux trois instructions dans des fichiers éparpillés par ci par là et un sed avec for do done fait très bien le travail :D
Mais 2G, c'est quoi ? Un dump de bdd ?! ou une compilation de wikipedia ^^
MagiCrazy dit –
Mon sentiment est que PHP n'évolue plus assez vite pour suivre ses concurrents, ça me laisse une vraie impression de stagnation (comme l'histoire du PHP6, annulé, confirmé, annulé....).
Sinon, je n'ai pas dit que Python était plus lent que Perl, sauf quand on parle de matching sur des regexp ^^ Historiquement, Perl est plutôt fait pour traiter du fichier texte et en manipuler les données, Python, plutôt pour du batching, soit pour le système, soit pour réaliser une tâche très répétitive ! J'ai une préférence pour le Ruby, mais c'est surtout à cause de sa syntaxe, assez originale et très intuitive. Les frameworks web valent tous le coup, ils ont tous une grosse base commune, et les quelques spécificités de chacun ne peuvent généralement pas influer sur le choix pour un projet, tant elles sont minimes...
Pour les fichiers de 2GB, c'est à mon travail qu'on traite ce genre de fichiers, et j'ai juste sorti un exemple, il peut aussi y avoir une multitude de petits fichiers, ou des fichiers gigantesques ^^ Ce sont principalement des données formattées.
Buzut dit –
C'est vrai que si tu prends l'exemple de PHP6… Enfin bon, PHP est tellement omniprésent qu'il parait difficile de le voir petit à petit abandonné. Et puis avec PEAR et PECL on peut faire des choses ! Enfin, c'est toi le spécialiste :) Et puis tant pis pour PHP, s'il y a mieux, on se tournera vers mieux. Je ne me tourmente pas pour ça.
Les PCRE ont été faites pour Perl d'un autre côté ! Donc ce n'est pas étonnant. Je trouve qu'il y a beaucoup de ressources sur Python (développez, SdZ…) c'est surtout pour ça que je porte mon choix vers lui (je profiterai peut-être de l'été pour une première approche).
MagiCrazy dit –
Python reste un très très bon langage, toujours très utilisé ! Donc les expressions rationnelles, c'est bien avec, mais pas à haute dose, où Ruby et Perl tiennent le haut du pavé. tiens, si tu veux jouer avec Python, et faire un truc bien sympa, http://aichallenge.org/ ça permet de bien découvrir un des langages (pour toi, Python), et d'y appliquer des mathématiques, tout en s'amusant ^^
Buzut dit –
Merci :) je vais essayer ça !
xidoc dit –
Heureusement que PHP a bien évolué depuis. Il paraîtrait que sur le serveur Swoole, il serait même plus performant que NodeJS. Qui l'aurait cru en 2012.
dalton dit –
Magnifique article très synthétique comme je les aime ;)
Tu peux préciser qu'il existe des générateurs et des analyseurs de REGEX.
C'est bien moins fatigant...
Pascaltech dit –
Merci pour cette présentation des regex. Je recherchais une explication de l'utilisation des parenthèses, c'est ici que je l'ai trouvé. Malgré tout, je ne comprends pas cette utilisation : . . . cat essai.txt | grep -Po "href=.\Khttps://test.com.+?xml(?=.+18 April 2019)" . . .
le critère recherché est le suivant : . . . <tr><td><a href='https://test.com/truc1.xml' target='_blank'>fichier1.xml</a><td>18 April 2019 10:00:00</td><td>1 KB</td></tr> . . .
Je ne comprends pas : . . . (?= . . . Le '?' répète la parenthèse ? Que signifie le signe '=' ? Merci.
Pascaltech dit –
Désolé, J'ai lu trop vite votre article. Le '?' après l'ouverture d la parenthèse signifie que ce n'est pas une parenthèse capturante. Mais que signifie le signe '=' ?
Buzut dit –
Le
=
n'a pas de signification particulière. Il sert donc à matcher le signe "=", tout simplement !Pascaltech dit –
J'ai une réponse différente ici :
(?=tt)
Positive Lookahead (?=tt) Assert that the Regex below matches tt matches the characters tt literally (case sensitive) Global pattern flags g modifier: global. All matches (don't return after first match) m modifier: multi line. Causes ^ and $ to match the begin/end of each line (not only begin/end of string)
site : https://regex101.com/
sans le "=", la fonction "Positive Lookahead" n'est pas reconnue.
Buzut dit –
Oui en effet ! My bad. Je raisonnais en terme de JavaScript et les lookahead n'ont été ajoutés que récemment.
Rejoignez la discussion !