27 octobre 2021

Regexp : pourquoi il faut aimer les expressions régulières – Partie 2

Dans la première partie de cet article sur les Regexp, nous expliquions comment et pourquoi les utiliser.

Regroupement

L’utilisation des parenthèses permet d’avoir une sorte de regroupement qui est très utile quand nous faisons de l’extraction de plusieurs informations. Prenons comme exemple d’utilisation l’extraction de l’adresse mail de l’expéditeur et l’adresse mail du destinataire de la chaîne de caractère de l’exemple précédent. Dans ce cas, nous allons écrire une regexp qui comporte un groupe correspondant à l’adresse mail de l’expéditeur et un autre groupe correspondant à l’adresse mail du destinataire. Voici une proposition de regexp :

z = re.findall('^From.*? (\S+@\S+) .* to (\S+@\S+)', chr)
print(z)

Le résultat est une liste de tuple contenant les deux groupes : l’adresse mail de l’expéditeur et l’adresse mail du destinataire :

[('marwa.thlithi@invivoo.com', 'admin@invivoo.com')]

 Ces groupes sont numérotés et nous pouvons y accéder un par un avec la fonction group() :

adresses_mail = re.match('^From.*? (\S+@\S+).*to (\S+@\S+)', chr)
chaine_complete = adresses_mail.group(0)
print(chaine_complete)
expediteur = adresses_mail.group(1)
print(expediteur)
destinataire = adresses_mail.group(2)
print(destinataire)

le résultat est :

From marwa.thlithi@invivoo.com Sat Jan 5 09:14:16 2020 to admin@invivoo.com marwa.thlithi@invivoo.com 
admin@invivoo.com

L’indice 0 est un argument par défaut qui correspond à l’ensemble de la regex.

Nous pouvons aussi utiliser la fonction groups() qui permet d’obtenir un tuple contenant les chaînes pour tous les sous-groupes :

adresses_mail = re.match('^From.*? (\S+@\S+).*to (\S+@\S+)', chr)
adresses_mail.groups()

le résultat est affiché dans l’ordre en commençant par le premier sous-groupe jusqu’au dernier :

('marwa.thlithi@invivoo.com', 'admin@invivoo.com')

NB : les fonctions group() et groups() sont applicables sur des objets de type MatchObject. Pour cela, j’ai utilisé la fonction re.match() dans l’exemple ci-dessus. Nous pouvons aussi utiliser la fonction re.search() qui retourne un MatchObject.

Groupes nommés

Le regroupement par des ensembles numérotés permet de capturer des sous-chaînes intéressantes et y accéder en utilisant les numéros de groupes, ce qui permet d’avoir des expressions régulières structurées. Néanmoins, quand il s’agit des regex complexes, le suivi des numéros de groupes devient très difficile. Dans ce cadre, une fonctionnalité de nommage de groupes a été proposée afin de référencer directement les groupes par leurs noms, ce qui permet d’avoir des regex encore plus structurées et plus claires. La syntaxe du nommage des groupe est (?P<nom> expr)nom est le nom du groupe et expr est la regex de la sous-chaîne à extraire.

Reprenons l’exemple précédent et réalisons-le maintenant avec des groupes nommés :

chr = 'From marwa.thlithi@invivoo.com Sat Jan 5 09:14:16 20 to admin@invivoo.com'
adresses_mail = re.match('^From.*? (?P<expediteur>\S+@\S+).*to (?P<destinataire>\S+@\S+)', chr)
expediteur = adresses_mail.group('expediteur')
print(expediteur)
destinataire = adresses_mail.group('destinataire')
print(destinataire)

Le résultat d’affichage est le même que celui avec les groupes numérotés, sauf qu’en termes de lisibilité de la regex, cette version est beaucoup plus claire et significative grâce au nommage des groupes :

marwa.thlithi@invivoo.com 
admin@invivoo.com

De plus, nous pouvons récupérer directement les groupes nommés dans un dictionnaire dont les clés correspondent aux noms des groupes indiqués dans la regex. Cela est réalisable avec la méthode groupdict() :

adresses_mail.groupdict()

Le dictionnaire obtenu est :

{'expediteur':'marwa.thlithi@invivoo.com','destinataire':'admin@invivoo.com'}

Prenons un autre cas d’utilisation des groupes nommés qui consiste à extraire la date et l’heure de la chaîne de caractères utilisée ci-dessus :

date_heure = re.search('^From.*? \S+@\S+ (?P<jour>[a-zA-Z]+)\s(?P<mois>[a-zA-Z]+)\s(?P<jour_mois>\d+)\s(?P<heure>\d+:\d+:\d+)\s(?P<annee>\d+)',chr)
date_heure.groupdict()

Voici le résultat :

{'jour': 'Sat',
 'mois': 'Jan',
 'jour_mois': '5',
 'heure': '09:14:16',
 'annee': '2020'}

Sans le nommage des groupes, la regex utilisée ne sera pas compréhensible et la récupération de chaque sous-chaîne ne sera pas aussi simple qu’avec les groupes nommés.

Compilation des expressions régulières

La création d’une expression régulière revient à la création d’un graphe appelé « automate fini », qui est considéré comme une machine à états. Donc, si vous êtes amenés à utiliser une expression régulière plusieurs fois, vous allez créer la même machine à états correspondante plusieurs fois. Cela possède un coût non négligeable. Afin de minimiser ce coût et de gagner en performances, les expressions régulières peuvent être compilées en objets motifs possédants des méthodes diverses telles que la recherche et la substitution. La fonction permettant de compiler une regex du module « re » est re.compile().

Compilons l’expression régulière de l’exemple d’extraction d’adresse mail de l’expéditeur :

import re
compiled_regex = re.compile('^From.*? (?P<expediteur>\S+@\S+)')
print(compiled_regex)

Voici l’objet retourné :

re.compile('^From.*? (?P<expediteur>\\S+@\\S+)')

Cette fonction fournit un champ optionnel qui permet de modifier le comportement des expressions régulières. Ces options de compilation sont accessibles dans le module « re » avec deux types de nom : un nom long et un nom court en une seule lettre. Parmi les options les plus utilisées, nous citons les options « IGNORECASE » et « VERBOSE » dont les noms courts correspondants sont « I » et « X », respectivement.

L’option « I » permet d’avoir une correspondance insensible à la casse, ce qui facilite l’écriture des regex pour certaines recherches. Par exemple, le motif [A-Z] correspond aussi aux lettres minuscules. Voici un exemple :

regex = re.compile('spam', re.I)
regex.search('Spam')

Son résultat est :

<_sre.SRE_Match object; span=(0, 4), match='Spam'>

L’utilisation de l’option « I » avec la regex « spam » dans cet exemple facilite la recherche et elle correspond ainsi à « Spam », « SPAM », « sPam »…

L’option « X » permet d’avoir des expressions régulières plus claires et lisibles en les écrivant en multilignes avec des commentaires. L’activation de cette option implique l’ignorance des blancs (les espaces) dans la regex, sauf lorsque le blanc se trouve dans une classe de caractères ou est précédé d’une barre oblique inversée.

Voici une version plus compréhensible de l’expression régulière que nous avons utilisée dans la section précédente pour l’extraction de l’adresse mail de l’expéditeur avec des commentaires que nous avons ajoutés grâce à l’option « X » :

regex = re.compile('''^From # commence par le mot From
                   .*?   # n'importe quel caractère plusieurs fois non vorace
			 \s    # espace
                   (     # commence l'extraction
                   \S+ # n'importe quel caractère sauf l'espace
                   @     
                   \S+
                   )''', re.X)

Nous pouvons spécifier plusieurs options de compilation en appliquant l’opérateur OR. Par exemple, « re.I | re.X » active à la fois les options « I » et « X ». Reprenons l’exemple de la partie précédente qui consiste à extraire la date et l’heure et écrivons-le maintenant de façon plus lisible avec ces options :

regexp = re.compile('''^From    # commence par le mot From
                    .*?       # n'importe quel caractère plusieurs fois
                              # non vorace
                    \s        # espace
                    \S+@\S+\s # le caractère @ précédé et suivi par n'importe
                              # quel caractère sauf l'espace
                    (?P<jour>[a-z]+) # au moins une lettre de la classe 
                                     # [a-zA-Z] en utilisant l'option 
						 # Ignorecase
                    \s
                    (?P<mois>[a-z]+)\s 
                    (?P<jour_mois>\d+)\s
                    (?P<heure>\d+:\d+:\d+)\s 
                    (?P<annee>\d+)''', re.I|re.X)

date_heure = regexp.search(chr)
date_heure.groupdict()

Nous avons pu commenter notre expression régulière complexe en utilisant l’option « X », ce qui la rend plus lisible. Nous l’avons aussi optimisée en spécifiant pour les groupes jour et mois, qui contiennent des lettres minuscules et majuscules, uniquement la classe [a-z] grâce à l’option « I ».

Caractères et séquences d’échappement

Si vous souhaitez qu’un caractère spécial d’expression régulière fonctionne normalement, il faut ajouter comme préfixe la barre oblique inversée (« \ ») avant le caractère ou bien le mettre entre deux crochets. Voici un exemple d’utilisation :

import re

chaine = 'We just received $10.00 for cookies'
y = re.findall('\$[0-9.]+', chaine)					
print(y)

Dans le code suivant, nous avons utilisé cette regexp :

Nous pouvons aussi utiliser cette regexp :

Le résultat est :

['$10.00']

À ce caractère d’échappement s’ajoutent la plupart des séquences d’échappement standards utilisées pour les chaînes littérales telles que « \u », « \U » et « \N ». La séquence « \N » a été ajoutée dans la version 3.8 de Python et elle s’utilise de la façon suivante : « \N{nom}» avec nom est le nom du caractère Unicode. L’avantage de cette séquence d’échappement est d’avoir une version de regex encore plus claire et compréhensible. Prenons un exemple d’utilisation qui consiste à extraire le nom d’une marque déposée. En voici une première proposition du code avec une regex utilisant la séquence « \u » :

import re

produit = 'Nutri-Bio® céréales Blé & Avoine'
marque_deposee_regex = re.compile('(?P<marque>\S.+)\u00AE\s')
nom_marque = marque_deposee_regex.search(produit).group('marque')
print(nom_marque)

Dans cette version d’expression régulière, nous avons utilisé le code du caractère Unicode marque déposée en mettant cette séquence « \u00AE ». Cela nous permet d’avoir le résultat attendu qui est le nom de la marque :

Nutri-Bio

Néanmoins, pour bien comprendre cette regex, il faut revenir à la table de caractères Unicodes pour déchiffrer le symbole correspondant au code que nous avons utilisé. Afin de rendre cette regex plus compréhensible et claire, nous pouvons utiliser la séquence d’échappement « \N » suivie du nom du caractère Unicode qui est « registered sign » et qui est plus significatif que le code « \u00AE ». Et voici une version plus cool de notre regexp :

marque_deposee_regex = re.compile('(?P<marque>\S.+)\N{registered sign}\s')

Barre oblique inversée

Comme nous venons de le voir, la barre oblique inversée « \ » est un caractère d’échappement utilisé pour intégrer des caractères spéciaux sans que leur signification spéciale ne soit invoquée. Mais dans le cas où la barre oblique inversée fait partie de la phrase telle que la phrase « C:\Users\Default », la regex devra contenir « \U » et elle doit échapper le caractère « » pour qu’il ne soit pas considéré comme un caractère spécial. Afin que ce caractère soit échappé, l’expression régulière devra être alors « ‘\\U’  », mais pour échapper encore le caractère « ‘\’  » dans un littéral chaîne de caractères Python, on devra écrire la regex « ‘\\\\U’ ».

Mais dans une regex complexe et/ou contenant plusieurs barres obliques inversées, cette syntaxe devient rapidement illisible et difficile à comprendre. La solution consiste à utiliser des r-strings. En effet, l’utilisation du modificateur « r » comme préfixe à la chaîne de la regex implique que Python ne va tout simplement pas parcourir la chaîne à la recherche de caractères spéciaux et il va l’utiliser littéralement. Donc, la regex de la phrase exemple sera « r’\\U’ ».

Mise en pratique

Pour finir, abordons deux exemples d’utilisation: le premier affiche les différentes adresses URL d’un fichier et le deuxième extrait toutes les versions de release de Python ainsi que les informations correspondantes. Le fichier que nous utilisons pour ces deux exemples est LICENSE_PYTHON.txt. Voici le code du premier exemple :

import re
regex_url = re.compile('http[s]?://www.[a-z./]*', re.I)

with open('C:\\Users\\Tools\\Anaconda3\\LICENSE_PYTHON.txt', 'r') as file:
    print('URLs :\n')
    for line in file:
        url_info = regex_url.search(line)
        if url_info:
            print(url_info.group(0))

Dans cet exemple, nous avons compilé notre regex, car elle est utilisée plusieurs fois dans la boucle for et nous avons utilisé l’option de compilation « I » afin de rendre la correspondance insensible à la casse puisque l’URL peut contenir des lettres en minuscules et en majuscules. Nous avons utilisé la notion de groupe en faisant appel à la fonction group() avec l’indice 0 et donc à la totalité de la sous-chaîne correspondante à la regexp, qui est l’adresse URL. Et voici le résultat :

URLs :

http://www.cwi.nl

http://www.cnri.reston.va.us

https://www.python.org/psf/

http://www.opensource.org

http://www.pythonlabs.com/logos.html

Dans le deuxième exemple, nous cherchons à afficher toutes les releases de Python à partir de la version 1.6 et pour chaque version, nous affichons l’année de sa sortie et la version de laquelle elle dérive. Voici une proposition de code :

import re
regex_release = re.compile(r'''^\s{4,} # commence par au moins 4 espaces 
                            (?P<Release>[0-9.]+(\s?\w*){2}) # le numéro 
							# de la release
                            \s{3,} # au moins 3 espaces consécutifs
                            (?P<Derived_from>[0-9.]+)\s+ # la version 
							# de laquelle dérive l'actuelle
                            (?P<Year>\d{4}[-]?\S*)  # année de la release
                            ''', re.X)

with open('C:\\Users\\Tools\\Anaconda3\\LICENSE_PYTHON.txt', 'r') as file:
    print(F'Releases de python à partir de la version 1.6 :\n')

    for line in file:
        release_info = regex_release.search(line)
        if release_info:
            print(release_info.groupdict())

Dans cet exemple, nous avons aussi commencé par compiler notre expression régulière et nous avons utilisé l’option « X » afin d’avoir une regexp plus lisible en l’écrivant en multilignes et plus compréhensible avec les commentaires que nous avons ajoutés. Nous avons nommé les groupes avec des noms significatifs ce qui rend cette regex complexe encore plus compréhensible et nous avons utilisé la fonction groupdict() afin d’obtenir directement en sortie un dictionnaire dont les clés sont les noms des groupes. D’où ce résultat :

Releases de python à partir de la version 1.6 :

{'Release': '1.6  ', 'Derived_from': '1.5.2', 'Year': '2000'}
{'Release': '2.0  ', 'Derived_from': '1.6', 'Year': '2000'}
{'Release': '1.6.1  ', 'Derived_from': '1.6', 'Year': '2001'}
{'Release': '2.1.2  ', 'Derived_from': '2.1.1', 'Year': '2002'}
{'Release': '2.1.3  ', 'Derived_from': '2.1.2', 'Year': '2002'}
{'Release': '2.2 and above', 'Derived_from': '2.1.1', 'Year': '2001-now'}

Conclusion

Nous avons vu que les regexp sont un moyen très puissant, énigmatique et amusant une fois que vous les comprenez. Les regex sont un langage en soi qui permet de faire, de manière très efficace, des recherches, des validations, des extractions de données, du filtrage… Dans cet article, nous avons parlé du module « re » de Python qui permet de manipuler les regexp et de ses fonctions principales qui sont :

  • Les fonctions de recherches et de validation : search() et match()
  • La fonction d’extraction : findall()
  • La fonction de sauvegarde et de compilation des regexp : compile()

Nous avons aussi parlé de la notion de regroupement qui permet d’avoir des regexp plus claires et compréhensibles grâce au nommage des groupes et d’extraire les données plus facilement en faisant appel aux fonctions magiques group(), groups() et groupdict().

A vous de jouer !

Vous pouvez découvrir notre expertise Python pour être accompagné par nos spécialistes.

Expertise Design & Code

$

Python, Java, C++, C# et Front-End

Découvrez nos formations

$

Parcourir le catalogue

Boostez votre carrière !

$

Nos offres pour Développeurs

Tous les mois recevez nos derniers articles !

Try X4B now !

Découvrez gratuitement XComponent for Business. Notre solution logicielle BizDevOps !

Écrit par Marwa Thlithi

0 Comments

Submit a Comment

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *