2A.eco - Les expressions régulières : à quoi ça sert ? (correction)

Links: notebook, html, PDF, python, slides, GitHub

Chercher un mot dans un texte est une tâche facile, c’est l’objectif de la méthode find attachée aux chaînes de caractères, elle suffit encore lorsqu’on cherche un mot au pluriel ou au singulier mais il faut l’appeler au moins deux fois pour chercher ces deux formes. Pour des expressions plus compliquées, il est conseillé d’utiliser les expressions régulières. C’est une fonctionnalité qu’on retrouve dans beaucoup de langages. C’est une forme de grammaire qui permet de rechercher des expressions.

from jyquickhelper import add_notebook_menu
add_notebook_menu()

Lorsqu’on remplit un formulaire, on voit souvent le format "MM/JJ/AAAA" qui précise sous quelle forme on s’attend à ce qu’une date soit écrite. Les expressions régulières permettent de définir également ce format et de chercher dans un texte toutes les chaînes de caractères qui sont conformes à ce format.

La liste qui suit contient des dates de naissance. On cherche à obtenir toutes les dates de cet exemple sachant que les jours ou les mois contiennent un ou deux chiffres, les années deux ou quatre.

s = """date 0 : 14/9/2000
date 1 : 20/04/1971     date 2 : 14/09/1913     date 3 : 2/3/1978
date 4 : 1/7/1986     date 5 : 7/3/47     date 6 : 15/10/1914
date 7 : 08/03/1941     date 8 : 8/1/1980     date 9 : 30/6/1976"""

Le premier chiffre du jour est soit 0, 1, 2, ou 3 ; ceci se traduit par [0-3]. Le second chiffre est compris entre 0 et 9, soit [0-9]. Le format des jours est traduit par [0-3][0-9]. Mais le premier jour est facultatif, ce qu’on précise avec le symbole ? : [0-3]?[0-9]. Les mois suivent le même principe : [0-1]?[0-9]. Pour les années, ce sont les deux premiers chiffres qui sont facultatifs, le symbole ? s’appliquent sur les deux premiers chiffres, ce qu’on précise avec des parenthèses : ([0-2][0-9])?[0-9][0-9]. Le format final d’une date devient :

[0-3]?[0-9]/[0-1]?[0-9]/([0-2][0-9])?[0-9][0-9]

Le module re gère les expressions régulières, celui-ci traite différemment les parties de l’expression régulière qui sont entre parenthèses de celles qui ne le sont pas : c’est un moyen de dire au module re que nous nous intéressons à telle partie de l’expression qui est signalée entre parenthèses. Comme la partie qui nous intéresse - une date - concerne l’intégralité de l’expression régulière, il faut insérer celle-ci entre parenthèses.

La première étape consiste à construire l’expression régulière, la seconde à rechercher toutes les fois qu’un morceau de la chaîne s définie plus haut correspond à l’expression régulière.

import re
# première étape : construction
expression = re.compile("([0-3]?[0-9]/[0-1]?[0-9]/([0-2][0-9])?[0-9][0-9])")
# seconde étape : recherche
res = expression.findall(s)
print(res)
[('14/9/2000', '20'), ('20/04/1971', '19'), ('14/09/1913', '19'), ('2/3/1978', '19'), ('1/7/1986', '19'), ('7/3/47', ''), ('15/10/1914', '19'), ('08/03/1941', '19'), ('8/1/1980', '19'), ('30/6/1976', '19')]

Le résultat une liste de couples dont chaque élément correspond aux parties comprises entre parenthèses qu’on appelle des groupes. Lorsque les expressions régulières sont utilisées, on doit d’abord se demander comment définir ce qu’on cherche puis quelles fonctions utiliser pour obtenir les résultats de cette recherche. Les deux paragraphes qui suivent y répondent.

Syntaxe

La syntaxe des expressions régulières est décrite sur le site officiel de python. La page Regular Expression Syntax décrit comment se servir des expressions régulières, les deux pages sont en anglais. Comme toute grammaire, celle des expressions régulières est susceptible d’évoluer au fur et à mesure des versions du langage python.

Les ensembles de caractères

Lors d’une recherche, on s’intéresse aux caractères et souvent aux classes de caractères : on cherche un chiffre, une lettre, un caractère dans un ensemble précis ou un caractère qui n’appartient pas à un ensemble précis. Certains ensembles sont prédéfinis, d’autres doivent être définis à l’aide de crochets.

Pour définir un ensemble de caractères, il faut écrire cet ensemble entre crochets : [0123456789] désigne un chiffre. Comme c’est une séquence de caractères consécutifs, on peut résumer cette écriture en [0-9]. Pour inclure les symboles -, +, il suffit d’écrire : [-0-9+]. Il faut penser à mettre le symbole - au début pour éviter qu’il ne désigne une séquence.

Le caractère ^ inséré au début du groupe signifie que le caractère cherché ne doit pas être un de ceux qui suivent. Le tableau suivant décrit les ensembles prédéfinis et leur équivalent en terme d’ensemble de caractères :

  • . désigne tout caractère non spécial quel qu’il soit.
  • \d désigne tout chiffre, est équivalent à [0-9].
  • \D désigne tout caractère différent d’un chiffre, est équivalent à [^0-9].
  • \s désigne tout espace ou caractère approché, est équivalent à [\; \t\n\r\f\v]. Ces caractères sont spéciaux, les plus utilisés sont \t qui est une tabulation, \n qui est une fin de ligne et qui \r qui est un retour à la ligne.
  • \S désigne tout caractère différent d’un espace, est équivalent à [^ \t\n\r\f\v].
  • \w désigne tout lettre ou chiffre, est équivalent à [a-zA-Z0-9_].
  • \W désigne tout caractère différent d’une lettre ou d’un chiffre, est équivalent à [^a-zA-Z0-9_].
  • ^ désigne le début d’un mot sauf s’il est placé entre crochets.
  • $ désigne la fin d’un mot sauf s’il est placé entre crochets.

A l’instar des chaînes de caractères, comme le caractère \ est un caractère spécial, il faut le doubler : [\\].

Le caractère \ est déjà un caractère spécial pour les chaînes de caractères en python, il faut donc le quadrupler pour l’insérer dans un expression régulière. L’expression suivante filtre toutes les images dont l’extension est png et qui sont enregistrées dans un répertoire image.

import re
s = "something\\support\\vba\\image/vbatd1_4.png"
print(re.compile("[\\\\/]image[\\\\/].*[.]png").search(s))  # résultat positif
print(re.compile("[\\\\/]image[\\\\/].*[.]png").search(s))  # même résultat
<re.Match object; span=(21, 40), match='\image/vbatd1_4.png'>
<re.Match object; span=(21, 40), match='\image/vbatd1_4.png'>

Les multiplicateurs

Les multiplicateurs permettent de définir des expressions régulières comme : un mot entre six et huit lettres qu’on écrira [\w]{6,8}. Le tableau suivant donne la liste des multiplicateurs principaux :

  • * présence de l’ensemble de caractères qui précède entre 0 fois et l’infini
  • + présence de l’ensemble de caractères qui précède entre 1 fois et l’infini
  • ? présence de l’ensemble de caractères qui précède entre 0 et 1 fois
  • {m,n} présence de l’ensemble de caractères qui précède entre m et n fois, si m=n, cette expression peut être résumée par {n}.
  • (?!(...)) absence du groupe désigné par les points de suspensions.

L’algorithme des expressions régulières essaye toujours de faire correspondre le plus grand morceau à l’expression régulière.

"<h1>mot</h1>"
'<h1>mot</h1>'

<.*> correspond avec <h1>, </h1> ou encore <h1>mot</h1>.

Par conséquent, l’expression régulière correspond à trois morceaux. Par défaut, il prendra le plus grand. Pour choisir les plus petits, il faudra écrire les multiplicateurs comme ceci : *?, +?

import re
s = "<h1>mot</h1>"
print(re.compile("(<.*>)").match(s).groups())   # ('<h1>mot</h1>',)
print(re.compile("(<.*?>)").match(s).groups())  # ('<h1>',)
print(re.compile("(<.+?>)").match(s).groups())  # ('<h1>',)
('<h1>mot</h1>',)
('<h1>',)
('<h1>',)

Exercice 1

Recherchez les dates présentes dans la phrase suivante

texte = """Je suis né le 28/12/1903 et je suis mort le 08/02/1957. Ma seconde femme est morte le 10/11/1963.
J'ai écrit un livre intitulé 'Comprendre les fractions : les exemples en page 12/46/83' """
import re
expression = re.compile("[0-9]{2}/[0-9]{2}/[0-9]{4}")
cherche = expression.findall(texte)
print(cherche)
['28/12/1903', '08/02/1957', '10/11/1963']

Puis dans celle-ci :

texte = """Je suis né le 28/12/1903 et je suis mort le 08/02/1957. Je me suis marié le 8/5/45.
J'ai écrit un livre intitulé 'Comprendre les fractions : les exemples en page 12/46/83' """
expression = re.compile("[0-3]?[0-9]/[0-1]?[0-9]/[0-1]?[0-9]?[0-9]{2}")
cherche = expression.findall(texte)
print(cherche)
['28/12/1903', '08/02/1957', '8/5/45']