Accueil du blog

Archives

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 : 11

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 "?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 : 11

- Le 09/05/2010

t4Duke

Wah tro dur pour moi pour linstant

- Le 09/06/2010

mightyMouse

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

- Le 05/06/2011

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

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\.\-\_]+(? 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.


___
Site web : http://php-expert.fr

- Le 19/06/2013

regexp

Bonjour Lilhoot,

Il y a une ligne que je n'ai pas comprise, c'est l'exemple de l'expression régulière permettant de dire si une chaîne ne contient pas 'oui' :

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

Si on regarde de plus près :

/^( (?!oui) .)*$/

On a vu que pour la notation "?!", on cherche le pattern qui ne SUIT pas l'élément. Donc l'élément est ce qui se trouve avant '(?!', or, il n'y a !!

Bien que cette expression marche, je n'arrive pas à la comprendre. Ici, l'élément est ? (l'élément est vide ?)

Ensuite, je ne comprends pas le fait de faire suivre cette expression d'un '.' (point) et d'entourer le tout d'autres parenthèses ?

Et enfin d'y coller une étoile ?

Je serais ravi d'avoir une explication détaillée de cette ligne de code.

D'avance, merci.

- Le 11/10/2014

Michel

Bonjour,

Bravo pour les explications. C'est clair et j'ai tout compris.

- Le 22/10/2015

AHmed

Bonjour, Merci pour ces explications
j'ai une préocupation

preg_match_all("/q(?=u)i/","quit",$b);


j'esperais qu'il retourne "q"
mais non
vous savez pourquoi?.

- Le 17/05/2016

Chennorris

J'avais déjà découvert les assertions négatives et positives il y a quelques temps mais avais un peu abandonné, leur syntaxe me paraissant vraiment très complexe à retenir et à utiliser. Et en découvrant cet article, j'ai fini par en utiliser une qui correspondait parfaitement à mon besoin. Un grand merci pour cet article très clairement expliqué.

- Le 25/05/2016

nckf

Salut,

J'aurais une chaîne de ce type à parser:
TCO|Théâtre contemporain ; 21T|Théâtre ; 2TH|Théâtre/Humour ; TCL|Théâtre classique ; ;

Les groupes id|nom sont "normalement" fournis par multiples de 3 (catégories à 3 niveaux) sauf que ce n'est évidemment pas le cas.

Je voudrais récupérer des tableaux du genre id = [TCO, 21T, 2TH, TCL] et nom = [Théâtre contemporain, Théâtre , Théâtre/Humour, Théâtre classique]
ou encore mieux cats = [TCO => Théâtre contemporain, 21T => Théâtre , 2TH => Théâtre/Humour, TCL => Théâtre classique]

J'oubliais (enfin non je le gardais pour la fin) les noms peuvent contenir des & et autres...


Possible à votre avis ?

- Le 25/05/2016

nckf

il fallait lire & amp; évidemment

- Le 30/12/2016

Neoblaster

Merci pour ce poste extrêmement pertinent.
Tous les sujets d'aide sur les assertion avant et arrière ne m'avait pas permis de comprendre exactement le fonctionnement !
Après avoir lu ton post, paff j'y arrive direct !

Thanks a lot !

Ecrire un commentaire

Captcha - Illisible?