Les expressions régulières (partie 2/2)

Dans ce deuxième chapitre consacré aux regex, nous allons voir leur utilisation au sein du JavaScript. En effet, le premier chapitre n'était là que pour enseigner la base de la syntaxe alors que, une fois couplées à un langage de programmation, les regex deviennent très utiles. En JavaScript, elles utilisent l'objetRegExpet permettent de faire tout ce que l'on attend d'une regex : rechercher un terme, le capturer, le remplacer, etc.

Construire une regex

Le gros de la théorie sur les regex est maintenant vu, et il ne reste plus qu'un peu de pratique. Nous allons tout de même voir comment écrire une regex pas à pas, de façon à ne pas se tromper.
Nous allons partir d'un exemple simple : vérifier si une chaîne de caractères correspond à une adresse e-mail. Pour rappel, une adresse e-mail est de cette forme :javascript@openclassrooms.com.

Une adresse e-mail contient trois parties distinctes :

  • La partie locale, avant l'arobase (ici « javascript ») ;

  • L'arobase @ ;

  • Le domaine, lui-même composé du label « openclassrooms » et de l'extension « com ».

Pour construire une regex, il suffit de procéder par étapes : faisons comme si nous lisions la chaîne de caractères et écrivons la regex au fur et à mesure. On écrit tout d'abord la partie locale, qui n'est composée que de lettres, de chiffres et éventuellement d'un tiret, un trait de soulignement et un point. Tous ces caractères peuvent être répétés plus d'une fois (il faut donc utiliser le quantificateur +) :

/^[a-z0-9._-]+$/

On ajoute l'arobase. Ce n'est pas un métacaractère, donc pas besoin de l'échapper :

/^[a-z0-9._-]+@$/

Après vient le label du nom de domaine, lui aussi composé de lettres, de chiffres, de tirets et de traits de soulignement. Ne pas oublier le point, car il peut s'agir d'un sous-domaine (par exemple @cours.openclassrooms.com) :

/^[a-z0-9._-]+@[a-z0-9._-]+$/

Puis vient le point de l'extension du domaine : attention à ne pas oublier de l'échapper, car il s'agit d'un métacaractère :

/^[a-z0-9._-]+@[a-z0-9._-]+\.$/

Et pour finir, l'extension ! Une extension de nom de domaine ne contient que des lettres, au minimum 2, au maximum 6. Ce qui nous fait :

/^[a-z0-9._-]+@[a-z0-9._-]+\.[a-z]{2,6}$/

Testons donc :

var email = prompt("Entrez votre adresse e-mail :", "javascript@siteduzero.com");
if (/^[a-z0-9._-]+@[a-z0-9._-]+\.[a-z]{2,6}$/.test(email)) {
alert("Adresse e-mail valide !");
} else {
alert("Adresse e-mail invalide !");
}

L'adresse e-mail est détectée comme étant valide !

L'objet RegExp

L'objetRegExpest l'objet qui gère les expressions régulières. Il y a donc deux façons de déclarer une regex : viaRegExpou via son type primitif que nous avons utilisé jusqu'à présent :

var myRegex1 = /^Raclette$/i;
var myRegex2 = new RegExp("^Raclette$", "i");

Le constructeurRegExpreçoit deux paramètres : le premier est l'expression régulière sous la forme d'une chaîne de caractères, et le deuxième est l'option de recherche, icii. L'intérêt d'utiliserRegExpest qu'il est possible d'inclure des variables dans la regex, chose impossible en passant par le type primitif :

var nickname = "Sébastien";
var myRegex = new RegExp("Mon prénom est " + nickname, "i");

Ce n'est pas spécialement fréquent, mais cela peut se révéler particulièrement utile. Il est cependant conseillé d'utiliser la notation littérale (le type primitif) quand l'utilisation du constructeurRegExpn'est pas nécessaire.

Méthodes

RegExpne possède que deux méthodes :test()etexec(). La méthodetest()a déjà été utilisée et permet de tester une expression régulière ; elle renvoietruesi le test est réussi oufalsesi le test échoue. De son côté,exec()applique également une expression régulière, mais renvoie un tableau dont le premier élément contient la portion de texte trouvée dans la chaîne de caractères. Si rien n'est trouvé,nullest renvoyé.

var sentence = "Si ton tonton";
var result = /\bton\b/.exec(sentence); // On cherche à récupérer le mot « ton »
if (result) { // On vérifie que ce n'est pas null
alert(result); // Affiche « ton »
}

Propriétés

L'objetRegExpcontient neuf propriétés, appelées$1,$2,$3… jusqu'à$9. Comme nous allons le voir dans la sous-partie suivante, il est possible d'utiliser une regex pour extraire des portions de texte, et ces portions sont accessibles via les propriétés$1à$9.

Tout cela va être mis en lumière un peu plus loin en parlant des parenthèses !

Les parenthèses

Les parenthèses capturantes

Nous avons vu pour le moment que les regex servaient à voir si une chaîne de caractères correspondait à un modèle. Mais il y a moyen de faire mieux, comme extraire des informations. Pour définir les informations à extraire, on utilise des parenthèses, que l'on appelle parenthèses capturantes, car leur utilité est de capturer une portion de texte, que la regex va extraire.

Considérons cette chaîne de caractères : « Je suis né en mars ». Au moyen de parenthèses capturantes, nous allons extraire le mois de la naissance, pour pouvoir le réutiliser :

var birth = 'Je suis né en mars';
/^Je suis né en (\S+)$/.exec(birth);
alert(RegExp.$1); // Affiche : « mars »

Cet exemple est un peu déroutant, mais est en réalité assez simple à comprendre. Dans un premier temps, on crée la regex avec les fameuses parenthèses. Comme les mois sont faits de caractères qui peuvent être accentués, on peut directement utiliser le type générique \S. \S+ indique qu'on recherche une série de caractères, jusqu'à la fin de la chaîne (délimitée, pour rappel, par $) : ce sera le mois. On englobe ce « mois » dans des parenthèses pour faire comprendre à l'interpréteur JavaScript que leur contenu devra être extrait.

La regex est exécutée viaexec(). Et ici une autre explication s'impose. Quand on exécutetest()ouexec(), le contenu des parenthèses capturantes est enregistré temporairement au sein de l'objetRegExp. Le premier couple de parenthèses sera enregistré dans la propriété$1, le deuxième dans$2et ainsi de suite, jusqu'au neuvième, dans$9. Cela veut donc dire qu'il ne peut y avoir qu'un maximum de neuf couples de parenthèses. Les couples sont numérotés suivant le sens de lecture, de gauche à droite.
Et pour accéder aux propriétés, il suffit de faireRegExp.$1,RegExp.$2, etc.

Voici un autre exemple, reprenant la regex de validation de l'adresse e-mail. Ici, le but est de décomposer l'adresse pour récupérer les différentes parties :

var email = prompt("Entrez votre adresse e-mail :", "javascript@siteduzero.com");
if (/^([a-z0-9._-]+)@([a-z0-9._-]+)\.([a-z]{2,6})$/.test(email)) {
alert('Partie locale : ' + RegExp.$1 + '\nDomaine : ' + RegExp.$2 + '\nExtension : ' + RegExp.$3);
} else {
alert('Adresse e-mail invalide !');
}

Essayer !

Ce qui nous affiche bien les trois parties :

Les trois parties sont bien renvoyées
Les trois parties sont bien renvoyées

Les parenthèses non capturantes

Il se peut que dans de longues et complexes regex, il y ait besoin d'utiliser beaucoup de parenthèses, plus de neuf par exemple, ce qui peut poser problème puisqu'il ne peut y avoir que neuf parenthèses capturantes exploitables. Mais toutes ces parenthèses n'ont peut-être pas besoin de capturer quelque chose, elles peuvent juste être là pour proposer un choix. Par exemple, si on vérifie une URL, on peut commencer la regex comme ceci :

/(https|http|ftp|steam):\/\//

Mais on n'a pas besoin que ce soit une parenthèse capturante et qu'elle soit accessible viaRegExp.$1. Pour la rendre non capturante, on va ajouter ?: au début de la parenthèse, comme ceci :

/(?:https|http|ftp|steam):\/\//

De cette manière, cette parenthèse n'aura aucune incidence sur les propriétés$deRegExp!

Les recherches non-greedy

Le mot anglais greedy signifie « gourmand ». En JavaScript, les regex sont généralement gourmandes, ce qui veut dire que lorsqu'on utilise un quantificateur comme le +, le maximum de caractères est recherché, alors que ce n'est pas toujours le comportement espéré. Petite mise en lumière : nous allons construire une regex qui va extraire l'adresse Web à partir de cette portion de HTML sous forme de chaîne de caractères :

var html = '<a href="www.mon-adresse.be">Mon site</a>';

Voici la regex qui peut être construite :

/<a href="(.+)">/

Et ça marche :

/<a href="(.+)">/.exec(html);
alert(RegExp.$1); // www.mon-adresse.be

Maintenant, supposons que la chaîne de caractères ressemble à ceci :

var html = '<a href="www.mon-adresse.be"><strong class="web">Mon site</strong></a>';

Et là, c'est le drame :

La valeur renvoyée n'est pas celle qu'on attendait
La valeur renvoyée n'est pas celle qu'on attendait

En spécifiant.+comme quantificateur, on demande de rechercher le plus possible de caractères jusqu'à rencontrer les caractères « "> », et c'est ce que le JavaScript fait :

JavaScript s'arrête à la dernière occurrence souhaitée
JavaScript s'arrête à la dernière occurrence souhaitée

Le JavaScript va trouver la partie surlignée : il cherche jusqu'à ce qu'il tombe sur la dernière apparition des caractères « "> ». Mais ce n'est pas dramatique, fort heureusement !

Pour pallier ce problème, nous allons écrire le quantificateur directement suivi du point d'interrogation, comme ceci :

var html = '<a href="www.mon-adresse.be"><strong class="web">Mon site</strong></a>';
/<a href="(.+?)">/.exec(html);
alert(RegExp.$1);

Le point d'interrogation va faire en sorte que la recherche soit moins gourmande et s'arrête une fois que le minimum requis est trouvé, d'où l’appellation non-greedy (« non gourmande »).

Rechercher et remplacer

Une fonctionnalité intéressante des regex est de pouvoir effectuer des « rechercher-remplacer ». Rechercher-remplacer signifie qu'on recherche des portions de texte dans une chaîne de caractères et qu'on remplace ces portions par d'autres. C'est relativement pratique pour modifier une chaîne rapidement, ou pour convertir des données. Une utilisation fréquente est la conversion de balises BBCode en HTML pour prévisualiser le contenu d'une zone de texte.

Un rechercher-remplacer se fait au moyen de la méthodereplace()de l'objetString. Elle reçoit deux arguments : la regex et une chaîne de caractères qui sera le texte de remplacement. Petit exemple :

var sentence = 'Je m\'appelle Sébastien';
var result = sentence.replace(/Sébastien/, 'Johann');
alert(result); // Affiche : « Je m'appelle Johann »

Très simple :replace()va rechercher le prénom « Sébastien » et le remplacer par « Johann ».

Utilisation dereplace()sans regex

À la place d'une regex, il est aussi possible de fournir une simple chaîne de caractères. C'est utile pour remplacer un mot ou un groupe de mots, mais ce n'est pas une utilisation fréquente, on utilisera généralement une regex. Voici toutefois un exemple :

var result = 'Je m\'appelle Sébastien'.replace('Sébastien', 'Johann');
alert(result); // Affiche : « Je m'appelle Johann »

L'optiong

Nous avions vu l'optioniqui permet aux regex d'être insensibles à la casse des caractères. Il existe une autre option,g, qui signifie « rechercher plusieurs fois ». Par défaut, la regex donnée précédemment ne sera exécutée qu'une fois : dès que « Sébastien » sera trouvé, il sera remplacé… et puis c'est tout. Donc si le prénom « Sébastien » est présent deux fois, seul le premier sera remplacé. Pour éviter ça, on utilisera l'optiongqui va dire de continuer la recherche jusqu'à ce que plus rien ne soit trouvé :

var sentence = 'Il s\'appelle Sébastien. Sébastien écrit un tutoriel.';
var result = sentence.replace(/Sébastien/g, 'Johann');
alert(result); // Il s'appelle Johann. Johann écrit un tutoriel.

Ainsi, toutes les occurrences de « Sébastien » sont correctement remplacées par « Johann ». Le mot occurrence est nouveau ici, et il est maintenant temps de l'employer. À chaque fois que la regex trouve la portion de texte qu'elle recherche, on parle d'occurrence. Dans le code précédent, deux occurrences de « Sébastien » sont trouvées : une juste après « appelle » et l'autre après le premier point.

Rechercher et capturer

Il est possible d'utiliser les parenthèses capturantes pour extraire des informations et les réutiliser au sein de la chaîne de remplacement. Par exemple, nous avons une date au format américain : 05/26/2011, et nous souhaitons la convertir au format jour/mois/année. Rien de plus simple :

var date = '05/26/2011';
date = date.replace(/^(\d{2})\/(\d{2})\/(\d{4})$/, 'Le $2/$1/$3');
alert(date); // Le 26/05/2011

Chaque nombre est capturé avec une parenthèse, et pour récupérer chaque parenthèse, il suffit d'utiliser$1,$2et$3(directement dans la chaîne de caractères), exactement comme nous l'aurions fait avecRegExp.$1.

Et si on veut juste remplacer un caractère par le signe dollar, il faut l'échapper ?

Pour placer un simple caractère $ dans la chaîne de remplacement, il suffit de le doubler, comme ceci :

var total = 'J\'ai 25 dollars en liquide.';
alert(total.replace(/dollars?/, '$$'); // J'ai 25 $ en liquide

Le mot « dollars » est effectivement remplacé par son symbole. Un point d'interrogation a été placé après le « s » pour pouvoir trouver soit « dollars » soit « dollar ».

Voici un autre exemple illustrant ce principe. L'idée ici est de convertir une balise BBCode de mise en gras ([b]un peu de texte[/b]) en un formatage HTML de ce type :<strong>un peu de texte</strong>. N'oubliez pas d'échapper les crochets qui sont, pour rappel, des métacaractères !

var text = 'bla bla [b]un peu de texte[/b] bla [b]bla bla en gras[/b] bla bla';
text = text.replace(/\[b\]([\s\S]*?)\[\/b\]/g, '<strong>$1</strong>');
alert(text);

Mais pourquoi avoir utilisé[\s\S]et non pas juste le point ?

Il s'agit ici de trouver tous les caractères qui se trouvent entre les balises. Or, le point ne trouve que des caractères et des espaces blanc hormis le retour à la ligne. C'est la raison pour laquelle on utilisera souvent la classe comprenant \s et \S quand il s'agira de trouver du texte comportant des retours à la ligne.

Cette petite regex de remplacement est la base d'un système de prévisualisation du BBCode. Il suffit d'écrire une regex de ce type pour chaque balise, et le tour est joué :

<script>
function preview() {
var value = document.getElementById("text").value;
value = value.replace(/\[b\]([\s\S]*?)\[\/b\]/g, '<strong>$1</strong>'); // Gras
value = value.replace(/\[i\]([\s\S]*?)\[\/i\]/g, '<em>$1</em>'); // Italique
value = value.replace(/\[s\]([\s\S]*?)\[\/s\]/g, '<del>$1</del>'); // Barré
value = value.replace(/\[u\]([\s\S]*?)\[\/u\]/g, '<span style="text-decoration: underline">$1</span>'); // Souligné
document.getElementById("output").innerHTML = value;
}
</script>
<form>
<textarea id="text"></textarea><br />
<button type="button" onclick="preview()">Prévisualiser</button>
<div id="output"></div>
</form>

Essayer !

Utiliser une fonction pour le remplacement

À la place d'une chaîne de caractères, il est possible d'utiliser une fonction pour gérer le ou les remplacements. Cela permet, par exemple, de réaliser des opérations sur les portions capturées ($1,$2,$3…).

Les paramètres de la fonction sont soumis à une petite règle, car ils doivent respecter un certain ordre (mais leurs noms peuvent bien évidemment varier) :function(str, p1, p2, p3 /* ... */, offset, s). Les paramètresp1,p2,p3… représentent les fameux$1,$2,$3... S'il n'y a que trois parenthèses capturantes, il n'y aura que trois « p ». S'il y en a cinq, il y aura cinq « p ». Voici les explications des paramètres :

  • Le paramètrestrcontient la portion de texte trouvée par la regex ;

  • Les paramètresp*contiennent les portions capturées par les parenthèses ;

  • Le paramètreoffsetcontient la position de la portion de texte trouvée ;

  • Le paramètrescontient la totalité de la chaîne.

Pour illustrer cela, nous allons réaliser un petit script tout simple, qui recherchera des nombres dans une chaîne et les transformera en toutes lettres. La transformation se fera au moyen de de la fonctionnum2Letters()qui a été codée lors du tout premier TP : Convertir un nombre en toutes lettres .

var sentence = 'Dans 22 jours, j\'aurai 24 ans';
var result = sentence.replace(/(\d+)/g, function(str, p1) {
p1 = parseInt(p1);
if (!isNaN(p1)) {
return num2Letters(p1);
}
});
alert(result); // Affiche : « Dans vingt-deux jours, j'aurai vingt-quatre ans »

L'exemple utilise une fonction anonyme, mais il est bien évidemment possible de déclarer une fonction :

function convertNumbers(str) {
str = parseInt(str);
if (!isNaN(str)) {
return num2Letters(str);
}
}
var sentence = 'Dans 22 jours, j\'aurai 24 ans';
var result = sentence.replace(/(\d+)/g, convertNumbers);

Autres recherches

Il reste deux méthodes deStringà voir,search()etmatch(), plus un petit retour sur la méthodesplit().

Rechercher la position d'une occurrence

La méthodesearch(), toujours de l'objetString, ressemble àindexOf()mis à part le fait que le paramètre est une expression régulière.search()retourne la position de la première occurrence trouvée. Si aucune occurrence n'est trouvée, -1 est retourné. Exactement commeindexOf():

var sentence = 'Si ton tonton';
var result = sentence.search(/\bton\b/);
if (result > -1) { // On vérifie que quelque chose a été trouvé
alert('La position est ' + result); // 3
}

Récupérer toutes les occurrences

La méthodematch()de l'objetStringfonctionne commesearch(), à la différence qu'elle retourne un tableau de toutes les occurrences trouvées. C'est pratique pour compter le nombre de fois qu'une portion de texte est présente par exemple :

var sentence = 'Si ton tonton tond ton tonton, ton tonton tondu sera tondu';
var result = sentence.match(/\btonton\b/g);
alert('Il y a ' + result.length + ' "tonton" :\n\n' + result);
Il y a trois occurrences de « tonton »
Il y a trois occurrences de « tonton »

Couper avec une regex

Nous avions vu que la méthodesplit()recevait une chaîne de caractères en paramètre. Mais il est également possible de transmettre une regex. C'est très pratique pour découper une chaîne à l'aide, par exemple, de plusieurs caractères distincts :

var family = 'Guillaume,Pauline;Clarisse:Arnaud;Benoît;Maxime';
var result = family.split(/[,:;]/);
alert(result);

L'alert()affiche donc un tableau contenant tous les prénoms, car il a été demandé àsplit()de couper la chaîne dès qu'une virgule, un deux-points ou un point-virgule est rencontré.

En résumé
  • Construire une regex se fait rarement du premier coup. Il faut y aller par étapes, morceau par morceau, car la syntaxe devient vite compliquée.

  • En combinant les parenthèses capturantes et la méthodeexec(), il est possible d'extraire des informations.

  • Les recherches doivent se faire en mode non-greedy. C'est plus rapide et correspond plus au comportement que l'on attend.

  • L'optiongindique qu'il faut effectuer plusieurs remplacements, et non pas un seul.

  • Il est possible d'utiliser une fonction pour la réalisation d'un remplacement. Ce n'est utile que quand il est nécessaire de faire des opérations en même temps que le remplacement.

  • La méthodesearch()s'utilise comme la méthodeindexOf(), sauf que le paramètre est une regex.