Introduction au module Regex de Python
Par Benjamin Delmée le 27.10.2019 (mis à jour le 30.10.2019)
Pour rappel, le terme regex provient de la contraction du terme regular expression (expression régulière en français). Formellement, les expressions régulières permettent de décrire les grammaires régulières. Il existe plusieurs autres types de grammaires (algébriques, contextuelles et générales) et sont décrites dans la hiérarchie de Chomsky.
Cependant les grammaires régulières sont généralement suffisantes pour effectuer des tâches de traitement de texte simples telles que la validation automatique de saisie ou encore du data cleaning.
Par exemple, on peut vérifier si un numéro de téléphone est correctement formé en testant s'il correspond à la grammaire suivante : "5 paires de chiffres, éventuellement séparées par un espace".
Cette grammaire est décrite par l'expression régulière suivante :
((\d\d){4}|(\d\d[ ]){4})\d\d
En Python, cela donnerait :
import re
pattern = re.compile(r'((\d\d){4}|(\d\d[ ]){4})\d\d')
if (pattern.match('01 23 45 67 89')):
print('Numéro valide')
else:
print('Numéro invalide')
# output: Numéro valide
Cet article détaille le fonctionnement du module regex de la librairie standard de Python mais ne revient pas sur les fondamentaux des regex.
Compilation des regex
Avec le module regex de Python, on a le choix entre :
- utiliser les fonctions de matching au niveau du module
- utiliser les fonctions de matching de l'objet Pattern
Les deux méthodes sont équivalentes en matière de fonctionnalités (elles font toutes les deux appel au même code). La différence est essentiellement dans le design du code.
Lorsqu'on utilise l'objet Pattern
on procède en deux étapes. On compile d'abors la regex puis ensuite on effectue des opérations sur les chaînes :
import re
# compilation
p = re.compile(r'h.*') # retourne un objet Pattern
# opérations
p.match('hello')
p.find('world')
L'avantage de cette méthode est que l'objet Pattern
retourné par re.compile
peut être stocké ou passé en paramètre d'une fonction. Le design du code est plus souple. Cependant il est possible de condenser les deux étapes en utilisant les fonctions à disposition au niveau du module :
import re
# compilation et opération
re.match(r'h.*', 'hello')
# compilation et opérations
re.find(r'h.*', 'world')
Le module s'est chargé à notre place d'effectuer la compilation. On peut le voir, par exemple, en regardant le code de la fonction re.match
:
# $> cat lib/python3.7/re.py
def match(pattern, string, flags=0):
"""Try to apply the pattern at the start of the string, returning
a Match object, or None if no match was found."""
return _compile(pattern, flags).match(string) # <- compilation puis matching
Concernant les performances, dans la plupart des cas les deux méthodes sont équivalentes. En effet le module regex met en cache les derniers patterns compilés :
# $> cat lib/python3.7/re.py
_MAXCACHE = 512 # <- met en cache les 512 derniers patterns compilés
def _compile(pattern, flags):
# ...
try:
return _cache[type(pattern), pattern, flags] # <- mise en cache
except KeyError:
pass
# ...
Eviter l'épidémie des backslashes
The Backslash Plague est le terme utilisé pour désigner ceci :
import re
re.match('\\\\\\d+', '\\1234')
Une accumulation de backslashes qui rendent la lecture de la regex très difficile pour un humain. Cette "épidémie" de backslashes provient du fait qu'ils sont utilisés par Python et par le moteur de regex comme caractère d'échappement. Par conséquent si l'on veut matcher la chaîne \1234
, on doit échapper une première fois la chaîne pour l'interpréteur Python (backslases x2) puis une seconde fois pour le moteur de regex(backslases x2), soit un total de quatre backslashes.
Afin d'économiser un niveau d'échappement, on utilise les raw strings de Python, c'est-à-dire des strings non interprétées. Pour obtenir une raw string, on préfixe la chaîne avec la lettre r
:
import re
re.match(r'\\\d+', '\\1234')
Les fonctions de matching
Maintenant que nous savons comment compiler (ou non) les regex et comment éviter que les backslashes polluent la lecture, passons aux fonctions qui permettent d'appliquer ces regex sur des chaînes de caractères.
Voici la liste des fonctions disponibles dans le module regex :
Fonction | Description |
---|---|
match | Match a regular expression pattern to the beginning of a string. |
fullmatch | Match a regular expression pattern to all of a string. |
search | Search a string for the presence of a pattern. |
sub | Substitute occurrences of a pattern found in a string. |
subn | Same as sub, but also return the number of substitutions made. |
split | Split a string by the occurrences of a pattern. |
findall | Find all occurrences of a pattern in a string. |
finditer | Return an iterator yielding a Match object for each match. |
compile | Compile a pattern into a Pattern object. |
purge | Clear the regular expression cache. |
escape | Backslash all non-alphanumerics in a string. |
Certaines fonctions ont des comportements trés similaires (match
et search
par exemple), les sections ci-dessous abordent les grands types de fonctions et illustrent les nuances par des exemples.
match
vs fullmatch
vs search
vs findall
Le fonctionement de ces quatres fonctions est similaire :
match
évalue si le début de la chaine correspond au patternfullmatch
évalue si l'intégralité de la chaine correspond au patternsearch
évalue si une sous-chaine quelconque correspond au patternfindall
renvoie toutes les sous-chaines correspondant au pattern
import re
p1 = re.compile('[a-z]+')
p2 = re.compile('[0-9]+')
# match
print(p1.match('abcd 1234 efgh'))
>>> <re.Match object; span=(0, 4), match='abcd'>
print(p2.match('abcd 1234 efgh'))
>>> None
# fullmatch
print(p1.fullmatch('abcd 1234 efgh'))
>>> None
print(p2.fullmatch('abcd 1234 efgh'))
>>> None
# search
print(p1.search('abcd 1234 efgh'))
>>> <re.Match object; span=(0, 4), match='abcd'>
print(p2.search('abcd 1234 efgh'))
>>> <re.Match object; span=(5, 9), match='1234'>
# findall
print(p1.findall('abcd 1234 efgh'))
>>> ['abcd', 'efgh']
print(p2.findall('abcd 1234 efgh'))
>>> ['1234']
sub
et subn
sub
et subn
permettent de subtituer une chaîne aux sous-chaînes qui matchent le pattern :
sub
renvoie une nouvelle chaîne de caractères avec les substitutions effectuéessubn
renvoie en plus le nombre de substitutions effectuées
import re
p = re.compile('[0-9]')
# sub
print(p.sub('#', '10 apples and 20 oranges'))
>>> '## apples and ## oranges'
# subn
print(p.subn('#', '10 apples and 20 oranges'))
>>> ('## apples and ## oranges', 4)
split
La fonction re.split
est très similaire à la fonction str.split
, à la différence que le séparateur de re.split
est une regex :
import re
p = re.compile(':+')
# split
print(p.split('abcd:1234::efgh:::5678'))
>>> ['abcd', '1234', 'efgh', '5678']
Flags de compilation
Il peut être utile de modifier le comportement du moteur de regex. Par exemple, on veut parfois ne pas faire de distinction entre les lettres minuscules et majuscules. Alors plutôt que de réécrire une regex générique (au détriment de la lisibilité), on peut simplement l'indiquer au moteur de regex avec un flag.
Les flags sont optionnels et on peut en utiliser zéro, un ou plusieurs par regex. Pour utiliser plusieurs flags, il suffit de les combiner à l'aide de l'opérateur bit à bit «|
».
Voici la liste des flags de compilation disponibles dans le module regex :
Flag | Description |
---|---|
ASCII | For string patterns, make \w, \W, \b, \B, \d, \D match the corresponding ASCII character categories (rather than the whole Unicode categories, which is the default). For bytes patterns, this flag is the only available behaviour and needn't be specified. |
IGNORECASE | Perform case-insensitive matching. |
LOCALE | Make \w, \W, \b, \B, dependent on the current locale. |
MULTILINE | "^" matches the beginning of lines (after a newline) as well as the string. "$" matches the end of lines (before a newline) as well as the end of the string. |
DOTALL | "." matches any character at all, including the newline. |
VERBOSE | Ignore whitespace and comments for nicer looking RE's. |
UNICODE | For compatibility only. Ignored for string patterns (it is the default), and forbidden for bytes patterns. |
Quelques exemples d'utilisation :
IGNORECASE
Le flag IGNORECASE
permet de rendre les regex insensibles à la casse.
import re
p1 = re.compile('[a-z]+')
p2 = re.compile('[a-z]+', re.IGNORECASE)
# without IGNORECASE
print(p1.search('helloWORLD'))
>>> <re.Match object; span=(0, 5), match='hello'>
# with IGNORECASE
print(p2.search('helloWORLD'))
>>> <re.Match object; span=(0, 10), match='helloWORLD'>
VERBOSE
Le flag VERBOSE
permet d'ajouter des espaces et des commentaires dans les regex. Cela permet une amélioration de la lisibilité de la regex.
Exemple avec la regex décrivant la syntaxe d'un numéro de téléphone :
import re
# without VERBOSE, hard to read
p = re.compile(r'((\d\d){4}|(\d\d[ ]){4})\d\d')
# with VERBOSE, easier to read
p = re.compile(r'''
(
(\d\d){4} # four times two numbers
| (\d\d[ ]){4} # or four times two numbers followed by a space
)
\d\d # ends with two numbers
''', re.VERBOSE)
À retenir
- On peut choisir de compiler ou non les regex. Dans le cas où le nombre de regex utilisées est faible, cela n'a pas d'impact sur les performances grace à la mise en cache.
- On utilise des raw strings afin d'éviter la multiplication des backslashes.
- Les flags de compilation permettent de modifier le comportement du moteur de regex.