Les expressions rationnelles ("régulières")

Les expressions rationnelles ("régulières")

Qu'est ce qu'une expression rationnelle ?

Tous les programmeurs disent du bien des expressions rationnelles. Pourtant, l'unanimité est assez rare dans le domaine. En fait, il y a une raison très simple à cela : elles sont à la fois maniables et performantes. Il serait donc dommage de s'en priver... Ce tutoriel vous propose donc d'aller y faire un petit tour. D'avance, je tiens à préciser que toutes les remarques, critiques, corrections et suggestions relatives à ce tutoriel sont les bienvenues. Je vous prie aussi de m'excuser pour les incohérences de mise en forme que vous pourriez rencontrer.

On entend presque toujours parler d'expressions régulières (ou de regexp, par contraction de l'anglais regular expressions). Pourtant, l'expression "expressions régulières" est un calque de l'anglais "regular expressions" : ce n'est pas une traduction correcte. On devrait plutôt parler d'expressions rationnelles. En maths, on parle d'ailleurs bien d'expressions rationnelles. En maths ? Hé oui ! Il n'y a pas d'erreur, vous avez bien lu. Ces expressions sont associées à des maths, même si ça ne se voit pas au premier coup d'oeil (pour en savoir plus, il faut aller chercher du côté de la théorie des automates et des langages formels). Mais rassurez-vous, je ne vais pas vous parler d'automates, même si c'est intéressant, et nous ne ferons pas de mathématiques !

Maintenant que les présentations sont faites, parlons un peu des expressions rationnelles avec Visual Basic. Pour les utiliser, cliquez sur le menu "Projet", puis sur l'onglet "Références". Dans la longue liste qui apparaît alors, cochez la ligne "Microsoft VBScript Regular Expressions 5.5". Voilà, c'est déjà terminé, vous pouvez vous en servir ! Certes oui... mais comment, me direz-vous ? C'est ce que nous allons apprendre maintenant...

Attention ! Pour tirer le meilleur parti de ce tutoriel, il faut que vous puissiez essayer vous-même les exemples que je vous propose. C'est la raison pour laquelle je vous conseille de vous servir du RegExp Workshop de Renfield : http://codes-sources.commentcamarche.net/source/17331-regexp-workshop Avec cet outil et un peu de bonne volonté, tout devrait bien se passer.

Que faut-il déclarer ?

Pour utiliser les expressions rationnelles, trois variables suffisent. Dans un module, ajoutez donc :

Option Explicit 
'Cette variable vous permet de paramétrer une expression rationnelle 
Private oRegExp As RegExp 
'Cette variable contient l'ensemble des occurrences trouvées dans un texte 
'à partir de votre expression rationnelle 
Private Results As MatchCollection 
'Cette variable désigne un résultat particulier au sein de la structure précédente 
Private Item As Match
  • La variable oRegExp contient l'expression rationnelle, c'est-à-dire son motif, ses options et quelques fonctions, mais nous détaillerons tout cela plus tard.
  • La variable Results désigne l'ensemble des morceaux de textes (les occurrences) qui satisfont les conditions définies dans le motif et les options de l'expression rationnelle.
  • La variable Item, enfin, désigne une occurrence quelconque à l'intérieur de la collection d'occurrences Results.

Pour ces trois variables, j'ai choisi une portée privée (limitée au module) mais vous pouvez tout aussi bien choisir une portée publique. Tout dépend naturellement de l'utilisation que vous souhaitez en faire. Pour ma part, je place généralement tout ce qui touche aux expressions rationnelles dans le même module (nommé mRegExp, ce qui n'est pas très original...), d'où mon choix.

La variable oRegExp doit être déclarée et instanciée.
Il est donc tout à fait possible d'écrire :

'Cette variable vous permet de paramétrer une expression rationnelle 
Private oRegExp As New RegExp 

C'est une écriture tout à fait correcte, mais contestable. Pourquoi ? Tout simplement parce qu'une variable qui utilise "New" est considérée par VB comme devant être auto-instanciée (cf. tutoriel sur l'optimisation avec VB6). De fait, vous vous exposez à des vérifications supplémentaires qui nuisent aux performances de votre programme (Attention ! Si vous faites un usage modéré des expressions rationnelles, vous ne sentirez pas la différence). Mais si vous êtes curieux de quantifier cette différence de temps d'exécution, tournez-vous vers les API QueryPerformanceCounter et QueryPerformanceFrequency. Il y a plusieurs bonnes source sur le sujet sur VBFrance. Je vous propose celle-ci : http://www.codes-sources.com/code.aspx?ID=39534.

Paramétrer une expression rationnelle

Pour paramétrer une expression rationnelle, nous allons commencer par créer une fonction Initialize, qui aura pour rôle d'instancier la variable oRegExp, d'une part, et de définir plusieurs options de recherche, d'autre part :

Private Sub Initialize()    
    'Création d'une nouvelle instance de l'objet RegExp (inutile si vous 
    'avez écrit "New" dans la déclaration)    
    Set oRegExp = New RegExp    
    With oRegExp       
        'Une recherche globale s'effectue sur la totalité du texte, sans 
        's'arrêter dès le premier résultat trouvé       
        .Global = True 
        'Le texte est-il considéré comme une seule et même ligne, ou comme 
        'plusieurs lignes distinctes ?
        .Multiline = True
        'Tient-on compte de la différence entre majuscules et minuscules 
        '(case sensitiveness) ou non (case insensitiveness) ?
        .IgnoreCase = True
        'Clef de voûte de notre expression rationnelle : son motif. Nous 
        'en parlerons un peu plus loin
        .Pattern = "&H[\dA-F * {1,6}"
    End With 
End Sub
  • Recherche globale : Lorsque la recherche est définie comme globale, le moteur poursuit son analyse jusqu'à la fin du texte. Dans le cas contraire, il cesse d'analyser le texte dès qu'une chaîne satisfait le motif d'expression rationnelle et les options associées.
  • Recherche multiligne : Lorsque la recherche est définie comme multiligne, le texte où s'effectue la recherche est considéré comme un ensemble de plusieurs lignes. Dans le cas contraire, il est considéré comme une seule et même ligne, le retour à la ligne étant alors traité comme un espace parmi d'autres. Ce paramètre influe sur les métacaractères ^ et $, mais nous le verrons plus tard.
  • Ignorer la casse : Si vous choisissez d'ignorer la casse, les majuscules et les minuscules seront considérées comme équivalentes. Dans le cas contraire, elles seront distinguées les unes des autres. C'est un peu ce qui se passe quand vous choisissez vbTextCompare plutôt que vbBinaryCompare.

Avant de nous intéresser au motif, ce qui constitue l'essentiel de ce tutoriel, créons rapidement deux autres fonctions : une fonction de libération de la mémoire, et une fonction de recherche des occurrences. Voici donc :

'Libération de la mémoire pour terminer le travail proprement 
Private Sub Terminate()
    Set oRegExp = Nothing
    Set Results = Nothing
    Set Item = Nothing
End Sub
'Cette procédure possède une portée publique car nous allons l'appeler 
'depuis une feuille (ou un autre module, etc...) 
Public Sub Search( ByVal sText As String )
    'D'abord, il faut créer une instance et paramétrer l'expression rationnelle.
     Call Initialize
    'Ensuite, il faut lancer la recherche et récupérer tous les résultats 
    '(variable Results, voir plus haut)
    Set Results = oRegExp.Execute(sText)
    'Ensuite, il faut agir sur les résultats. Je ne vous décris pas encore 
    'cette partie, car nous en parlerons plus tard ! Patience :-)
    For Each Item In Results
       MsgBox "Couleur HTML valide : " & Item.Value
    Next
    'Tout bon travail finit proprement. Il est temps de libérer la mémoire !
    Call Terminate
End Sub

Créer un motif d'expression rationnelle

Attention ! Avant de commencer, il faut rappeler une distinction importante. Les expressions rationnelles mettent à votre disposition un puissant outil de contrôle de validité syntaxique. Cela signifie que vous pouvez savoir si quelque chose est "bien écrit". En fait, les expressions rationnelles travaillent sur la forme, pas sur le fond. Ainsi, il est vrai que sofjgkfpzjebeoa@free.fr représente une adresse électronique syntaxiquement correcte, mais rien ne prouve qu'elle est réelle et attribuée à quelqu'un (si tel était le cas, ce dont je doute, mais bon... je tiens d'avance à m'excuser auprès de la personne malchanceuse... et farfelue (!) qui possède cette adresse). Maintenant que vous savez tout ça, nous pouvons entrer dans le vif du sujet.

Nous allons apprendre un créer un motif d'expression rationnelle. Démystifions la chose tout de suite : un motif (mot anglais : pattern) est une chaîne de caractères (type String). La seule différence (Attention ! Il n'y en a qu'une, mais elle est importante) entre une chaîne de caractères traditionnelle et un motif d'expression rationnelle, c'est que les caractères qui les composent n'ont pas le même sens. Dans une chaîne de caractères traditionnelle, le sens du caractère est le "sens courant". Par exemple, "[ ]" sont de simples crochets, "{}" de simples accolades, "." un point sans prétention, "\d" un antislash suivi de la lettre d, "+" le signe de l'addition, et ainsi de suite... En revanche, dans un motif d'expression rationnelle, certains caractères possèdent un "sens différent" qui leur permet de décrire la disposition des caractères de "sens courant". Créer un motif d'expression rationnelle consiste à abstraire un cas particulier pour obtenir... un cas général, dans lequel entrent tous les cas particuliers ! C'est la raison pour laquelle certains caractères changent de sens et acquièrent un statut différent des autres. Ce sont des caractères "de niveau supérieur"... des métacaractères (voilà, le mot est jeté !). Si vous voulez une comparaison, imaginez un peu que j'entreprenne de vous présenter l'anglais. Je pourrais écrire "La phrase good morning sert à souhaiter le bonjour en début de journée". Dans cet exemple, on distingue un langage décrit (l'anglais), et un métalangage descriptif (le français). C'est un peu pareil avec les expressions rationnelles. Les caractères "classiques" sont décrits par des caractères promus à un "niveau supérieur", avec un sens nouveau, qui en fait des métacaractères.

Considérez par exemple une couleur HTML classique. Si je vous demande de m'écrire une exemple de ce type de couleur, vous me proposerez peut-être quelque chose du genre "&HF0C01B". Ce n'est pas un très bon exemple, parce qu'il pourrait laisser croire à quelqu'un qui ne connaît pas les couleurs HTML qu'une couleur de ce type comporte toujours six chiffres. Or il est possible d'écrire "&HAB". Finalement, aucun exemple ne couvre tous les cas de figure. Il est plus judicieux de se demander ce qu'est une couleur HTML en tant que telle (hé oui, c'est comme Socrate qui demande ce qu'est la vertu, et à qui on donne en réponse des exemples de vertus, et pas la définition de la vertu dans le cas général). Dans ce domaine, les expressions rationnelles vont nous aider. Enfin... elles ne font pas tout quand même. Nous avons un peu de culture, nous savons qu'une couleur HTML est une valeur compris entre 0 et FFFFFF en notation hexadécimale, précédée de &H. Hé bien, voilà une bonne définition ! Il ne reste plus qu'à traduire cette phrase dans un langage accessible à l'ordinateur : un motif d'expression rationnelle. Pour cela, nous allons suivre une progression en douze points (hé oui, tant que ça !) :

  • Les métacaractères,
  • Les quantificateurs,
  • Les intervalles de reconnaissance,
  • Les classes de caractères,
  • Les classes complémentées,
  • Les classes génériques,
  • Masques et alternatives,
  • Les assertions simples,
  • Les assertions complexes,
  • Les ancrages,
  • Capturer des portions d'occurrences,
  • Retour sur les quantificateurs : la gourmandise.

Attention ! La liste paraît impressionnante... mais vous verrez que l'on en vient vite à bout. Après, quand la syntaxe vous sera devenue familière, cela vous paraîtra même "assez court". Evidemment, la première fois, ce n'est jamais facile. Et ce n'est rien que de bien normal !

Les métacaractères

Les métacaractères, comme je l'ai expliqué tout à l'heure, sont des caractères qui ont un sens particulier. Evidemment, à cause de ce sens supplémentaire, ils ne peuvent pas être employés n'importe comment. En particulier, vous vous doutez qu'il ne sera pas possible de les employer comme caractères classiques sans leur ajouter quelque chose pour les différencier de leur utilisation en tant que métacaractères. Mais en attendant, voici déjà une liste bien fournie (mais pas encore exhaustive, il en manque quelques-uns, ce sera pour une prochaine mise à jour).
Attention ! Ne prenez pas peur si vous trouvez plein de mots barbares ci-dessous. Nous allons revenir très longuement sur chacun d'entre eux. C'est promis, vous saurez tout (enfin presque tout...) mais pas tout de suite ! Pour l'instant, contentez-vous de savoir que ce sont des métacaractères.

"^" : Placé en début de motif, il signifie "début de la ligne". Placé après un crochet ouvrant, il définit une classe complémentée.

"$" : Comme métacaractère, toujours placé en fin de motif. Il signifie "fin de ligne"

"[" et "]" : Les crochets (ouvrant et fermant) délimitent les classes.

"(" et ")" : Les parenthèses (ouvrante et fermante) délimitent les masques et les alternatives.

"{" et "}" : Les accolades (ouvrante et fermante) délimitent les intervalles de reconnaissance.

"." : Le point remplace n'importe quel caractère, à l'exclusion d'un retour à la ligne.

"?", "*" et "+" : Le point d'interrogation possède deux autres sens (suppression de la gourmandise, suppression de la capture)

"|" : Séparateur des différents choix d'un masque ou d'une alternative (c'est l'union ensembliste chère aux matheux !).

"\" : Introduit une classe générique ou un ancrage. Également très utile pour séparer un caractère d'un métacaractère.
Voici une question que j'abordais déjà au début de ce paragraphe : Comment faire pour employer ces métacaractères dans leur "sens normal", c'est-à-dire comme de simples caractères? La réponse est très simple, il suffit de les faire précéder d'un antislash. Par exemple, "\+" désigne le caractère "+" et non le métacaractère "+", en raison de la présence de l'antislah (sauf erreur, c'est aussi de cette façon que l'on indique, en C, un guillemet dans une chaîne de caractères, en écrivant \". Enfin, bon, on ne parle pas de C ici, donc laissons tomber cet exemple).
Attention ! Il existe des exceptions. Gardez ça dans un coin de votre mémoire, car je ne l'ai pas écrit ici (ça fait appel à des notions que nous n'avons pas encore abordées). Mais je n'oublierai pas de vous le dire par la suite.

Les quantificateurs

Les quantificateurs, comme leur nom l'indique, permettent de dénombrer des caractères. Imaginez un peu les quantificateurs en anglais : some, any et no. Ce que nous allons faire, c'est à peu près la même chose, sauf que nous allons utiliser des symboles, et rendre la chose un peu plus rigoureuse.

? : signifie "au plus une occurrence" (c'est-à-dire rien ou une occurrence)
Exemple : Le motif "a?" permet de décrire exclusivement "a" et "" (la chaîne vide)

Le caractère étoile : * : signifie "rien, une ou plusieurs occurrences"
Exemple : Le motif "a*" permet de décrire entre autres "" (la chaîne vide), "a", mais aussi "aa", "aaa", "aaaa", etc...

+ : signifie "au moins une occurrence" (c'est-à-dire une ou plusieurs occurrences)
Exemple : Le motif "a+" permet de décrire entre autres "a", "aa", "aaa", "aaaa", etc..., mais pas la chaîne vide

Observation pour conclure :
Tous les quantificateurs sont postfixés, c'est-à-dire qu'ils suivent le caractère auquel ils s'appliquent.

Les intervalles de reconnaissance

Bon, vous avouerez que, jusqu'à présent, nous avons dénombré les occurrences de façon assez grossière : absence, unité ou répétition. Avec les expressions rationnelles, nous allons pouvoir aller plus loin et utiliser des entiers. C'est ce que l'on appelle les intervalles de reconnaissance :

{n} signifie "exactement n occurrences"
Exemple : Le motif "a{3}" permet de décrire la chaîne "aaa", et rien d'autre !

{n, p} signifie "entre n et p occurrences"
Exemple : Le motif "a{1, 3}" permet de décrire les chaînes "a", "aa" et "aaa"

{n,} signifie "au moins n occurrences"
Exemple : Le motif "a{5,}" permet de décrire les chaînes "aaaaa", "aaaaaa", "aaaaaaa", etc...

Observation pour conclure :
Ici aussi, les intervalles de reconnaissance sont postfixés.

Les classes de caractères

Les classes de caractères sont délimitées par des crochets. A l'intérieur de ces crochets se trouvent des caractères ou des intervalles de caractères (les caractères extrêmes sont alors reliés entre eux par un trait d'union). Par exemple :

[a-z] : signifie "tout caractère entre a et z (toute lettre de l'alphabet latin)"
Exemples :
"[a-z]+" permet de décrire "bonjour", "a", "et", etc..., mais pas la chaîne vide (à cause du quantificateur "+")
"[ziag]*" permet de décrire "zigzag", "z", "ai", "" (la chaîne vide), etc..., mais pas "ah" (car "h" n'est pas un élément de "[ziag]")
"[rdvls]i[sxz]e?" permet de décrire "ris", "riz", "rixe", "dis", "dix", "dise", "vis", "vise", "lis", "lise", "sis", "sise", "six".

[0-9] : signifie "tout chiffre compris entre 0 et 9"
Exemple : "[0-9]?" permet de décrire "1", "8", "9", "" (la chaîne vide), etc.., mais pas "21" (à cause du quantificateur "?")

Plus généralement : [c(1)c(2)...c(n)] signifie "qui comporte un caractère au choix parmi c(1), c(2), ..., c(n)"
Attention ! Si vous avez choisi de tenir compte de la casse (via oRegExp.IgnoreCase = False pour ceux qui auraient oublié), sachez que [a-z] diffère de [A-Z].

Remarque : Si vous souhaitez indiquer les intervalles a-z et A-Z, n'écrivez pas [A-z] sous prétexte que les majuscules viennent avant les minuscules dans le code ASCII. Il ne faut pas oublier que, sans vous en rendre compte, vous ajouteriez aussi la plage ASCII comprise entre (90 désigne "Z") 91 et 96 (97 désigne "a"), qui ne correspond pas à des caractères alphabétiques ! Évitez donc ces fantaisies et écrivez plutôt [a-zA-Z]. N'écrivez pas non plus des choses comme [aA-zZ] ou autres, qui n'ont évidemment pas le même sens.

Attention ! Vous avez vu que le trait d'union sert à désigner un intervalle de caractères (par exemple dans "[a-z]"). Pour l'utiliser en tant que trait d'union, il faut le placer en début de classe. Par exemple "[-a-z]" équivaut à "trait d'union et lettres de l'alphabet".

Les classes complémentées

Les classes complémentées sont des classes dans lesquelles vous indiquez non pas les caractères à rechercher, comme nous l'avons fait précédemment, mais ceux qu'il faut au contraire exclure des résultats. Ce sont aussi des ensembles de caractères mais, cette fois-ci, les occurrences retenues se composent de caractères qui ne sont pas éléments de l'ensemble.

[^a-z] : signifie "tout caractère non compris entre a et z (autre qu'une lettre de l'alphabet)"
Exemple : Le motif "[^aeiou]+" permet de décrire "pff", "pst", "grrr", mais pas "oh" (car le "o" est exclu : c'est une voyelle)

[^0-9_] : signifie "qui ne comporte ni chiffre ni underscore"
Exemple : Le motif "[^2-9_]{1,2}" permet de décrire "34", "8", etc.. mais pas "20" (zéro exclus) ou "234" (pas plus de deux caractères)

Plus généralement : [^c(1)c(2)...c(n)] signifie "qui ne comporte aucun des caractères c(1), c(2), ..., c(n)"

Attention ! Mêmes remarques que précédemment.
Vous vous souvenez ? Je vous avais dit de garder quelque chose dans un coin de votre mémoire... hé bien, nous y sommes ! Le crochet fermant et le trait d'union sont deux métacaractères. Ils gardent leur statut de métacaractère dans une classe, mais sont considérés comme de simples littéraux en dehors de celle-ci. Conclusion : en dehors d'une classe, il est inutile d'écrire "\]" ou "\-" avec un antislash.

Les classes génériques

Les classes génériques sont des classes prédéfinies (car très souvent utilisées). Il vous est donc inutile d'utiliser les classes de caractères et les classes complémentées correspondantes. Sachez cependant que cela ne provoquerait aucune erreur.

"\w" (w pour word) correspond à la classe de caractères "[a-zA-Z0-9_]"
"\d" (d pour digit) correspond à la classe de caractères "[0-9]"
"\W" (w majuscule pour (not a) word) correspond à la classe complémentée "[^a-zA-Z0-9_]"
"\D" (d majuscule pour (not a) digit) correspond à la classe complémentée "[^0-9]"
"\xn" avec n un entier en hexadécimal correspond à une valeur du code ASCII. Par exemple, Asc("A") = 65 et 65 = &H41 d'où "\x41" désigne A !

Masques et alternatives

Jusqu'à présent, nous avons vu comment inclure plusieurs caractères, mais nous ne nous sommes pas souciés de leur ordre. En effet, l'ordre d'apparition des caractères dans les classes est sans importance. Si vous souhaitez créer une structure où l'ordre des caractères est important, il faut utiliser des masques.
Prenons un exemple :

"fran(ce|co|c)" correspond exclusivement à l'un des trois mots suivants : "franc", "france", "franco"
En revanche "fran[ceo]{1,2}" correspond à "franc", "frane", "frano", "france", "franeo", "franco", etc...

Plus généralement : (séquence(0)|séquence(1)|...|séquence(n)) permet d'obtenir la séquence(0) ou la séquence(2) ou ... ou la séquence(n) avec respect de l'ordre des caractères. De plus, comme vous l'avez vu, vous pouvez définir des séquences de longueur différente.

Les assertions simples

Les assertions sont des contraintes imposées au moteur de recherche. Elles ne décrivent pas des caractères mais indiquent une zone de recherche. En gros, elles vous permettent de parler de "début de ligne", de "fin de ligne", de "limites de mots", etc...
Ce sont :

"^". Placé en tout début de motif, il signifie "au début d'une ligne"
Exemple : "^A.*" permet de rechercher toutes les lignes qui commencent par la lettre majuscule A ("A" tout seul, ou encore "Ah le bel exemple")

"$". Placé tout à la fin du motif, il signifie "à la fin de la ligne"
Exemple : ".*t$" permet de rechercher toutes les lignes qui finissent par la lettre minuscule t ("t" tout seul, ou "Enfin un exemple concret")

"\b". Cette assertion permet de désigner "une limite de mot"
Exemple : "^.*bon\b.*$" permet de décrire "un bon exemple" mais pas "le bonheur" car bon n'est plus suivi d'une limite de mot, mais d'un "h"

"\B". cette assertion permet de désigner "ce qui n'est pas une limite de mot"
Exemple : "^.*bon\B.*$" permet de décrire "le bonheur" mais pas "un bon exemple qui ne l'est plus" car bon est suivi d'une limite de mot

Les assertions complexes

Les assertions complexes ne sont pas très difficiles à comprendre, malgré leur nom.

(?:) Transforme des parenthèses normales en parenthèses non capturantes

(?=) Assertion avant positive (en anglais : positive lookahead)
Exemple : Soit la chaîne "Un bel exemple que celui-ci !". On applique le motif "bel\s(?=exemple)". Une occurrence est trouvée car "bel" est suivi de "exemple".
A l'inverse, si on applique ce même motif à la chaîne "Un bel oiseau sur la branche", aucune occurrence ne sera trouvée.

(?!) Assertion avant négative (en anglais : negative lookahead)
Exemple : Soit la chaîne "Un autre bel oiseau !". Le motif "bel\s(?!exemple)" donne une occurrence car bel n'est pas suivi de "exemple"
A l'inverse, si on applique ce même motif à la chaîne "Un bel exemple", aucune occurrence ne sera trouvée.

(?<=) Les assertions arrière positive (en anglais : positive lookbehind) et arrière négative (en anglais negative lookbehind) ne sont malheureusement pas accessibles avec Visual Basic 6. Sauf erreur, les expressions rationnelles obtenues ne sont pas considérées comme valides. A vérifier quand même... dans une prochaine mise à jour

Les ancrages

"\n" désigne une nouvelle ligne. C'est la syntaxe habituelle dans de nombreux langages (comme le C, le Caml, etc...).
"\r" désigne un retour chariot. Pour ceux qui ne le sauraient pas (je ne sais pas s'il y en a... mais bon je n'en suis pas à quelques mots près) : le retour chariot, Chr(13), "symbolise" ce qui se passait jadis (oh, ce n'est pas non plus du temps de Cro Magnon !) quand on utilisait une machine à écrire. Pour aller à la ligne, il fallait tourner la molette et ramener le chariot en position initiale. C'est ainsi que, sous Windows, les retours à la ligne sont de type vbCrLf, avec vbCr le retour chariot (ou Chr(13)), et vbLf (ou Chr(10)) le retour à la ligne proprement dit. Attention toutefois à ne pas dire que vbCrLf et vbNewLine sont une seule et même chose car ce n'est pas vrai avec tous les systèmes d'exploitation. C'est vrai sous Windows, mais faux sous Linux, où vbNewLine se confond avec vbLf (absence du retour chariot vbCr).
"\s" (s pour space) désigne un espace blanc. Il peut s'agir d'une tabulation ("\t"), d'un espace ("\f"), d'un retour à la ligne ("\n") ou d'un retour chariot ("\r")
"\S" (s majuscule pour (not a) space) représente tout ce qui n'est pas un espace blanc. On peut lui trouver un équivalent sous la forme de la classe complémentée [^\t\r\n\f].

Capturer des portions d'occurrences

Remettons les choses au clair. Lorsque vous appelez la méthode Execute, vous récupérez un jeu (une collection) de résultats sous la forme d'un objet de type MatchCollection (notre variable Results). Chaque résultat contenu dans l'objet MatchCollection est une occurrence, représentée par un objet de type Match (notre variable Item). Seulement, vous souhaitez peut-être diviser chacune de ces occurrences en plus petites unités. C'est ici qu'interviennent les parenthèses. Considérez par exemple le motif suivant :

"\w+\d{2}"

Si nous voulons obtenir séparément le texte et les chiffres, nous écrirons plutôt :

"(\w+)(\d{2})"

L'ajout de parenthèses indique que nous souhaitons découper les occurrences en petites unités. Ces sous-résultats sont appelés des SubMatches. Ce ne sont pas des objets, contrairement à Match et MatchCollection. Ce sont de simples chaînes de caractères. Pour les obtenir à partir d'une variable de type Match, on peut utiliser deux syntaxes (même si, en fait, l'une des deux doit être préférée). La première, la moins bonne, est :

'Variable destinée à recevoir les sous-résultats 
Dim sSubItem As Variant 
For Each sSubItem In Item
    MsgBox sSubItem
Next

Je réprouve cette écriture car elle nécessite une variable de type Variant (comprendre : fantaisiste et pas vraiment typée), alors qu'il n'y a aucune ambiguïté puisque tous les sous-résultats sont de type String. Comme il n'y a aucune raison d'écrire de telles choses, il vaut mieux préférer :

'Index des sous-résultats 
Dim k As Long 
For k = 0 To Item.SubMatches.Count
    MsgBox Item.SubMatches(k) 
Next 

Attention ! Capturer des portions de texte (autrement dit : créer des SubMatches) est une opération coûteuse dont il convient de ne se servir que lorsqu'elle est vraiment utile (d'ailleurs, même si elle ne l'était pas, par principe, on ne programme pas quelque chose qui ne sert à rien). Si vous souhaitez utiliser des parenthèses dans un motif sans que le moteur d'expressions rationnelles ne capture le texte pour autant, placez "?:" (un point d'interrogation suivi du signe de ponctuation deux points) directement après la parenthèse ouvrante. Par exemple :

Le motif "<([a-z]+|[0-9]{2,3})>" capture un sous-résultat correspondant au motif [a-z]+|[0-9]{2,3}
Le motif "<(?:[a-z]+|[0-9]{2,3})>" ne capture aucun sous-résultat.

Retour sur les quantificateurs : la gourmandise

Vous en savez maintenant assez pour que j'aborde une nuance soigneusement cachée jusqu'à présent... La gourmandise. Non, je ne veux pas vous parler de notre rapport à la nourriture :-). Les quantificateurs + et * sont "gourmands". Cela signifie qu'ils vont toujours chercher à trouver la plus longue chaîne possible. Par exemple, imaginons une page fictive du réseau Codes-Sources, où se trouverait le code HTML suivant :

<http://www.codes-sources.com/code.aspx?ID=999999>Ghost page</xxx>

Admettons que nous cherchions à récupérer l'adresse de la page de code (fictive... heureusement qu'il n'y a pas autant de sources sur VBFrance !).
Nous pourrions élaborer le motif suivant : "<(.+)>".
Attention ! On peut proposer d'autres motifs plus précis qui éviteraient le problème que je vais soulever... mais l'intérêt serait alors nul ! J'ai proposé ce motif simpliste dans un but bien particulier. Ne me dites donc pas qu'on pouvait faire autrement et contourner ainsi le problème.

Revenons à notre sujet. En appliquant le motif (simpliste) au texte précédent, nous obtenons un objet MatchCollection contenant un élément Match tel que :
Value = "<http://www.codes-sources.com/code.aspx?ID=999999>Ghost page</xxx>"
SubMatches(0) = "http://www.codes-sources.com/code.aspx?ID=999999>Ghost page</xxx"

Oh ! Ce n'est pas du tout ce que nous voulons. Pourquoi ? Tout simplement parce que le plus long résultat ne s'arrête pas au premier symbole ">" puisqu'il en existe un autre plus loin. C'est ce comportement que l'on appelle la gourmandise. Alors, comment y remédier. Facile !
Ajoutons un point d'interrogation de la façon suivante : "<(.+?)>"

Maintenant, effectuons un nouvel essai.
Que trouvons-nous ? Que du bonheur...

Value = "<http://www.codes-sources.com/code.aspx?ID=999999>Ghost page</xxx>"
SubMatches(0) = "http://www.codes-sources.com/code.aspx?ID=999999"

Conclusion : Si vous souhaitez que les quantificateurs cessent de chercher l'occurrence la plus longue, et se bornent à la première séquence conforme, il vous faut remplacer les versions gourmandes * et + par leurs équivalents non gourmands *? et +?. A ce propos, notez que le point d'interrogation est polysémique ! Placé après un caractère, c'est un quantificateur. Placé après un quantificateur, il supprime la gourmandise... Et ce n'est pas tout ! Nous verrons par la suite qu'il possède un troisième sens (pour les impatients, allez voir "L'objet Match" et la rubrique "Attention !")...

Les autres commandes

Nous avons vu qu'il existait une commande Execute grâce à laquelle il était possible de trouver toutes les occurrences d'un motif. Vous disposez également de deux autres commandes qui peuvent se révéler utiles :

'Existe-t-il au moins une occurrence du motif ? 
Dim bCanFindSomething As Boolean 
    bCanFindSomething  = ORegExp.Test(sText) 
'Remplacer les occurrences par un autre texte 
Dim sReplace As String, sNewText As String 
'Affectation : vous pouvez mettre ce que vous voulez ! 
sReplace =  "New !" 
'Le résultat de la fonction Replace n'est autre que le nouveau texte, après les remplacements 
sNewText = ORegExp.Replace(sText, sReplace)

MatchCollection et Match dans le détail

L'objet MatchCollection

Attention ! Souvenez-vous que "Results" est une variable objet de type MatchCollection définie tout au début de ce tutoriel. Je n'ai pas repris sa déclaration ici.

Nous avons vu que les variables MatchCollection contiennent l'ensemble des résultats obtenus après exécution d'une commande Execute. Vous disposez de deux méthodes :

'Dénombrer le nombre d'occurrences
Dim ItemCount As Integer 
ItemCount  = Results.Count 

Ceci vous sera utile après la recherche, pour savoir si quelque chose a été trouvé.

For k =  0 To Results.Count - 1
    'Sélection d'un résultat
    Set Item = Results.Item(k) 
    'ou Set Item = Results(k)
     '       ...
Next 

La méthode Item vous permet d'accéder à un résultat particulier à partir de son index (compris entre 0 et Results.Count - 1). Cependant, une telle boucle ne devrait pas être privilégiée, car elle est moins rapide que son équivalent For Each ( Attention ! Encore une fois, c'est une question d'optimisation. Il n'y a pas de différence flagrante pour une utilisation modérée des expressions rationnelles. Cependant, si vous prenez l'habitude d'utiliser la deuxième structure, ça ne nuira pas à votre code...) :

For Each Item In Results
    '...
Next

L'objet Match

L'objet Match est plus riche que le précédent. Il vous permet en effet d'obtenir plusieurs choses relativement intéressantes. Attention ! Souvenez-vous que "Item" est une variable objet de type Match définie tout au début de ce tutoriel (je sais, ça fait longtemps...). Je n'ai pas repris sa déclaration ici.

'Obtenir le texte de l'occurrence
Dim sItemText As String
sItemText  = Item.Value

'Obtenir la longueur de l'occurrence
Dim lItemLength As Long
lItemLength =  Item.Length

Remarque : Cela revient exactement à calculer Len(sItemText) où sItemText désigne la variable précédemment définie... mais c'est aberrant. Il est inutile de refaire ce qui a déjà été fait !

'Index de la première occurrence dans le texte d'origine
Dim lFirstIndex As Long
lFirstIndex = Item.FirstIndex

Un exemple concret : les adresses électroniques

Suite à une remarque de Econs, je vous propose maintenant d'appliquer ce que nous avons vu dans ce tutoriel en proposant un motif d'expression rationnelle pour des adresses électroniques. Pour cela, il faut d'abord se demander ce qui définit une adresse électronique. Il s'agit :

  • Du nom choisi par l'utilisateur (pierre_6, paul12, etc...)
  • Du nom du fournisseur d'accès à internet (free, yahoo, etc... et je ne fais pas de publicité !)
  • Du type (fr, com, etc...)

On admet généralement que le nom d'utilisateur peut comporter n'importe quelle lettre de l'alphabet. En revanche, les chiffres, le point et l'underscore ne sont pas admis qu'à partir du second caractère. Sinon, pour le nom du fournisseur d'accès à internet, ainsi que pour le type, nous allons nous simplifier le travail et considérer que tout élément de la classe générique \w (petit rappel : [a-zA-Z0-9_]) est accepté. Voici ce que ça donne, en plusieurs étapes :

Nom d'utilisateur

Le premier caractère est toujours une lettre : [a-z]
Le (les) autre(s) caractère(s), s'il(s) existe(nt), autorisent aussi le point, l'underscore et les chiffres : [\w\.]*
-> Le \w désigne la classe générique word déjà vue plus haut
-> \. désigne le point en tant que point, et non en tant que métacaractère.
Rappelez-vous : il faut un antislash, sauf hors d'une classe pour le crochet fermant et le trait d'union.
-> * est un quantificateur qui autorise : absence (pas de caractère), unité (un caractère) ou multiple (plusieurs caractères).

Résumons-nous : Le nom d'utilisateur s'exprime grâce à [a-z][\w\.]*

L'arobas (ou arobase, ou arrobe...)

C'est d'une intense difficulté :-).
La bonne réponse est @

Le nom du fournisseur

On simplifie la tâche en disant simplement \w+
Le + est utile parce que les fournisseurs ne choisissent jamais la chaîne vide comme nom !

Le point (hé oui il y a un point entre le nom du fournisseur et le type !)

On ne veut pas le métacaractère mais le caractère donc \.

Le type

Comme précédemment.

L'heure est à la simplicité : \w+

Conclusion : Dormez tranquilles. On ne remplira plus vos formulaires avec des adresses électroniques syntaxiquement incorrectes.
Utilisez "[a-z][\w\.]*@\w+\.\w+".
Maintenant, peut-être voulez-vous capturer le nom, le nom du fournisseur et le type séparément ? Pas de problème !
Utilisez des parenthèses capturantes : "([a-z][\w\.]*)@(\w+)\.(\w+)".
Et puis, dernière chose pour finir, n'oubliez pas de ne pas tenir compte de la casse (oRegExp.IgnoreCase = True ).

Perspectives

Quoi de plus naturel que de terminer un tutoriel par quelques idées de programmes ou d'algorithmes en rapport avec les expressions rationnelles ? En vrac, voici quelques idées qui me viennent à l'esprit. La liste ne prétend évidemment pas à l'exhaustivité !

  • Comptes de messagerie (serveurs, adresse)
  • Extraction "avancée" de données à partir d'un tableau, d'une page web
  • Coloration syntaxique de code source (à la volée avec RichTextBox et SendMessage)
  • Jeux de lettres tels que Scrabble, anagrammes, dictionnaire, etc.
  • Tellement d'autres choses encore...

Conclusion

Vous voici au bout de vos peines... du moins en théorie. Maintenant que vous savez comment rédiger un motif d'expression rationnelle, et utiliser l'outil mis à votre disposition sous Visual Basic, il ne vous reste plus qu'à vous entraîner pour prendre vos marques et acquérir une certaine aisance. Courage, le plus dur est fait ! Et puis... tout ceci méritait bien les efforts que vous avez fournis : grâce aux expressions rationnelles, vous regarderez d'un autre oeil les Left, Right et autres Mid !

Ce document intitulé « Les expressions rationnelles ("régulières") » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous