Tutoriel : les expressions rationnelles du composant COM RegExp et ses aspects non documentés

omen999

Les expressions rationnelles (ou régulières) sont souvent écartées des scripts VBS ou VBA sous MS Office par les développeurs occasionnels qui trouvent la syntaxe des motifs particulièrement rebutante. Pour leur part, les plus expérimentés jugent limitées les possibilités offertes par ce composant par comparaison aux outils disponibles sous Perl, Python ou d'autres langages. Cette réputation a été, en outre, desservie par une documentation incomplète qui ne rend pas bien compte de certains des aspects les plus avancés du composant. Ce sont ces aspects que cet article se propose de détailler.
Commentez

- Titre : Tutoriel : les expressions rationnelles du composant COM RegExp et ses aspects non documentés

- Auteur : omen999

- Parution : 15 mai 2020

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2020 omen999. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Les groupes sans capture

- II-A. (?:modèle) : consommation sans capture

- II-B. (?=modèle) : assertion avant positive

- II-C. (?!modèle) : assertion avant négative

- III. La propriété MultiLine

- IV. La méthode Replace

- IV-A. Les substitutions supportées

- IV-B. La fonction de rappel

- IV-C. Un exemple VBA

- V. Liens et téléchargement

- VI. Remerciements

I - Introduction

Le composant « Microsoft VBScript Regular Expressions 1.0 » ou RegExp accompagnait la sortie de la version 5.0 de VBScript. Ses possibilités étaient tellement limitées - ou risibles - que Microsoft a très rapidement revu sa copie à l'occasion de la sortie de la dernière évolution majeure de VBScript, la version 5.5. Pour marquer cette simultanéité, la nouvelle version sera directement numérotée 5.5 et c'est donc le « Microsoft VBScript Regular Expressions 5.5 » qui est désormais disponible depuis juillet 2000 pour tous les langages capables d'instancier des objets COM, plus particulièrement VBScript, JScript ainsi que le VBA des applications MS Office.

Mais à la même époque, Microsoft révélait publiquement son projet de framework NET qui allait mobiliser de gros moyens dans les années qui suivirent et pousser le Windows Script Host et VBScript vers l'obsolescence. Ce désintérêt brutal a eu un effet direct sur la qualité de la documentation qui accompagnait ce produit désormais mineur. La dernière documentation fichier complète se rapporte à la version 5.6 mais il s'agit de la simple mise à jour, faite avec plus au moins de bonheur et d'attention, de celle originale de la version 5.0.

L'actualisation de la documentation de référence du composant RegExp 5.5 pour VBScript a consisté, pour l'essentiel, à ajouter la description de la collection SubMatches sans vraiment se soucier de sa cohérence avec les explications générales communes aux deux langages JScript/VBScript. Il en résulte que plusieurs éléments de sa syntaxe n'ont pas été clairement décrits voire pas du tout pour certains d'entre eux. C'est pourquoi cet article décrira successivement les groupes sans capture, les assertions avant y comprises, la propriété Multiline ainsi que la syntaxe complète de la méthode Replace avec des exemples pour illustrer leur emploi. Pour cette dernière, un exemple spécifique en VBA sera donné compte tenu des particularités du langage.

Cet article n'est pas une initiation à l'utilisation du composant RegExp et suppose que le lecteur en possède une bonne connaissance préalable. Il existe un article détaillé sur le sujet pour aborder la question.

II - Les groupes sans capture

L'utilisation du groupe dans la syntaxe des motifs permet d'introduire la notion de sous-correspondance pour chaque occurrence principale retrouvée. Il consiste à entourer par des parenthèses une portion du motif que l'on cherche à capturer pour placer son résultat dans une mémoire tampon qui sera accessible de trois façons différentes : soit dans la suite du motif sous la forme d'une référence arrière pour gérer les répétitions (\1 à \99), soit comme membre de la collection SubMatches d'une occurrence renvoyée par la méthode Execute, soit enfin en tant que variable de la chaîne de substitution de la méthode Replace ($1 à $99). Il est donc possible de définir jusqu'à quatre-vingt-dix-neuf groupes distincts dans un motif.

Mais à cette notion simple s'ajoutent des groupes qui ont la particularité inverse de ne pas rechercher la sous-correspondance voire même de ne pas consommer de caractères. En effet, l'utilisation des groupes sans capture présente deux avantages :

- un code plus performant, car la création de sous-correspondances est coûteuse en temps d'exécution comme en occupation mémoire ;

- la possibilité de créer des motifs complexes tout en restant dans la limite des quatre-vingt-dix-neuf sous-correspondances.

Ces groupes ne figurent pas dans la syntaxe de référence de la propriété Pattern.

II-A - (?:modèle) : consommation sans capture

À première vue, il pourrait sembler paradoxal de définir un groupe dans un motif tout en refusant son objet principal : la capture dans une sous-correspondance. Cette option a manifestement été créée pour pallier les effets de la syntaxe particulière de l'alternative. En effet, celle-ci a besoin d'opérer au sein d'un groupe pour être effective(1). Or, l'utilisation d'une alternative n'implique pas, le plus souvent, qu'on ait besoin de capturer son résultat. C'est pourquoi la combinaison du groupe non capturant et de l'alternative est la situation la plus courante :

Exemple de motif :

damoise(?:aux|au|lles|lle)

II-B - (?=modèle) : assertion avant positive

Il est assez fréquent de vouloir capturer des occurrences suivies par des caractères fixes et définis. Un exemple banal va consister à récupérer uniquement les montants de devises libellés en euros sachant que ce symbole figure toujours en fin de montant avec ou sans espace. La solution sans assertion avant consisterait à définir un motif incluant le symbole monétaire correspondant, le montant recherché étant alors récupéré au moyen d'une sous-correspondance :

Exemple avec sous-correspondance :

reg.Pattern = "\b((?:\d|\.|,)+) ?€"
MsgBox reg.Execute("231.451,12€  452$  458.21£")(0).SubMatches(0)

Le même résultat peut être obtenu avec une assertion avant positive tout en faisant l'économie de la sous-correspondance :

Exemple avec assertion avant positive :

reg.Pattern = "\b(?:\d|\.|,)+(?= ?€)"
MsgBox reg.Execute("231.451,12€  452$  458.21£")(0)

Résultat pour les deux exemples :

231.451,12€

L'absence de consommation de caractères réserve en pratique les assertions avant positives à la détection de la fin du motif.

II-C - (?!modèle) : assertion avant négative

L'assertion avant négative va nous permettre, dans l'exemple qui suit, d'écarter d'une liste de différentes dates exprimées en trois langues celles qui tombent un mois de septembre. L'utilisation de l'assertion négative parce qu'elle ne consomme pas de caractères, est quasiment indispensable dans ce cas de figure :

Exemple avec assertion avant négative :

reg.Global = True
' un groupe sans capture est aussi utilisé avec l'alternative ainsi qu'un quantificateur non gourmand
reg.Pattern = "\b\d{2,4}-(?!sept(?:iembre|embre|ember)).+?-\d{1,2}"
Set matchs = reg.Execute("2009-mars-12 07-septembre-08 2005-novembre-10 2017-marzo-25 2007-july-21 2016-august-31 2019-juin-10 2018-september-7 2001-septiembre-11")
For i = 0 to matchs.Count - 1
MsgBox matchs(i)
Next

Résultat (sur une seule ligne) :

2009-mars-12 2005-novembre-10 2017-marzo-25 2007-july-21 2016-august-31 2019-juin-10"

Enfin, l'utilisateur averti notera que l'objet RegExp ne supporte pas les assertions arrière personnalisables mais seulement les plus simples comme ^ ou \b.

III - La propriété MultiLine

Historiquement, les premiers outils de recherche d'expressions rationnelles avaient été conçus pour travailler ligne par ligne et par voie de conséquence, les assertions simples ou ancrages que sont les caractères « ^ » et « $ » ne pouvaient servir qu'à repérer respectivement le début et la fin de chaque ligne. Ce mode, qui n'est pas actif par défaut dans RegExp, peut être défini en plaçant la propriété MultiLine de l'objet RegExp à True. Cette propriété non documentée n'a d'ailleurs que ce seul effet. Elle n'a pas d'incidence sur le comportement du métacaractère « . » qui ne peut jamais capturer le caractère nouvelle ligne « \r », et ce, quel que soit le statut de MultiLine.

Si l'activation de cette propriété n'apporte aucune aide pour la gestion des captures de saut de ligne dans un motif, elle peut apporter une petite simplification dans l'écriture de celui-ci. En effet, le caractère « ^ » est une assertion arrière positive et, à ce titre, celle-ci ne consomme pas de caractères inutiles. Il est donc possible d'écrire plus simplement :

Exemple avec MultiLine = True :

reg.MultiLine = True
' recherche des mots abcde placés en début de ligne
reg.Pattern = "^abcde\b"

Au lieu de :

Exemple avec MultiLine = False :

' recherche des mots abcde placés en début de ligne
reg.Pattern = "(?:^|\n)abcde\b"

IV - La méthode Replace

Les éléments de syntaxe décrits ci-dessus peuvent être retrouvés dans certains tutoriels consacrés à la syntaxe du composant RegExp. Leurs auteurs ont compris qu'il fallait compléter la documentation de référence par des éléments piochés dans l'introduction générale consacrée à la présentation du composant.

En revanche, les possibilités réelles de la méthode Replace ont été passées sous silence presque totalement. Le « presque » fait référence à un bref résumé des nouveautés introduites par la version 5.5 écrit par Peter Torr, l'un des responsables du projet. Dans un des derniers paragraphes, il indique laconiquement à propos de la méthode Replace : « Replacement functions in VBScript work much the same as in JScript. Simply pass the return of the GetRef function instead of the replacement string. »

En clair, la méthode Replace présente en réalité deux syntaxes distinctes :

- la première, simple et (à moitié) documentée, consiste à fournir en deuxième paramètre une chaîne de substitution qui viendra remplacer une ou toutes les occurrences capturées selon le mode de RegExp ;

- la seconde attend comme deuxième paramètre l'adresse d'une fonction de rappel dont la valeur renvoyée viendra remplacer la ou les occurrences capturées.

Cerise sur le hamburger, même la première syntaxe n'a pas été correctement complétée puisqu'un certain nombre de variables de substitution ont été purement et simplement oubliées. Ce sont ces variables que nous allons passer en revue dans un premier temps avant de décrire en détail les caractéristiques de la fonction de rappel et les situations dans lesquelles elle présente un réel intérêt. Enfin, un exemple écrit en VBA pour Word fournira un exemple plus spécialement adapté à ce langage.

IV-A - Les substitutions supportées

La chaîne de substitution attendue comme deuxième paramètre par la méthode Replace(source,substitution) peut contenir des variables qui ont la particularité d'être incluses sous forme littérale dans la chaîne. La documentation précise qu'il est possible d'utiliser au maximum neuf de ces variables notées ($1 à $9) représentant chacune une sous-correspondance dans l'occurrence capturée. Cette limite est erronée car il est tout à fait possible de définir un plus grand nombre de variables. Par ailleurs, et pour une obscure raison, quatre autres variables ont été oubliées alors qu'elles présentent un réel intérêt :

- $& : l'occurrence complète ;

- $` : la sous-chaîne qui précède l'occurrence courante depuis le début de la chaîne source ;

- $' : la sous-chaîne qui suit l'occurrence courante jusqu'à la fin de la chaîne source ;

- $_ : la totalité de la chaîne source.

Un exemple simple suffit à le démontrer :

reg.Pattern = "(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})"
MsgBox reg.Replace("123456789012345678901234567890","$1 $2 $3 $4 $5 $6 $7 $8 $9 $10 $11 $12 $_ suite ")

Résultat :

12 34 56 78 90 12 34 56 78 90 12 34 123456789012345678901234567890 suite 567890

Notez que les variables $1 à $9 peuvent également s'écrire $01 à $09 quand il devient nécessaire, par exemple de distinguer $10 de $1 + « 0 ».

IV-B - La fonction de rappel

Les variables de substitution ont été imaginées pour effectuer sur les occurrences capturées toutes les opérations de formatage et de permutation imaginables à l'aide des sous-correspondances. En revanche, si vous avez besoin de procéder à des traitements plus complexes sur les occurrences concernées (changement de casse, renumérotation ou conversion d'unités), alors la fonction de rappel est faite pour vous.

Le court exemple fourni par Peter Torr dans l'article visé supra n'évoque même pas la gestion des sous-correspondances par la fonction de rappel. Il considérait sans doute que le renvoi à la syntaxe du JScript devait suffire au lecteur éclairé. Après examen de cette dernière, on est donc en mesure de déterminer l'exacte syntaxe de la méthode Replace et sa fonction de rappel dont le nom peut être choisi librement (ici cbReplaceProc) :

' l'exemple a été rendu plus complexe avec deux groupes
Function cbReplaceProc(match, sub1, sub2, pos, source)
If sub2 = 9 Then
sub1 = sub1 + 1
cbReplaceProc = "(" & sub1 & ".0)"
Else
sub2 = sub2 + 1
cbReplaceProc = "(" & sub1 & "." & sub2 & ")"
End If
End Function
reg.Pattern = "\((\d{1,4})\.(\d{1})\)" ' deux groupes ont été définis
' renumérotation avec un incrément de 0.1
MsgBox reg.Replace("(1.5) -- (1002.9) -- (12.0)",GetRef("cbReplaceProc"))

Résultat :

(1.6) -- (1003.0) -- (12.1)

Le plus important est de noter la façon dont les sous-correspondances sont présentées à la fonction de rappel. Comme VBScript ne supporte pas les paramètres facultatifs, la syntaxe de la fonction doit impérativement suivre celle du motif quant au nombre de groupes définis.

Notez que son utilisation ne présente pas vraiment d'intérêt quand le mode Global est False. Il est tout aussi simple de recourir à la technique équivalente qui consistera à extraire l'occurrence au moyen de la méthode Execute, d'y appliquer le traitement souhaité puis appeler la méthode Replace avec son résultat. En revanche, une renumérotation portant sur des centaines d'occurrences au sein d'un long document sera beaucoup plus performante avec cette technique.

IV-C - Un exemple VBA

Comme tout composant COM, RegExp peut être exploité par tous les langages capables d'instancier un objet conforme à cette norme. Une macro VBA des applications MS Office peut donc le référencer pour bénéficier de ses fonctionnalités. Si la majeure partie de ce qui précède peut être utilisée directement dans une macro, la fonction de rappel utilise dans l'exemple ci-dessus une fonction spécifique du VBScript, GetRef, qui est absente de ce langage. Elle est absente pour la bonne et simple raison qu'elle existe déjà, mais sous une autre forme.

Contrairement aux fonctions de rappel traditionnelles dont le paramètre est un pointeur direct vers l'adresse d'entrée du code de la fonction, la méthode Replace attend aussi un pointeur mais vers un object IDispatch dont la fonction sera la méthode par défaut. C'est le rôle de GetRef de fournir cette adresse. Le VBA obtient le même résultat en créant un module de classe dont la méthode publique par défaut sera la fonction de rappel. Malheureusement, l'IDE natif du VBA masque le paramètre qui permet de qualifier par défaut une méthode de classe. Il est donc nécessaire d'importer le module de classe à partir d'un fichier texte pour contourner cette limitation. En tout état de cause, l'article ne fournit pas le fichier Word prêt à l'emploi mais un fichier archive qui contient un fichier texte Fournitures.txt à convertir dans Word au format docm ainsi que les deux fichiers NewMacros.bas et clsReplace.cls à importer dans le projet du document ouvert.

La macro exemple ConvertDevises(2) pour Word, a pour tâche de rechercher dans l'ensemble du document actif tous les montants de devises connues et de les convertir en euros aux taux de change définis par le programme. Cette macro convertira au plus trois devises caractérisées par leur code ISO précédant le montant d'un espace (le dollar américain USD, le yen japonais JPY et le bolivar souverain vénézuélien VES). La conversion est appliquée directement sur le document qui sera mis à jour.

Exemple de macro utilisant la fonction de rappel : NewMacros.bas

Attribute VB_Name = "NewMacros"
Dim reg As New RegExp
Dim cbReplace As New clsReplace
Sub ConvertDevises()
reg.Global = True
reg.IgnoreCase = True
reg.Pattern = "\b(USD|JPY|VES) ((?:\d|\.|,)+)"
ActiveDocument.Range.Text = reg.Replace(ActiveDocument.Range.Text, cbReplace)
End Sub

Exemple de macro utilisant la fonction de rappel : clsReplace.cls

VERSION 1.0 CLASS
BEGIN
MultiUse = -1  'True
END
Attribute VB_Name = "clsReplace"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Public Function cbReplaceProc(match As String, sub1 As String, sub2 As String, pos As Long, source As String) As String
Attribute ReplaceProc.VB_Description = "Fonction de rappel RegExp.Replace"
Attribute ReplaceProc.VB_UserMemId = 0
Dim cur As String, curC As Currency, Tx As Variant
Tx = Array(0.92,0.0086,0.0000054)
cur = Replace(sub2, ".", "")
Select Case UCase(sub1)
Case "USD"
curC = CCur(cur) * Tx(0)
Case "JPY"
curC = CCur(cur) * Tx(1)
Case "VES"
curC = CCur(cur) * Tx(2)
End Select
cbReplaceProc = FormatCurrency(curC)
End Function

Contenu du document Word à traiter : Masques.txt

Masques : USD 1.250,12  JPY 133.325,45  VES 213431250,00
Ballons : JPY 80.184,00 USD 754,45  VES 128.543.568,00
Gel moussant : VES 3.143.816.687,00   USD 18451,74  JPY 1967693,59
Quantité : 2500     Code : 4581-852AF
(répété plusieurs fois)

V - Liens et téléchargement

L'archive peut être téléchargée ici : VBASample.zip.

Vous pouvez poser vos questions et laisser vos commentaires et suggestions sur le forum ici : le lien.

VI - Remerciements

Je tiens à remercier escartefigue pour sa relecture attentive et ses observations.

(1) Les drosophilephiles m'objecteront que ce n'est pas tout à fait exact et que les parenthèses ont seulement pour effet, en modifiant la priorité d'exécution de l'opérateur, d'en limiter la portée mais l'observation reste juste en pratique.

(2) Pour être fonctionnelle, la macro suppose que les préférences régionales définissent l'euro comme devise et la virgule comme séparateur des décimales.