Accueil  / Semaine 4 / Expressions régulières avancées

Expressions régulières avancées

Deux types d’applications

Les expressions régulières sont utilisées à deux fins, essentiellement. Nous avons vu, avec grep, qu’il est possible de trouver toutes les lignes où un motif apparaît. La ligne nous est donnée intégralement, mais pas le motif lui-même.

Ainsi, dans le texte :

La vie est belle
Oh! La vie est belle!

L’expression régulière « O. » correspond à la seconde ligne à cause de la chaîne « Oh ». Une autre application des expressions régulières consiste à trouver uniquement les motifs correspondant à notre expression régulière, c’est-à-dire « Oh » dans notre exemple.

Nous verrons, la semaine prochaine, comment construire des applications de toutes sortes en Java. En attendant, gardez à l’esprit ces deux types de problèmes.

Avertissement

Nous utilisons ici une syntaxe générique dans la formulation des expressions régulières. La syntaxe exacte utilisée par un moteur d’expressions régulières peut varier. En particulier, les programmes de type « grep » se limitent souvent à une syntaxe simplifiée. Par contre, un éditeur de texte comme Visual Studio Code supporte une syntaxe riche. Nous vous invitons à explorer les limites des différents outils et à en tenir compte.

Notations pratiques

Un caractère alphanumérique est noté « \w » (pour « word character ») et il supporte les accents en Java : ce qui est plus pratique que la chaîne « [a-zA-Z0-9] ». Le contraire de « \w » est « \W ». L’espace « » est noté « \s » (pour « space »). Un caractère numérique est noté « \d » (en fait, il s’agit de [0-9]), alors qu’un caractère non numérique est « \D ».

La définition précise de ce qui fait partie des caractères alphanumériques peut varier selon le moteur d’expression régulière que vous utilisez. Par exemple, le caractère ’_’ est souvent inclus, mais pas le caractère ’-’. L’expression ’C++’ ne sera généralement pas reconnue comme une séquence de trois caractères alphanumériques.

Parenthèses capturantes

Certains moteurs d’expressions régulières, dont le moteur fourni par l’API Java, nous permettent de sélectionner non seulement le motif déterminé par l’expression régulière, mais aussi tous les sous-motifs trouvés dans des parenthèses. Par exemple, dans l’expression régulière « a([bc])c », il y aura un motif global qui sera soit « abc » ou « acc » et un sous-motif qui sera soit « b » ou « c ». On verra, plus tard, comment accéder aux sous-motifs dans le cas de l’API Java. On peut aussi utiliser des parenthèses capturantes au sein d’un expression régulière. Dans ce cas, le contenu de la première parenthèse est noté « \1 », le contenu de la seconde « \2 » et ainsi de suite. L’expression « (.)\1 » représente le motif « un caractère répété ». De la même façon, l’expression régulière « \W(\w+)\W*\1 » permet de trouver un mot répété, comme dans la phrase « La vie est est belle ».

Ancrages

Les expressions régulières peuvent reconnaître non seulement les caractères, mais aussi les « ancrages ».

L’ancrage de début de ligne : « ^ » est le plus utilisé et le plus simple. L’expression « jo » appliquée à la chaîne « pas jojo » trouve deux motifs correspondants. Par contre, l’expression « ^jo » ne correspond pas à un motif dans la chaîne « pas jojo », car les sous-chaînes « jo » ne sont pas au début de la ligne.

L’ancrage de fin de ligne est noté avec le signe de dollar. Par exemple, la chaîne « jo » suivi du signe de dollar (« jo$ ») trouvera le dernier « jo » dans « pas jojo ».

On peut définir un mot comme une séquence de caractères alphanumériques (voir ’\w’). La limite d’un mot est notée « \b » (pour « boundary »). Elle apparaître au début et à la fin d’un mot. Ainsi, l’expression « \bjo » trouvera tout mot qui commence par « jo », et cela, même si le mot commence le texte, alors que l’expression « jo\b » trouvera tout mot qui se termine par « jo », y compris le dernier mot le cas échéant. Notons que l’espace ne peut être utilisé à la place de « \b ». Par exemple, la chaîne « pas jojo » comprend bien un motif correspondant à « jo\b », mais pas un motif correspondant à « jo\s » ; elle comprend par contre un motif correspondant à « \s\bjo ».

Tests avant/arrière

Pour trouver toutes les lettres qui précèdent la lettre « f » dans une chaîne de caractères, on utilise l’expression régulière « \w(?=f) ». Notez la syntaxe « (?...) ». La différence entre « \wf » et « \w(?=f) » est que la première expression sélectionne tout le motif, y compris la lettre « f », tandis que l’autre ne sélectionnera que les caractères précédant la lettre « f ».

On peut aussi faire des tests arrière avec une syntaxe similaire : « (?<=f)\w » trouvera tous les caractères qui suivent la lettre « f ». Notez que dans le cas des tests arrières, il faut que l’expression régulière porte sur un nombre maximal de caractères connu d’avance. Par exemple, il n’est pas permis de faire « (?<=dog.*)\w » alors que « (?<=dog)\w » est parfaitement convenable.

On peut aussi tester l’absence d’un motif. Par exemple, pour trouver toutes les lettres qui ne sont pas suivies de la lettre « f », il suffit d’utiliser l’expression « \w(?!f) » c’est-à-dire qu’on remplace ?= par ?!. Pour trouver toutes les lettres qui ne suivent pas la lettre f, il faut faire « (?< !f)\w ».

Groupement atomique

Le titre Les joyeux naufragés est un motif reconnu par l’expression régulière « (\s*\w*[sx]\s*)* ». La composante « \s*\w*[sx]\s* » de l’expression représente chacun des mots qui se termine par « s » ou « x ». Si vous aviez une très longue phrase avec pratiquement tous les mots qui se terminent par « s » ou « x », sauf pour le dernier mot, il pourrait en résulter un très long temps de calcul. On règle ce type de problème en rendant la reconnaissance de chaque mot « atomique » avec la syntaxe « (?>...) » (à ne pas confondre avec la syntaxe « (?=...) » présentée précédemment). Dans cet exemple, l’expression régulière « (?>\s*\w*[sx]\s*)* » sera beaucoup plus rapide que notre choix initial (« (\s*\w*[sx]\s*)* »).

Le terme atomique signifie que ce qui est entre parenthèses ne dépend pas du reste de l’expression. Par exemple, dans l’expression régulière « L.*s », la composante « .* » représente tout ce qui se trouve entre L et s, soit « es joyeux naufragé ». Ainsi, pour déterminer à quoi correspond « .* » dans la chaîne, le moteur doit attendre de trouver la dernière lettre « s ». Si on utilisait plutôt l’expression « L(?>.*)s », alors la composante « (?>.*) » ignorerait tout ce qui suit, c’est-à-dire la lettre « s », et capturerait donc tout le reste du titre. Le résultat net est que l’expression « L(?>.*)s » ne reconnaît pas un motif dans le titre « Les joyeux naufragés », mais « L( ?>.*s) » ou « L( ?>.*) » reconnaissent le titre comme motif.

Quantificateurs

Nous avons déjà traité des quantificateurs tels « * », « + », « ? » et « {m,n} » qui servent à indiquer combien de fois une expression est répétée. Il y a trois variantes de ces quantificateurs.

Quantificateurs avides

Par défaut, les quantificateurs sont avides, c’est-à-dire qu’ils tentent de reconnaître autant de texte que possible. Ainsi, l’expression régulière « a.*s » dans le texte « abscdsa » sélectionne le motif « abscds », mais pas le motif « abs ».

Quantificateurs paresseux

En ajoutant le symbole « ? » après le quantificateur, on le rend paresseux, c’est-à-dire qu’au lieu de reconnaître autant de texte que possible, il en reconnaît aussi peu que possible. L’expression régulière « a.* ?s », appliquée au texte « abscdsa », sélectionne le motif « abs » mais pas le motif « abscds ».

Quantificateurs possessifs

Les quantificateurs possessifs s’obtiennent en ajoutant le symbole « + » après le quantificateur. Ils sont tout simplement des quantificateurs atomiques. Par exemple, « .*+ » signifie la même chose que « (?>.*) ».