Laisser un commentaire

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 :

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 :

<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.

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

Rejoignez la discussion !

Vous pouvez utiliser Markdown pour les liens [ancre de lien](url), la mise en *italique* et en **gras**. Enfin pour le code, vous pouvez utiliser la syntaxe `inline` et la syntaxe bloc

```
ceci est un bloc
de code
```