Python. Introduction aux expressions rationnelles pour les presque nuls

In [1]:
from termcolor import cprint
from dateutil.tz import gettz
import datetime as dt
import locale

locale.setlocale(locale.LC_ALL, ('fr_FR', 'UTF-8'))

tzi = gettz('Europe/Brussels')
cprint('{} {}'.format('danielhagnoul', dt.datetime.now(tz=tzi).isoformat()), 'blue')
danielhagnoul 2020-05-01T11:22:14.114351+02:00

En Python, la méthode compile de l'objet re offre un mode verbeux qui permet d'écrire autant de commentaires que l'on en a besoin.

Voici le squelette générant un objet motif :

In [2]:
import re

obj_motif = re.compile(r'''
# code 
# commentaire
''', re.VERBOSE | re.UNICODE)

Pour vos débuts, vous pouvez utiliser ce squelette de code tel quel, plus tard vous lirez avec attention les autres possibilités dans les documents en référence ci-dessous.

Capture de nombre formé de 1 ou plusieurs chiffres.

In [3]:
obj_motif = re.compile(r'''
(\d+)
# capture d'un groupe : ()
# groupe contenant un nombre constitué de chiffres [0-9] : \d
# nombre formé de 1 ou plusieurs chiffres : +
''', re.VERBOSE | re.UNICODE)

Pour nos essais de capture, nous utiliserons exclusivement la méthode finditer. Lorsque vous serez devenu un expert, vous découvrirez d'autres méthodes en lisant avec attention les documents en référence ci-dessous.

In [4]:
texte = '12 batteurs battant, 11 cornemuseurs, 10 seigneurs sautant, 126lapins'

iterator = obj_motif.finditer(texte)

for match in iterator:
    print("group = {}, groups = {}, span = {}, start = {}, end = {}".format(
        match.group(), match.groups(), match.span(), match.start(), match.end()))
group = 12, groups = ('12',), span = (0, 2), start = 0, end = 2
group = 11, groups = ('11',), span = (21, 23), start = 21, end = 23
group = 10, groups = ('10',), span = (38, 40), start = 38, end = 40
group = 126, groups = ('126',), span = (60, 63), start = 60, end = 63

Capture de nombre formé de 1 ou plusieurs chiffres et suivi d'un espace.

In [5]:
obj_motif = re.compile(r'''
(\d+)\s
# capture d'un groupe : ()
# groupe contenant un nombre constitué de chiffres [0-9] : \d
# nombre formé de 1 ou plusieurs chiffres : +
# groupe suivi par un espace : \s
''', re.VERBOSE | re.UNICODE)
In [6]:
iterator = obj_motif.finditer(texte)

# 12 batteurs battant, 11 cornemuseurs, 10 seigneurs sautant, 126lapins

for match in iterator:
    print("group = {}, groups = {}, span = {}, start = {}, end = {}".format(
        match.group(), match.groups(), match.span(), match.start(), match.end()))
group = 12 , groups = ('12',), span = (0, 3), start = 0, end = 3
group = 11 , groups = ('11',), span = (21, 24), start = 21, end = 24
group = 10 , groups = ('10',), span = (38, 41), start = 38, end = 41

group() donne toujours le nombre inclus dans la capture (\d+) mais span() tient compte de l'espace

Capture de caractères ou de nombre.

In [7]:
obj_motif = re.compile(r'''
(\w+|\d{2})
# le groupe de capture sélectionne 
# un ou plusieurs caractères \w+
# ou |
# un nombre constitué de deux chiffres \d{2}
''', re.VERBOSE | re.UNICODE)
In [8]:
iterator = obj_motif.finditer(texte)

# 12 batteurs battant, 11 cornemuseurs, 10 seigneurs sautant, 126lapins

for match in iterator:
    print("group = {}, groups = {}, span = {}, start = {}, end = {}".format(
        match.group(), match.groups(), match.span(), match.start(), match.end()))
group = 12, groups = ('12',), span = (0, 2), start = 0, end = 2
group = batteurs, groups = ('batteurs',), span = (3, 11), start = 3, end = 11
group = battant, groups = ('battant',), span = (12, 19), start = 12, end = 19
group = 11, groups = ('11',), span = (21, 23), start = 21, end = 23
group = cornemuseurs, groups = ('cornemuseurs',), span = (24, 36), start = 24, end = 36
group = 10, groups = ('10',), span = (38, 40), start = 38, end = 40
group = seigneurs, groups = ('seigneurs',), span = (41, 50), start = 41, end = 50
group = sautant, groups = ('sautant',), span = (51, 58), start = 51, end = 58
group = 126lapins, groups = ('126lapins',), span = (60, 69), start = 60, end = 69

"126lapins" est capturé par \w+

Capture de caractères ou de nombre suivi d'un espace

In [9]:
obj_motif = re.compile(r'''
((\w+)\s|(\d{2})\s)
# capture contenant
# une capture d'un ou plusieurs caractères suivis par un espace (\w+)\s
# ou |
# une capture d'un nombre composé de deux chiffres et suivi par un espace (\d+)\s
''', re.VERBOSE | re.UNICODE)
In [10]:
iterator = obj_motif.finditer(texte)

# 12 batteurs battant, 11 cornemuseurs, 10 seigneurs sautant, 126lapins

for match in iterator:
    print("group = {}, groups = {}, span = {}, start = {}, end = {}".format(
        match.group(), match.groups(), match.span(), match.start(), match.end()))
group = 12 , groups = ('12 ', '12', None), span = (0, 3), start = 0, end = 3
group = batteurs , groups = ('batteurs ', 'batteurs', None), span = (3, 12), start = 3, end = 12
group = 11 , groups = ('11 ', '11', None), span = (21, 24), start = 21, end = 24
group = 10 , groups = ('10 ', '10', None), span = (38, 41), start = 38, end = 41
group = seigneurs , groups = ('seigneurs ', 'seigneurs', None), span = (41, 51), start = 41, end = 51

groups() contient trois valeurs, un nombre ou des caractères suivis de un espace, un nombre ou des caractères sans espace, None pour le groupe n'ayant rien capturé.

Capture d'un numéro de téléphone

Nous allons maintenant chercher uniquement des chiffres, mais dans un numéro de téléphone ce qui complique singulièrement la tâche.

Wikipédia nous donne la structure des numéros de téléphone en Belgique

  • Format national 9 chiffres : 0ZZ CC CC CC ou 0Z CCC CC CC
  • Format international : +32 ZZ CC CC CC ou +32 Z CCC CC CC
  • Le premier chiffre dans le format national est toujours le zéro.
  • Il faut toujours composer l'indicatif de zone (Z ou ZZ).
  • L'indicatif de zone fait le plus souvent 2 chiffres, excepté dans les grandes agglomérations où l'on est passé à 1 chiffre. (Bruxelles : 2, Anvers : 3, Liège : 4, Gand : 9).
  • Les 070 et 078 ne sont pas utilisés pour des zones, mais pour des numéros spéciaux à bas prix (078 prix d'un appel local, 070 = 0,30 €/min), seul le 071 concerne bien une zone géographique.
  • GSM (national) 10 chiffres : de 045C CC CC CC à 049C CC CC CC
  • GSM (international) : de +32 45C CC CC CC à +32 49C CC CC CC
  • Numéro gratuit : 0800
  • Numéro payant a minimum 1 euro/min : 0900

Nota bene : lorsque vous construirez une expression rationnelle n'essayez pas de prendre en compte tous les errements que l'utilisateur pourrait commettre en tapant son texte. Je vous conseille d'être très directif. Vous devez obliger l'utilisateur à respecter des modèles précis, modèles que vous devez lui fournir en exemple.

In [11]:
obj_motif = re.compile(r'''
^ # la capture doit commencer au début du texte
(00\s{1}32\s{1}|\+32\s{1}|0)
# capture comprenant
# 00 32 un espace
# ou +32 un espace
# ou 0 sans espace
((\d{1})\s{1}(\d{3})|(\d{2})\s{1}(\d{2})|(\d{3}\s{1}\d{2}))
# capture comprenant
# un chiffre un espace trois chiffres
# ou 2 chiffres un espace deux chiffres
# ou 3 chiffres un espace deux chiffres
\s{1} # un espace
(\d{2}) # on capture deux chiffres
\s{1} # un espace
(\d{2}) # on capture deux chiffres
$ # la capture doit se terminer à la fin du texte
''', re.VERBOSE | re.UNICODE)

textes = [
    '+32 4 125 56 78',
    '+32 491 12 13 14',
    '013 08 59 13',
    '0451 12 13 14',
    '00 32 451 12 13 14',
    '0800 01 02 03',
    '0900 03 04 05',
    '070 99 98 97',
    '078 11 44 99',
]

for texte in textes:
    iterator = obj_motif.finditer(texte)

    for match in iterator:
        print("group = {}, groups = {}, span = {}, start = {}, end = {}".format(
            match.group(), match.groups(), match.span(), match.start(), match.end()))
group = +32 4 125 56 78, groups = ('+32 ', '4 125', '4', '125', None, None, None, '56', '78'), span = (0, 15), start = 0, end = 15
group = +32 491 12 13 14, groups = ('+32 ', '491 12', None, None, None, None, '491 12', '13', '14'), span = (0, 16), start = 0, end = 16
group = 013 08 59 13, groups = ('0', '13 08', None, None, '13', '08', None, '59', '13'), span = (0, 12), start = 0, end = 12
group = 0451 12 13 14, groups = ('0', '451 12', None, None, None, None, '451 12', '13', '14'), span = (0, 13), start = 0, end = 13
group = 00 32 451 12 13 14, groups = ('00 32 ', '451 12', None, None, None, None, '451 12', '13', '14'), span = (0, 18), start = 0, end = 18
group = 0800 01 02 03, groups = ('0', '800 01', None, None, None, None, '800 01', '02', '03'), span = (0, 13), start = 0, end = 13
group = 0900 03 04 05, groups = ('0', '900 03', None, None, None, None, '900 03', '04', '05'), span = (0, 13), start = 0, end = 13
group = 070 99 98 97, groups = ('0', '70 99', None, None, '70', '99', None, '98', '97'), span = (0, 12), start = 0, end = 12
group = 078 11 44 99, groups = ('0', '78 11', None, None, '78', '11', None, '44', '99'), span = (0, 12), start = 0, end = 12

Construire une liste alphabétique avec les mots contenu dans un texte

In [12]:
from collections import Counter

""" Nota bene
import locale

locale.setlocale(locale.LC_ALL, ('fr_FR', 'UTF-8'))

sont déclarés au début de ce documen.
"""

texte = '''
C'est l'évadé, du Névada.
Qui s'évada dans, la vallée
Dans la vallée, du Névada ?
Qu'il dévala, pour s'évader
Sur un vilain vélo volé !
Qu'il a volé, dans une villa
Et le valet, qui fut volé.
Vit l'évadé, qui s'envola
'''

obj_motif = re.compile(r'''
(\w+)
''', re.VERBOSE | re.UNICODE)

# Capture des mots, le texte étant mit en minuscules au préalable
iterator = obj_motif.finditer(texte.lower())

# Exploiter l'iterator en construisant une liste triée avec les groupes capturés
lst = sorted([match.group() for match in iterator if len(
    match.group()) > 2], key=locale.strxfrm)

print('Nombre de mots = {}'.format(len(lst)))

# Construire un dictionnaire mot: fréquence
cnt = Counter(lst)

# Imprimer le contenu du dictionnaire
for key, value in cnt.items():
    print('Mot : {}, fréquence : {}'.format(key, value))
Nombre de mots = 29
Mot : dans, fréquence : 3
Mot : dévala, fréquence : 1
Mot : envola, fréquence : 1
Mot : est, fréquence : 1
Mot : évada, fréquence : 1
Mot : évadé, fréquence : 2
Mot : évader, fréquence : 1
Mot : fut, fréquence : 1
Mot : névada, fréquence : 2
Mot : pour, fréquence : 1
Mot : qui, fréquence : 3
Mot : sur, fréquence : 1
Mot : une, fréquence : 1
Mot : valet, fréquence : 1
Mot : vallée, fréquence : 2
Mot : vélo, fréquence : 1
Mot : vilain, fréquence : 1
Mot : villa, fréquence : 1
Mot : vit, fréquence : 1
Mot : volé, fréquence : 3

Capture les mots du texte contenant au moins un caractère accentué

In [13]:
obj_motif = re.compile(r'''
# (?:(A)\w+) signifie capture le reste des caractères
# s'il commence par le contenu de A, lequel
# est capturé en premier.
(?:([a-z]{0,}[\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF]{1})\w{0,})
# A contient 
# la capture de zéro ou plusieurs lettres de a à z
# la capture d'un caractère accentué FR
''', re.VERBOSE | re.UNICODE)

iterator = obj_motif.finditer(texte.lower())

lst = sorted([match.group() for match in iterator if len(
    match.group()) > 2], key=locale.strxfrm)

print('Nombre de mots = {}'.format(len(lst)))

cnt = Counter(lst)

for key, value in cnt.items():
    print('Mot : {}, fréquence : {}'.format(key, value))
Nombre de mots = 13
Mot : dévala, fréquence : 1
Mot : évada, fréquence : 1
Mot : évadé, fréquence : 2
Mot : évader, fréquence : 1
Mot : névada, fréquence : 2
Mot : vallée, fréquence : 2
Mot : vélo, fréquence : 1
Mot : volé, fréquence : 3

Capture les caractères du texte qui sont suivis par une virgule

In [14]:
obj_motif = re.compile(r'''
(\w+(?=,))
# capture uniquement les caractères suivis par une virgule
''', re.VERBOSE | re.UNICODE)

iterator = obj_motif.finditer(texte.lower())

lst = sorted([match.group() for match in iterator if len(
    match.group()) > 2], key=locale.strxfrm)

print('Nombre de mots = {}'.format(len(lst)))

cnt = Counter(lst)

for key, value in cnt.items():
    print('Mot : {}, fréquence : {}'.format(key, value))
Nombre de mots = 7
Mot : dans, fréquence : 1
Mot : dévala, fréquence : 1
Mot : évadé, fréquence : 2
Mot : valet, fréquence : 1
Mot : vallée, fréquence : 1
Mot : volé, fréquence : 1

Cette introduction se termine alors que nous avons seulement découvert quelques-unes des possibilités des expressions rationnelles en Python. Je pense que si vous vous appropriez son contenu en faisant quelques exercices vous aurez beaucoup moins de mal à comprendre le contenu des liens ci-dessous.

Références à lire absolument pour connaître toutes les facettes du sujet

In [15]:
!jupyter nbconvert --to html exp_reg.ipynb