Expressions régulières

Quelques raccourcis :

Cet article porte sur les expressions régulières, un sujet vaste et plein de rebondissements. Je doute en arriver à un texte « définitif » sur le sujet, mais j'espère pouvoir mettre ici suffisamment d'information et de pistes pour faciliter le démarrage de celles et ceux qui rencontrent les expressions régulières pour la première fois, et dépanner celles et ceux qui les utilisent mais rencontrent des difficultés et sont à la recherche d'un petit coup de pouce.

La base

Les expressions régulières constituent un langage permettant de décrire des modèles (des patterns) par lesquels on pourra faire la correspondance avec des éléments d'un texte. Ce langage est concis, permettant d'exprimer des modèles complexes dans un espace somme toute restreint, et se prête à une représentation sous-jacent efficace (typiquement, un automate, parfois accompagné d'une pile).

Quelques exemples simples d'expressions régulières banales suivent :

Modèle Correspond à Ne correspond pas à Remarques
a
a
aa
b
ab
ba

J'ai utilisé a ici, mais on aurait pu utiliser n'importe quel texte « normal ». Un texte tel que abcd correspond, sans surprises, au texte abcd.

.
a
b
A
aa
abcdefg

Le symbole . signifie « n'importe quel caractère », mais un seul à la fois.

[aeiouy]
a
u
aa
c

Placer des symboles entre crochets signifie « n'importe lequel d'entre eux ». Notre exemple ici correspond à toute voyelle, mais une seule d'entre elles.

[a-zA-Z]+
int
getName
abd123

L'écriture a-z signifie « tout symbole inclusivement situé entre a et», alors que A-Z signifie « tout symbole inclusivement situé entre A et». Les crochets signifient essentiellement « l'un de... », donc [a-zA-Z] signifie « un symbole alphabétique ». Le suffixe + signifie « au moins un », donc [a-zA-Z]+ signifie une séquence d'au moins un symbole alphabétique.

Source

Illustration avec C++

À titre d'exemple, dans bien des langages de programmation, un identifiant (nom de constante, de variable, de fonction, etc.) débute par un symbole alphabétique ou un caractère de soulignement, suivi par zéro ou plus symboles alphanumériques ou de soulignement. Exprimé sous la forme d'une expression régulière, on parlerait de [a-zA-Z_]([a-zA-Z0-9_])* ce qui est, on en conviendra, plus compact. Un programme C++ mettant en relief cette fonctionnalité serait :

#include <iostream>
#include <regex>
#include <string>
#include <sstream>
using namespace std;
int main() {
   string texte { "int abc123 :$ 34 -8 _ bbb_ddd" };
   string pattern { "[a-zA-Z_]([a-zA-Z0-9_])*" };
   regex expression{ pattern };
   stringstream sstr{ texte };
   for (string s; sstr >> s;)
      if (regex_match(s, expression))
         cout << "Le mot \"" << s << "\" correspond au pattern \"" << pattern << "\"" << endl;
}

À l'exécution, ce programme affichera :

Le mot "int" correspond au pattern "[a-zA-Z_]([a-zA-Z0-9_])*"
Le mot "abc123" correspond au pattern "[a-zA-Z_]([a-zA-Z0-9_])*"
Le mot "_" correspond au pattern "[a-zA-Z_]([a-zA-Z0-9_])*"
Le mot "bbb_ddd" correspond au pattern "[a-zA-Z_]([a-zA-Z0-9_])*"

Les mots "$:", "34", et "-8" ne sont pas reconnus à l'aide de notre pattern, tel que prévu.

Règle du Max Munch

Les expressions régulières appliquent par défaut un comportement « gourmand », au sens où elles consomment la plus longue séquence possible correspondant au Pattern qui leur est donné. Ce comportement est raisonnable : c'est grâce à lui qu'il est possible de distinguer les mots for et for_each par exemple. C'est ce qu'on nomme la règle du Max Munch.

Prenons l'exemple suivant. Comparez les Patterns des expressions régulières re_vilain et re_mieux : les deux sont utilisables, mais donneront (avec une chaîne comme texte) des résultats bien différents :

#include <iostream>
#include <algorithm>
#include <string>
#include <regex>
using namespace std;
int main() {
   string texte = "<b>allo</b> <b>man</b>";
   regex re_vilain{ R"(<(.*?)>(.*)</(\1)>)" };
   regex re_mieux{ R"(<(.*?)>(.*?)</(\1)>)" };
   for_each(sregex_iterator{ begin(texte), end(texte), re_vilain }, sregex_iterator{}, [](const auto &s) {
      cout << "Trouve : " << s.str() << '\n';
   });
   cout << string(70, '-') << '\n';
   for_each(sregex_iterator{ begin(texte), end(texte), re_mieux }, sregex_iterator{}, [](const auto &s) {
      cout << "Trouve : " << s.str() << '\n';
   });
   cout << endl;
}

En effet, à l'exécution, ce programme affichera :

Trouve : <b>allo</b> <b>man</b>
----------------------------------------------------------------------
Trouve : <b>allo</b>
Trouve : <b>man</b>

En appliquant la règle du Max Munch, re_vilain consommera la plus longue séquence possible (car .* est gourmand) jusqu'au point où une balise fermante correspondant à la balise ouvrante rencontrée initialement sera rencontrée, alors que re_mieux progressera à petits pas (car .*? n'est pas gourmand).

Illustration avec JavaScript

Avec JavaScript, il est bien sûr possible d'utiliser des expressions régulières, et ce dans plusieurs fonctions de manipulation de chaînes de caractères. Quelques exemples suivent.

Séparer une chaîne à partir d'un critère : la méthode split() d'une string JavaScript accepte une expression régulière en tant que critère pour séparer les éléments d'une chaîne sur la base d'un pattern particulier. Faites le test par vous-mêmes!

Pattern :

Le pattern de séparation sera exclu du résultat. Ainsi, essayez la lettre e par exemple, ou encore un simple caractère d'espacement (\s ou \s+ vous donneront deux résultats distincts), pour constater des variations amusantes.

function testSplit(s,patt) {
   return s.split(patt);
}
function testSplitFunction() {
   var texte = document.getElementById("testSplitTextArea").value;
   var patt = document.getElementById("testSplitTextPatt").value;
   var résultat = testSplit(texte,new RegExp(patt));
   for(var i = 0; i != résultat.length; ++i) {
      alert("Élément " + i + " : " + résultat[i]);
   }
}

Reconnaître des sous-chaînes à partir d'un critère : la méthode match() d'une string JavaScript accepte une expression régulière en tant que critère pour reconnaître les éléments d'une chaîne sur la base d'un pattern particulier. Faites le test par vous-mêmes!

Pattern :

Ici, \w+ est un raccourci pour « tout mot », et \s signifie « suivi d'un blanc ». À titre d'expérience, enlevez le \s à la fin, ou utilisez [a-zA-Z]+ à titre de pattern. Si vous utilisez [a-z]+ à titre de pattern, cela devrait limiter la reconnaissance aux seules séquences de lettres minuscules. Si vous utilisez [a-zA-Z]+\. à titre de pattern, vous devriez obtenir les mots ne contenant que des symboles alphabétiques mais suivis immédiatement d'au moins un point (le \ avant le point signifie qu'on s'intéresse au symbole .; par défaut, le . signifie « n'importe quel symbole »).

function testMatch(s,patt) {
   return s.match(patt);
}
function testMatchFunction() {
   var texte = document.getElementById("testMatchTextArea").value;
   var patt = document.getElementById("testMatchTextPatt").value;
   var résultat = testMatch(texte,new RegExp(patt, "g"));
   if (résultat == null) {
      alert("Aucun match");
   } else {
      for(var i = 0; i != résultat.length; ++i) {
         alert("Élément " + i + " : " + résultat[i]);
      }
   }
}

L'exemple ci-dessous cherchera des patterns comprenant (par défaut; vous pouvez jouer avec le pattern) deux mots séparés par au moins un blanc, le deuxième mot devant être suivi par un ';' (il peut y avoir des blancs avant le ';' étant donné le pattern proposé par défaut). Ceci exclura par exemple un mot comprenant un caractère non-alphabétique et une paire de mots qui ne serait pas suivie d'un ';'.

Pattern :

Ici, les parenthèses permettraient d'indiquer que nous souhaitons que le pattern reconnu porte un nom ($1, $2, et ainsi de suite) pour être en mesure de lui référer par la suite.

function testManip(s,patt) {
   var res = s.match(patt);
   for(var i = 0; i != res.length; ++i) {
      if (res[i] != null && res[i] != "") {
         res[i] = res[i].substring(0, res[i].length - 1);
      }
   }
   return res;
}
function testManipFunction() {
   var texte = document.getElementById("testMatchTextArea").value;
   var patt = document.getElementById("testMatchTextPatt").value;
   var résultat = testManip(texte,new RegExp(patt, "g"));
   if (résultat == "" || résultat == null) {
      alert("Aucun match");
   } else {
      for(var i = 0; i != résultat.length; ++i) {
         alert("Élément " + i + " : " + résultat[i]);
      }
   }
}

Pédagogie

Quelques sources d'information sur le sujet des expressions régulières :

Quelques outils

« Regex is like if ASCII had a plastic surgery addiction » (source)

Les expressions régulières ont leurs détracteurs, mais elles ont aussi leurs aficionados. Heureusement pour nous, certains de ces amants de la concision et de l'efficacité des expressions régulières ont mis au point des outils susceptibles de faciliter notre existence. En voici quelques-uns :

En complément, une petite explication de la manière par laquelle DuckDuckGo et Wolfram Alpha interfacent l'un avec l'autre à travers des expressions régulières : http://duckduckgo.com/walpha.html

Applications atypiques

Que peut-on faire avec des expressions régulières? Beaucoup de choses, en fait. Quelques applications atypiques suivent.


Valid XHTML 1.0 Transitional

CSS Valide !