Regex - 08 - Lookahead et lookbehind

Les assertions de largeur nulle : regarder devant et derrière sans consommer de caractères.

08 - Lookahead et lookbehind

Ce que tu vas apprendre

  • Le lookahead positif (?=...) et negatif (?!...)
  • Le lookbehind positif (?<=...) et negatif (?<!...)
  • Pourquoi ces assertions ne consomment pas de caractères
  • Valider des mots de passe, extraire du texte entre delimiteurs, matcher conditionnellement

Prerequisites

07 - Les groupes nommes


Les lookahead et lookbehind font partie de ces fonctionnalités de regex qui semblent magiques la première fois qu'on les decouvre. Tu peux vérifier qu'un pattern existe devant ou derrière ta position actuelle, sans pour autant l'inclure dans le match. C'est comme tourner la tête pour vérifier ce qui vient, sans bouger de ta place.

Le concept : assertions de largeur nulle

Quand une regex classique matche abc, elle "consomme" ces trois caractères. Le curseur avance. Les lookahead et lookbehind ne consomment rien. Ils verifient une condition, et si elle est remplie, le curseur reste ou il etait.

C'est pour ca qu'on les appelle "zero-width assertions". Ils ont une largeur de zero caractères dans le match final.

Lookahead positif : (?=...)

"Matche ce qui est suivi de..." :

javascript// Trouver "prix" seulement s'il est suivi d'un nombre
const regex = /prix(?=\s*\d)/g;

"prix 42 euros".match(regex);      // ["prix"]
"prix fixe".match(regex);           // null
"le prix99 est bon".match(regex);   // ["prix"]

Le (?=\s*\d) vérifié qu'apres "prix" il y a (eventuellement des espaces puis) un chiffre. Mais les chiffres ne font pas partie du match. Seul "prix" est capture.

Lookahead negatif : (?!...)

"Matche ce qui n'est PAS suivi de..." :

javascript// Trouver les nombres qui ne sont PAS suivis de "px"
const regex = /\d+(?!px)/g;

"12px 24em 36rem".match(regex);  // ["2", "24", "36"]

Attention au résultat ici. 12px matche partiellement : 1 n'est pas suivi de "px" (il est suivi de 2), donc 1 matche, puis le moteur continue et 2 est suivi de "px" donc il ne matche pas. Le moteur optimise et renvoie "2" qui est la plus longue sous-chaîne de 12 qui ne finit pas devant "px". Ce genre de subtilite rend les lookahead negatifs parfois surprenants.

Pour matcher correctement des nombres entiers pas suivis de "px" :

javascriptconst regex = /\b\d+\b(?!px)/g;
"12px 24em 36rem".match(regex);  // ["24", "36"]

Les \b (word boundaries) forcent le match du nombre complet.

Lookbehind positif : (?<=...)

"Matche ce qui est precede de..." :

javascript// Trouver le montant apres le symbole euro
const regex = /(?<=\$)\d+(?:\.\d{2})?/g;

"Le total est $42.50 et la taxe $3.20".match(regex);
// ["42.50", "3.20"]

Le $ fait partie de la condition mais pas du match. Tu recuperes uniquement le nombre.

Lookbehind negatif : (?

"Matche ce qui n'est PAS precede de..." :

javascript// Trouver "test" qui n'est pas precede de "unit"
const regex = /(?<!unit)test/g;

"unittest loadtest smoketest".match(regex);
// ["test", "test"] (loadtest et smoketest)

Cas pratique : validation de mot de passe

C'est LE cas d'usage classique des lookahead. Tu veux vérifier plusieurs conditions simultanément sur la meme chaîne :

javascript// Au moins 8 caracteres, une majuscule, un chiffre, un caractere special
const strongPassword = /^(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*])(?=.{8,})/;

strongPassword.test("abc");           // false (trop court, manque tout)
strongPassword.test("Abcdefgh1");     // false (manque caractere special)
strongPassword.test("Abcdefg1!");     // true
strongPassword.test("A1!aaaa");       // false (seulement 7 caracteres)

Comment ca marche ? Chaque (?=...) est un lookahead positif ancre au début de la chaîne (grâce à ^). Ils verifient chacun une condition sans consommer de caractères :

  1. (?=.*[A-Z]) : il existe une majuscule quelque part
  2. (?=.*\d) : il existe un chiffre quelque part
  3. (?=.*[!@#$%^&*]) : il existe un caractère special quelque part
  4. (?=.{8,}) : il y a au moins 8 caractères

Comme aucun lookahead ne consomme rien, ils se verifient tous depuis la meme position (le début). Si toutes les conditions passent, le match reussit.

Sur paltemps.fr, j'utilise exactement ce pattern pour la validation de mots de passe cote client. C'est plus lisible qu'une serie de if avec des regex separees.

Cas pratique : extraire du texte entre delimiteurs

Tu veux le contenu entre guillemets, sans les guillemets :

javascript// Sans lookbehind/lookahead, tu as les guillemets dans le match
"il a dit \"bonjour\" puis \"au revoir\"".match(/"[^"]+"/g);
// ["\"bonjour\"", "\"au revoir\""]

// Avec lookbehind et lookahead, tu n'as que le contenu
"il a dit \"bonjour\" puis \"au revoir\"".match(/(?<=")\w+(?=")/g);
// ["bonjour", "au revoir"]

Meme principe pour extraire le contenu entre balises :

javascriptconst html = "<strong>gras</strong> et <em>italique</em>";
const regex = /(?<=<(\w+)>).*?(?=<\/\1>)/g;

// Attention : matchAll est necessaire ici pour les groupes
for (const m of html.matchAll(/(?<=<(\w+)>).*?(?=<\/\1>)/g)) {
  console.log(m[0]); // "gras", "italique"
}

Cas pratique : formatage de nombres

Ajouter des separateurs de milliers :

javascriptfunction formatNumber(n) {
  return n.toString().replace(
    /\B(?=(\d{3})+(?!\d))/g,
    " "
  );
}

formatNumber(1234567);    // "1 234 567"
formatNumber(42);          // "42"
formatNumber(1000000000);  // "1 000 000 000"

Le (?=(\d{3})+(?!\d)) est un lookahead qui cherche les positions suivies d'un nombre de chiffres multiple de 3, et pas suivies d'un autre chiffre. \B empeche de matcher au début du nombre.

Les limites des lookbehind

Les lookbehind ont une limitation en JavaScript : le pattern a l'intérieur doit avoir une longueur determinable. En pratique, les moteurs modernes (V8, SpiderMonkey) supportent les lookbehind de longueur variable, mais ce n'est pas garanti par la spec.

javascript// Fonctionne en V8 (Node.js, Chrome) mais pas garanti partout
/(?<=\w+)@/.test("user@email.com");  // true

// Plus portable : longueur fixe
/(?<=\w{1,50})@/.test("user@email.com");  // true

Combiner lookbehind et lookahead

Tu peux enchaîner les deux pour des conditions precises :

javascript// Trouver les nombres entre parentheses, sans les parentheses
const regex = /(?<=\()\d+(?=\))/g;
"valeurs (42) et (100) ok".match(regex);  // ["42", "100"]

// Trouver les mots entoures d'etoiles (markdown bold)
const regex2 = /(?<=\*\*)\w+(?=\*\*)/g;
"du texte **gras** et **important**".match(regex2);  // ["gras", "important"]

Résumé

  • (?=...) : lookahead positif, vérifié ce qui suit sans consommer
  • (?!...) : lookahead negatif, vérifié que ce qui suit n'est PAS le pattern
  • (?<=...) : lookbehind positif, vérifié ce qui precede
  • (?<!...) : lookbehind negatif, vérifié que ce qui precede n'est PAS le pattern
  • Les lookahead sont parfaits pour valider plusieurs conditions simultanees (mots de passe)
  • Les lookbehind/lookahead combinés extraient du contenu entre delimiteurs proprement
  • Attention aux subtilites du lookahead negatif avec les correspondances partielles

Article précédent : 07 - Les groupes nommes Article suivant : 09 - Les flags

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.