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.
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 |
---|---|---|---|
|
|
|
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. |
|
|
|
Le symbole . signifie « n'importe quel caractère », mais un seul à la fois. |
|
|
|
Placer des symboles entre crochets signifie « n'importe lequel d'entre eux ». Notre exemple ici correspond à toute voyelle, mais une seule d'entre elles. |
|
|
|
L'écriture a-z signifie « tout symbole inclusivement situé entre a et z », alors que A-Z signifie « tout symbole inclusivement situé entre A et Z ». 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. |
À 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.
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).
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. |
|
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 »). |
|
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. |
|
Quelques sources d'information sur le sujet des expressions régulières :
« 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
Que peut-on faire avec des expressions régulières? Beaucoup de choses, en fait. Quelques applications atypiques suivent.