13. Les expressions régulières▲
Les expressions régulières(51) fournissent une notation générale très puissante permettant de décrire abstraitement des éléments textuels. Il s'agit d'un vaste domaine dont nous ne proposons qu'une introduction.
13-1. Introduction▲
Dès les débuts de l'informatique, les concepteurs des systèmes d'exploitation eurent l'idée d'utiliser des métacaractères permettant de représenter des modèles généraux. Par exemple, dans un shell Linux ou dans une fenêtre de commande Windows, le symbole *(52) remplace une série de lettres, ainsi *.png indique tout nom de fichier contenant l'extension png. Python offre en standard les modules glob et fnmatch pour utiliser la notation des métacaractères.
Depuis ces temps historiques, les informaticiens(53) ont voulu généraliser ces notations. On distingue classiquement trois stades dans l'évolution des expressions régulières :
- les expressions régulières de base (BRE, Basic Regular Expressions) ;
- les expressions régulières étendues (ERE, Extended Regular Expressions) ;
- les expressions régulières avancées (ARE, Advanced Regular Expressions).
Trois stades auxquels il convient d'ajouter le support de l'encodage Unicode.
Python supporte toutes ces évolutions.
13-2. Les expressions régulières▲
Une expression régulière(54) se lit (et se construit) de gauche à droite. Elle constitue ce qu'on appelle traditionnellement un motif de recherche(55).
13-2-1. Les expressions régulières de base▲
Elles utilisent six symboles qui, dans le contexte des expressions régulières, acquièrent les significations suivantes :
- le point . représente une seule instance de n'importe quel caractère sauf le caractère de fin de ligne. Ainsi l'expression t.c représente toutes les combinaisons de trois lettres commençant par « t » et finissant par « c », comme tic, tac, tqc ou t9c, alors que b.l.. pourrait représenter bulle, balai ou bêler.
- la paire de crochets [ ] représente une occurrence quelconque des caractères qu'elle contient. Par exemple [aeiouy] représente une voyelle, et Duran[dt] désigne Durand ou Durant. Entre les crochets, on peut noter un intervalle en utilisant le tiret. Ainsi, [0-9] représente les chiffres de 0 à 9, et [a-zA-Z] représente une lettre minuscule ou majuscule. On peut de plus utiliser l'accent circonflexe en première position dans les crochets pour indiquer le contraire de … Par exemple [^a-z] représente autre chose qu'une lettre minuscule, et [^'"] n'est ni une apostrophe ni un guillemet.
- l'astérisque * est un quantificateur, il signifie aucune ou plusieurs occurrences du caractère ou de l'élément qui le précède immédiatement.
L'expression ab* signifie la lettre a suivie de zéro ou plusieurs lettres b, par exemple ab, a ou abbb et [A-Z]* correspond à zéro ou plusieurs lettres majuscules. - l'accent circonflexe ^ est une ancre. Il indique que l'expression qui le suit se trouve en début de ligne.
L'expression ^Depuis indique que l'on recherche les lignes commençant par le mot Depuis. - le symbole dollar $ est aussi une ancre. Il indique que l'expression qui le précède se trouve en fin de ligne. L'expression suivante :$ indique que l'on recherche les lignes se terminant par « suivante : ».
L'expression ^Les expressions régulières$ extrait les lignes ne contenant que la chaîne Les expressions régulières, alors que ^$ extrait les lignes vides. - la contre-oblique \ permet d'échapper à la signification des métacaractères. Ainsi \. désigne un véritable point, \* un astérisque, \^ un accent circonflexe, \$ un dollar et \\ une contre-oblique.
13-2-2. Les expressions régulières étendues▲
Elles ajoutent cinq symboles qui ont les significations suivantes :
- la paire de parenthèses ( ) est utilisée à la fois pour former des sous-motifs et pour délimiter des sous-expressions, ce qui permettra d'extraire des parties d'une chaîne de caractères. L'expression (to)* désignera to, tototo, etc. ;
- le signe plus + est un quantificateur comme *, mais il signifie une ou plusieurs occurrences du caractère ou de l'élément qui le précède immédiatement. L'expression ab+ signifie la lettre a suivie d'une ou plusieurs lettres b ;
- le point d'interrogation ? troisième quantificateur, il signifie zéro ou une instance de l'expression qui le précède. Par exemple écran(s)? désigne écran ou écrans ;
- la paire d'accolades { } précise le nombre d'occurrences permises pour le motif qui le précède. Par exemple [0-9]{2,5} attend entre deux et cinq nombres décimaux. Les variantes suivantes sont disponibles : [0-9]{2,} signifie au minimum deux occurrences d'entiers décimaux et [0-9]{2} deux occurrences exactement ;
- la barre verticale | représente des choix multiples dans un sous-motif. L'expression Duran[dt] peut aussi s'écrire (Durand|Durant). On pourrait utiliser l'expression (lu|ma|me|je|ve|sa|di) dans l'écriture d'une date.
Dans de nombreux outils et langages (dont Python), la syntaxe étendue comprend aussi une série de séquences d'échappement :
- \ : symbole d'échappement ;
- \e : séquence de contrôle escape ;
- \f : saut de page ;
- \n : fin de ligne ;
- \r : retour-chariot ;
- \t : tabulation horizontale ;
- \v : tabulation verticale ;
- \d : classe des nombres entiers ;
- \s : classe des caractères d'espacement ;
- \w : classe des caractères alphanumériques ;
- \b : délimiteurs de début ou de fin de mot ;
- \D : négation de la classe \d ;
- \S : négation de la classe \s ;
- \W : négation de la classe \w ;
- \B : négation de la classe \b.
13-3. Les expressions régulières avec Python▲
Le module re permet d'utiliser les expressions régulières dans les scripts Python. Les scripts devront donc comporter la ligne :
import
re
13-3-1. Pythonismes▲
Le module re utilise la notation objet. Les motifs et les correspondances de recherche seront des objets de la classe re auxquels on pourra appliquer des méthodes.
13-3-1-a. Utilisation des raw strings▲
La syntaxe des motifs comprend souvent le caractère contre-oblique qui doit être lui-même échappé dans une chaîne de caractères, ce qui alourdit la notation. On peut éviter cet inconvénient en utilisant des « chaînes brutes ». Par exemple au lieu de :
"
\\
d
\\
d ?
\\
w+
\\
d{4}"
on écrira :
r"\d\d ? \w+ \d{4}"
13-3-1-b. Les options de compilation▲
Grâce à un jeu d'options de compilation, il est possible de piloter le comportement des expressions régulières. On utilise pour cela la syntaxe (?…) avec les drapeaux suivants :
- a : correspondance ASCII (Unicode par défaut) ;
- i : correspondance non sensible à la casse ;
- L : les correspondances utilisent la locale, c'est-à-dire les particularités du pays ;
- m : correspondance dans des chaînes multilignes ;
- s : modifie le comportement du métacaractère point qui représentera alors aussi le saut de ligne ;
- u : correspondance Unicode (par défaut) ;
- x : mode verbeux.
Voici un exemple de recherche non sensible à la casse :
13-3-1-c. Les motifs nominatifs▲
Python possède une syntaxe qui permet de nommer des parties de motif délimitées par des parenthèses, ce qu'on appelle un motif nominatif :
- syntaxe de création d'un motif nominatif : (?P<nom_du_motif>) ;
- syntaxe permettant de s'y référer : (?P=nom_du_motif) ;
- syntaxe à utiliser dans un motif de remplacement ou de substitution : \g<nom_du_motif>.
13-3-2. Exemples▲
On propose plusieurs exemples d'extraction de dates historiques telles que "14 juillet 1789"
.
13-3-2-a. Extraction simple▲
Cette chaîne peut être décrite par le motif \d\d? \w+ \d{4} que l'on peut expliciter ainsi : « un ou deux entiers décimaux suivi d'un blanc suivi d'une chaîne d'au moins un caractère suivi d'un blanc suivi de quatre entiers décimaux ».
Détaillons le script :
import
re
motif_date =
re.compile(
r"\d\d ? \w+ \d{4}"
)
corresp =
motif_date.search
(
"Bastille le 14 juillet 1789"
)
print
(
corresp.group
(
))
Après avoir importé le module re, la variable motif_date reçoit la forme compilée de l'expression régulière correspondant à une date historique. Puis on applique à ce motif compilé la méthode search() qui retourne la première position du motif dans la chaîne et l'affecte à la variable corresp. Enfin on affiche la correspondance complète (en ne donnant pas d'argument à group()).
Son exécution produit :
14 juillet 1789
13-3-2-b. Extraction des sous-groupes▲
On aurait pu affiner l'affichage du résultat en modifiant l'expression régulière de recherche de façon à pouvoir capturer les éléments du motif :
import
re
motif_date =
re.compile(
r"(\d\d ?) (\w+) (\d{4})"
)
corresp =
motif_date.search
(
"Bastille le 14 juillet 1789"
)
print
(
"corresp.group() :"
, corresp.group
(
))
print
(
"corresp.group(1) :"
, corresp.group
(
1
))
print
(
"corresp.group(2) :"
, corresp.group
(
2
))
print
(
"corresp.group(3) :"
, corresp.group
(
3
))
print
(
"corresp.group(1,3) :"
, corresp.group
(
1
,3
))
print
(
"corresp.groups() :"
, corresp.groups
(
))
Ce qui produit à l'exécution :
corresp.group() : 14 juillet 1789
corresp.group(1) : 14
corresp.group(2) : juillet
corresp.group(3) : 1789
corresp.group(1,3) : ('14', '1789')
corresp.groups() : ('14', 'juillet', '1789')
13-3-2-c. Extraction des sous-groupes nommés▲
Une autre possibilité est l'emploi de la méthode groupdict() qui renvoie une liste comportant le nom et la valeur des sous-groupes trouvés (ce qui nécessite de nommer les sous-groupes).
import
re
motif_date =
re.compile(
r"(?P<jour>\d\d ?) (?P<mois>\w+) (\d{4})"
)
corresp =
motif_date.search
(
"Bastille le 14 juillet 1789"
)
print
(
corresp.groupdict
(
))
print
(
corresp.group
(
'jour'
))
print
(
corresp.group
(
'mois'
))
Ce qui donne :
{'jour': '14', 'mois': 'juillet'}
14
juillet
13-3-2-d. Extraction d'une liste de sous-groupes▲
La méthode findall retourne une liste des occurrences trouvées. Si par exemple on désire extraire tous les nombres d'une chaîne, on peut écrire :
>>>
import
re
>>>
nbr =
re.compile(
r"\d+"
)
>>>
print
(
nbr.findall
(
"Bastille le 14 juillet 1789"
))
['14'
, '1789'
]
13-3-2-e. Scinder une chaîne▲
La méthode split() permet de scinder une chaîne à chaque occurrence du motif. Si on ajoute un paramètre numérique n (non nul), la chaîne est scindée en au plus n éléments :
>>>
import
re
>>>
nbr =
re.compile(
r"\d+"
)
>>>
>>>
print
(
"Une coupure à chaque occurrence :"
, nbr.split
(
"Bastille le 14 juillet 1789"
))
Une coupure à chaque occurrence : ['Bastille le '
, ' juillet '
, ''
]
>>>
print
(
"Une seule coupure :"
, nbr.split
(
"Bastille le 14 juillet 1789"
, 1
))
Une seule coupure : ['Bastille le '
, ' juillet 1789'
]
13-3-2-f. Substitution au sein d'une chaîne▲
La méthode sub() effectue des substitutions dans une chaîne. Le remplacement peut être une chaîne ou une fonction. Comme on le sait, en Python, les chaînes de caractères sont non modifiables et donc les substitutions produisent de nouvelles chaînes.
Exemples de remplacement d'une chaîne par une autre et des valeurs décimales en leur équivalent hexadécimal :
import
re
def
int2hexa
(
match) :
return
hex(
int(
match.group
(
)))
anniv =
re.compile(
r"1789"
)
print
(
"Premier anniversaire :"
, anniv.sub
(
"1790"
, "Bastille le 14 juillet 1789"
))
nbr =
re.compile(
r"\d+"
)
print
(
"En hexa :"
, nbr.sub
(
int2hexa, "Bastille le 14 juillet 1789"
))
Ce qui affiche :
Premier anniversaire : Bastille le 14 juillet
En hexa : Bastille le 0xe juillet 0x6fd
Les deux notations suivantes sont disponibles pour les substitutions :
- & : contient toute la chaîne recherchée par un motif ;
- \n : contient la sous-expression capturée par la ne paire de parenthèses du motif de recherche (kitxmlcodeinlinelatexdvp1 \le n \le 9finkitxmlcodeinlinelatexdvp).