REGEX et PREG - assertions avant-arrière (lookahead & lookbehind assertions) : récupérer les chaînes qui ne contiennent pas un mot particulier

Comment écrire une expression régulière qui n'accepte que les chaînes de caractères qui ne contiennent pas tel mot ou telle expression. Une réponse avec les assertions avant-arrière (lookahead & lookbehind assertions)

Commentaires : 4

C'est un problème récurrent sur les forums de développeurs : comment écrire une expression régulière qui n'accepte que les chaînes de caractères qui ne contiennent pas tel mot ou telle expression. La réponse est dans les assertions, qui vont nous permettre de faire plein d'autres choses. Il y a quatre types d'assertions : les assertions avant positives, les assertions avant négatives, les assertions arrière positives, les assertions arrière négatives

1. Assertion avant positive (positive lookahead)

'/element(?=pattern)/'

Cette assertion "?=" permet de vérifier si un élément est suivi d'un pattern spécifique avant de capturer cet élément. On dit "ne capture que les (element) qui sont suivis de (pattern)".

preg_match_all('/\b\w+\b(?=, mais)/', 'Bah oui, mais bon alors voilà! oui, ok mais et alors?', $matches);

L'élément ici est "\b\w+\b", c'est-à-dire un seul mot quelconque, le pattern ", mais". On se retrouve avec un tableau où seul 'oui' est capturé, le pattern entre parenthèses qui le suit n'est pas capturé.

2. Assertion avant négative (negative lookahead)

'/element(?!pattern)/'

Cette assertion "?!" permet de vérifier si un élément n'est pas suivi d'un pattern spécifique avant de capturer cet élément. On dit "ne capture que les (element) qui ne sont pas suivis de (pattern)".

preg_match_all('/\b\w{3}\b(?!, mais)/', 'Bah oui, mais bon alors voilà! oui, ok mais et alors?', $matches);

L'élément ici est "\b\w{3}\b", le pattern ", mais". On se retrouve avec un tableau avec 'bah', 'bon', 'oui' capturés, mais le premier 'oui' n'a pas été capturé car il est suivi du pattern.

3. Assertion arrière positive (positive lookbehind)

'/(?<=pattern)element/'

Cette assertion "?<=" permet de vérifier si un élément est précédé d'un pattern spécifique avant de capturer cet élément. On dit "ne capture que les (element) qui sont précédés de (pattern)".

preg_match_all('/(?<=oui, )\b\w{4}\b/', 'Bah oui, mais bon alors voilà! oui, ok mais et alors?', $matches);

L'élément ici est "\b\w{4}\b", le pattern "oui, ". On se retrouve avec un tableau contenant 'mais', car c'est le seul mot de 4 lettres précédé de "oui, ".

4. Assertion arrière négative (negative lookbehind)

'/(?<!pattern)element/'

Cette assertion "?<!" permet de vérifier si un élément n'est pas précédé d'un pattern spécifique avant de capturer cet élément. On dit "ne capture que les (element) qui ne sont pas précédés de (pattern)".

preg_match_all('/(?<!oui, )\b\w{2}\b/', 'Bah oui, mais bon alors voilà! oui, ok mais et alors?', $matches);

L'élément ici est "\b\w{2}\b", le pattern toujours "oui, ". On se retrouve avec un tableau contenant 'et', car c'est le seul mot de 2 lettres qui n'est pas précédé de "oui, ".

Les choses sont plus claires maintenant? Alors comment récupérer (matcher) les chaînes qui ne contiennent pas un mot particulier? Facile!

echo preg_match('/^((?!oui).)*$/', 'chaine avec un oui');

Indique 0 car la chaîne contient 'oui';

echo preg_match('/^((?!oui).)*$/', 'chaine avec un non');

Indique 1. Mais si c'est pour faire ça, autant utiliser strpos, beaucoup plus simple

echo strpos('chaine avec oui', 'oui') !== false;

Et basta!
Mais voici un exemple d'application des assertions avant-arrière bien plus intéressant. On voudrait subdiviser la chaîne suivante selon les virgules tout en ne gardant que les parties qui ne contiennent pas "oui".

"string avec oui 1, string avec non 2, string avec oui 3, string avec non 4, string avec non 5"

Voici une manière d'y arriver.

preg_match_all('/(?<=^|, )(?:(?!oui)[^,])*(?=,|$)/', 'string avec oui 1, string avec non 2, string avec oui 3, string avec non 4, string avec non 5', $matches);

Qu'est-ce qu'il retourne?

array(1) {
  [0]=>
  array(3) {
    [0]=>
    string(17) "string avec non 2"
    [1]=>
    string(17) "string avec non 4"
    [2]=>
    string(17) "string avec non 5"
  }
}

Exactement ce qu'on voulait.

  • (?<=^|, ) va vérifier si avant la partie à trouver il y a ", " ou si c'est le début de la chaîne.
  • (?=,|$) à la fin va vérifier si après la partie à trouver il y a "," ou si c'est la fin de la chaîne.
  • (?:(?!oui)[^,])* va chercher tout ce qui ne contient ni virgule ni "oui".

Essayez maintenant avec "non" à la place de oui". Sympa, non?

Comme souvent, il y plein de manières de coder pour les mêmes résultats. Ici, je voulais juste illustrer mon propos.

Commentaires : 4

- Le 09/05/2010 à 23:42

t4Duke

Wah tro dur pour moi pour linstant

- Le 09/06/2010 à 18:52

mightyMouse

Pile poil ce que je voulais dans ce dernier exemple !
Txh for sharing

- Le 05/06/2011 à 14:14

kit

Bonjour,

J'ai suivi votre article avec intérêt.

Je souhaite capturer à l'aide de preg_match_all des emails dans une string ne contenant pas plusieurs mots, exemple rapide:

// Recherche email ne contenant pas oui ou non

#((?=.*@.*)(?!.*(?:oui|non).*))#ui

Cela ne fonctionne pas, après avoir tenter plusieurs syntaxes je sèche complément...

Cordialement

- Le 26/09/2011 à 13:15

Coum

Salut Kit,

Ton post date un peu, mais cela aidera peut être d'autre personne.
Avant de commencer, je précise tout de suite que c'est loin d'être la meilleurs méthode pour aboutir au résultat souhaité mais cela illustrera bien l'exemple précédent.

Il faut se souvenir que les assertions ne capture pas le texte leur faisant référence.

# Prenons une chaîne:
$str = 'foo.bar@baz.org, baz.biz@bz.org, some.foo@toto.org';

# Avec l’expression ci-dessous, nous capturons toutes les adresses email de la chaine
$matches = array();
preg_match_all('`\b([A-Z0-9\.\-\_]+@[^\.]+\.[A-Z]+)\b`i', $str, $matches);
var_dump($matches);

#Cela aura pour résultat:
[0]=>
array(3) {
[0]=>
string(15) "foo.bar@baz.org"
[1]=>
string(14) "baz.biz@bz.org"
[2]=>
string(17) "some.foo@toto.org"
}

# Maintenant la même expression en éliminant les mot biz et baz
$matches = array();
preg_match_all('`\b([A-Z0-9\.\-\_]+(?<!(biz|baz))@(?!(biz|baz))[^\.]+\.[A-Z]+)\b`i', $str, $matches);
var_dump($matches);
#résultat
[1]=>
array(1) {
[0]=>
string(17) "some.foo@toto.org"
}

Cet exemple montre bien que les assertions n'ont pas déplacé le curseur alors qu'elles sont positionnées autour du @.

Bonne chance à tous et à toutes !


Coum.

Ecrire un commentaire

Captcha - Illisible?