il s’agit d’une notion transverse aux langages de programmation, et présente dans la plupart d’entre eux
et en particulier historiquement dans Perl, qui en avait fait un first-class citizen
c’est quoi ?¶
vous cherchez à trouver toutes les adresses e-mail, ou tous les numéros de téléphone dans un texte ?
vous voulez savoir si une chaine pourrait être utilisée comme non de variable Python ?
les expressions régulières sont faites pour ce genre d’application
voici pour commencer deux exemples élémentaires:
a*décrit tous les mots composés de 0 ou plusieursa'','a','aa', … sont les mots reconnus
(ab)+: toutes les suites de au moins 1 occurrence deab'ab','abab','ababab', … sont les mots reconnus
le module re¶
en Python, les expressions régulières sont accessibles au travers du module re
import rere.match()¶
re.match() vérifie si l’expression régulière peut être trouvée au début de la chaine
# en anglais on dit pattern, ou regexp
# en français on dit filtre, ou encore parfois motif
pattern = "a*"# OUI
re.match(pattern, 'aa')<re.Match object; span=(0, 2), match='aa'># OUI
re.match('(ab)+', 'ab')<re.Match object; span=(0, 2), match='ab'># NON: retourne None
re.match('(ab)+', 'ba')# ici seulement LE DÉBUT du mot est reconnu, mais c'est OK
match = re.match('(ab)+', 'ababzzz')
match<re.Match object; span=(0, 4), match='abab'># le détail de ce qui a été trouvé
match.start(), match.end()(0, 4)# par contre ici le seul match n'est pas au début, donc NON
re.match('a+', 'zzzaaa')re.search()¶
re.search()cherche la première occurrence de l’expression régulièreet donc pas forcément au début de la chaine
# match répond non car seulement LE DÉBUT de la chaine est essayé
re.match('abzz', 'ababzzz')# un use case pour le "walrus operator"
# i.e. une affectation mais qui est aussi une expression
if (match := re.search('abzz', 'ababzzz')):
print(f"{match.start()=}, {match.end()=}")match.start()=2, match.end()=6
les objets Match¶
le résultat de re.match() est ... de type Match, qui fournit
les détails de ce qui a été trouvé (où et quoi)
et aussi les sous-chaines correspondant aux groupes, dont on reparlera...
pattern, string = "(ab)+", "the baba of regexps"
(match := re.search(pattern, string))<re.Match object; span=(5, 7), match='ab'>begin, end = match.span()
string[begin:end]'ab'helper¶
la fonction suivante va juste nous servir à visualiser les résultats d’un re.match() sur plusieurs chaines
# digression : une fonction utilitaire pour montrer
# le comportement d'un même pattern sur plusieurs chaines
def match_all(pattern, strings):
"""
match a pattern against a set of strings
and display results properly aligned
"""
# compute max space
margin = max(len(x) for x in strings) + 2 # for the quotes
for string in strings:
string_repr = f"'{string}'"
print(f"'{pattern}' ⇆ {string_repr:>{margin}} → ", end="")
if not (match := re.match(pattern, string)):
print("NO")
elif not (match.start() == 0 and match.end() == len(string)):
# start() is always 0
print(f"PARTIAL until char {match.end()}")
else:
print("FULL MATCH")# ce qui nous permettra de faire par exemple
match_all('(ab)+', ['ab', 'abab', 'ababzzz', ''])'(ab)+' ⇆ 'ab' → FULL MATCH
'(ab)+' ⇆ 'abab' → FULL MATCH
'(ab)+' ⇆ 'ababzzz' → PARTIAL until char 4
'(ab)+' ⇆ '' → NO
construire un pattern¶
une fois ceci en place, voyons les différents outils - on va dire aussi opérateurs - qui nous permettent de construire ce fameux pattern
un caractère précis a¶
# si j'écris dans un pattern un caractère "normal"
# ça signifie que je veux trouver exactement ce caractère dans la chaine
match_all("a", ["a", "ab", "bc"])'a' ⇆ 'a' → FULL MATCH
'a' ⇆ 'ab' → PARTIAL until char 1
'a' ⇆ 'bc' → NO
match_all(r"\.", ["a", ".a"])'\.' ⇆ 'a' → NO
'\.' ⇆ '.a' → PARTIAL until char 1
n’importe quel caractère : .¶
# un '.' dans le pattern signifie EXACTEMENT UN un caractère
# mais n'importe lequel, à cet endroit dans la chaine
match_all('.', ['a', 'Θ', '.', 'ab', ''])'.' ⇆ 'a' → FULL MATCH
'.' ⇆ 'Θ' → FULL MATCH
'.' ⇆ '.' → FULL MATCH
'.' ⇆ 'ab' → PARTIAL until char 1
'.' ⇆ '' → NO
# pour bien comprendre la nécessité du \ devant le .
match_all(r'\.', ['a', 'Θ', '.', 'ab', ''])'\.' ⇆ 'a' → NO
'\.' ⇆ 'Θ' → NO
'\.' ⇆ '.' → FULL MATCH
'\.' ⇆ 'ab' → NO
'\.' ⇆ '' → NO
un seul caractère parmi un ensemble: [..]¶
avec les [] on peut désigner un ensemble de caractères, par exemple
[acg]exactement un caractère parmiaoucoug[a-z]une lettre minuscule[a-zA-Z0-9_]une lettre ou un chiffre ou un underscore
match_all('[a-z]', ['a', '', '0'])'[a-z]' ⇆ 'a' → FULL MATCH
'[a-z]' ⇆ '' → NO
'[a-z]' ⇆ '0' → NO
match_all('[a-z0-9]', ['a', '9', '-'])'[a-z0-9]' ⇆ 'a' → FULL MATCH
'[a-z0-9]' ⇆ '9' → FULL MATCH
'[a-z0-9]' ⇆ '-' → NO
# pour insérer un '-', on peut par exemple le mettre à la fin
match_all('[0-9+-]', ['0', '+', '-', 'A'])'[0-9+-]' ⇆ '0' → FULL MATCH
'[0-9+-]' ⇆ '+' → FULL MATCH
'[0-9+-]' ⇆ '-' → FULL MATCH
'[0-9+-]' ⇆ 'A' → NO
idem mais complémenté : [^..]¶
si l’ensemble de caractères entre [] commence par un ^, alors cela désigne le complémentaire dans l’espace des caractères
# complémentaires
match_all('[^a-z]', ['a', '0', '↑', 'Θ'])'[^a-z]' ⇆ 'a' → NO
'[^a-z]' ⇆ '0' → FULL MATCH
'[^a-z]' ⇆ '↑' → FULL MATCH
'[^a-z]' ⇆ 'Θ' → FULL MATCH
match_all('[^a-z0-9]', ['a', '9', '-', 'Θ'])'[^a-z0-9]' ⇆ 'a' → NO
'[^a-z0-9]' ⇆ '9' → NO
'[^a-z0-9]' ⇆ '-' → FULL MATCH
'[^a-z0-9]' ⇆ 'Θ' → FULL MATCH
0 ou plusieurs occurrences : ..*¶
en ajoutant * après un pattern, cela signifie 0 ou plus occurrences de ce pattern
on en a vu un exemple déjà avec a*, mais le pattern peut être arbitrairement complexe
# toutes les lettres au début de la chaine
match_all('[a-z]*', ['', 'abc', 'xyz9'])'[a-z]*' ⇆ '' → FULL MATCH
'[a-z]*' ⇆ 'abc' → FULL MATCH
'[a-z]*' ⇆ 'xyz9' → PARTIAL until char 3
# toutes les suites de 'ab' au début de la chaine
match_all('(ab)*', ['', 'ab', 'abcd', 'abab'])'(ab)*' ⇆ '' → FULL MATCH
'(ab)*' ⇆ 'ab' → FULL MATCH
'(ab)*' ⇆ 'abcd' → PARTIAL until char 2
'(ab)*' ⇆ 'abab' → FULL MATCH
le * s’applique au (bout de) pattern immédiatement à sa gauche
on peut avoir à utiliser des parenthèses si nécessaire
# ici l'étoile s'applique seulement au 'b'
match_all('ab*', ['a', 'abb', 'abab', 'baba'])'ab*' ⇆ 'a' → FULL MATCH
'ab*' ⇆ 'abb' → FULL MATCH
'ab*' ⇆ 'abab' → PARTIAL until char 2
'ab*' ⇆ 'baba' → NO
# si je veux qu'il s'applique à 'ab', je mets des parenthèses
match_all('(ab)*', ['a', 'abb', 'abab', 'baba'])'(ab)*' ⇆ 'a' → PARTIAL until char 0
'(ab)*' ⇆ 'abb' → PARTIAL until char 2
'(ab)*' ⇆ 'abab' → FULL MATCH
'(ab)*' ⇆ 'baba' → PARTIAL until char 0
1 ou plusieurs occurrences : ..+¶
exactement comme *, sauf qu’il faut au moins une occurrence cette fois
match_all('[a-z]+', ['', 'abc', 'xyz9'])'[a-z]+' ⇆ '' → NO
'[a-z]+' ⇆ 'abc' → FULL MATCH
'[a-z]+' ⇆ 'xyz9' → PARTIAL until char 3
match_all('(ab)+', ['', 'ab', 'abcd', 'abab'])'(ab)+' ⇆ '' → NO
'(ab)+' ⇆ 'ab' → FULL MATCH
'(ab)+' ⇆ 'abcd' → PARTIAL until char 2
'(ab)+' ⇆ 'abab' → FULL MATCH
concaténation¶
ensuite, et de manière très naturelle, quand on concatène deux filtres, la chaine doit matcher l’un puis l’autre, évidemment
# c'est le seul mot qui matche
match_all('ABC', ['ABC'])'ABC' ⇆ 'ABC' → FULL MATCH
match_all('A*B', ['B', 'AB', 'AAB', 'AAAB'])'A*B' ⇆ 'B' → FULL MATCH
'A*B' ⇆ 'AB' → FULL MATCH
'A*B' ⇆ 'AAB' → FULL MATCH
'A*B' ⇆ 'AAAB' → FULL MATCH
groupement : (..)¶
les parenthèses sont très utiles, on l’a déjà vu à l’instant avec notre exemple (ab)*
il faut savoir aussi que cela définit un groupe qui peut être retrouvé dans le match
notamment grâce à la méthode groups()
c’est notamment comme ça qu’on peut retrouver des morceaux dans une chaine
dans cette forme simple les groupes sont anonymes, et on les retrouve par leur rang, i.e. l’ordre dans lequel apparaissent les parenthèses ouvrantes
# ici on a deux groupes
pattern = "([a-z]+)=([a-z0-9]+)"
# cette chaine a bien la bonne forme
string = "foo=barbar99"
# la preuve
match = re.match(pattern, string)
match<re.Match object; span=(0, 12), match='foo=barbar99'># et si on veut ensuite extraire de la chaine
# les deux parties (la variable et la valeur)
# on les a ici, dans l'ordre où apparaissent les groupes
match.groups()('foo', 'barbar99')en général on n’aime pas trop l’idée de coder avec le rang du groupe (c’est fragile)
on verra plus bas comment, pour éviter ça, on peut les nommer
alternative : ..|..¶
pour filtrer avec une regexp ou une autre
ça se complique un peu, attention à bien lire les choses
# 'ab' ou 'cd'
match_all('ab|cd', ['ab', 'cd', 'abcd'])'ab|cd' ⇆ 'ab' → FULL MATCH
'ab|cd' ⇆ 'cd' → FULL MATCH
'ab|cd' ⇆ 'abcd' → PARTIAL until char 2
# 'ab' ou 'cd*'
match_all('ab|cd*', ['ab', 'c', 'cd', 'cdd'])'ab|cd*' ⇆ 'ab' → FULL MATCH
'ab|cd*' ⇆ 'c' → FULL MATCH
'ab|cd*' ⇆ 'cd' → FULL MATCH
'ab|cd*' ⇆ 'cdd' → FULL MATCH
# 'ab' ou '(cd)*'
match_all('ab|(cd)*', ['ab', 'c', 'cd', 'cdd'])'ab|(cd)*' ⇆ 'ab' → FULL MATCH
'ab|(cd)*' ⇆ 'c' → PARTIAL until char 0
'ab|(cd)*' ⇆ 'cd' → FULL MATCH
'ab|(cd)*' ⇆ 'cdd' → PARTIAL until char 2
# 0 ou + occurrences de (ab ou cd)
match_all('(ab|cd)*', ['ab', 'c', 'cd', 'cdd', 'abcd'])'(ab|cd)*' ⇆ 'ab' → FULL MATCH
'(ab|cd)*' ⇆ 'c' → PARTIAL until char 0
'(ab|cd)*' ⇆ 'cd' → FULL MATCH
'(ab|cd)*' ⇆ 'cdd' → PARTIAL until char 2
'(ab|cd)*' ⇆ 'abcd' → FULL MATCH
0 ou 1 occurrences : ..?¶
très pratique pour les morceaux optionnels
# 0 ou 1 caractère
match_all('[a-z]?', ['', 'b', 'xy'])'[a-z]?' ⇆ '' → FULL MATCH
'[a-z]?' ⇆ 'b' → FULL MATCH
'[a-z]?' ⇆ 'xy' → PARTIAL until char 1
nombre d’occurrences dans un intervalle : ..{n,m}¶
a{3}: exactement 3 occurrences deaa{3,}: au moins 3 occurrencesa{3,6}: entre 3 et 6 occurrences
le a peut bien sûr être n’importe quelle regexp compliquée hein
# entre 1 et 3 occurrences de 'ab'
match_all('(ab){1,3}', ['', 'ab', 'abab', 'ababab', 'ababababababab'])'(ab){1,3}' ⇆ '' → NO
'(ab){1,3}' ⇆ 'ab' → FULL MATCH
'(ab){1,3}' ⇆ 'abab' → FULL MATCH
'(ab){1,3}' ⇆ 'ababab' → FULL MATCH
'(ab){1,3}' ⇆ 'ababababababab' → PARTIAL until char 6
classes de caractères \s etc..¶
raccourcis qui filtrent un caractère dans une classe
définis en fonction de la configuration de l’OS en termes de langue
\s(pour Space) : exactement un caractère de séparation (typiquement Espace, Tabulation, Newline)\w(pour Word) : exactement un caractère alphabétique ou numérique\d(pour Digit) : un chiffre\S,\Wet\D: les complémentaires
match_all(r'\w+', ['eFç0', 'été', ' ta98'])'\w+' ⇆ 'eFç0' → FULL MATCH
'\w+' ⇆ 'été' → FULL MATCH
'\w+' ⇆ ' ta98' → NO
match_all(r'\s?\w+', ['eFç0', 'été', ' ta98'])'\s?\w+' ⇆ 'eFç0' → FULL MATCH
'\s?\w+' ⇆ 'été' → FULL MATCH
'\s?\w+' ⇆ ' ta98' → FULL MATCH
groupe nommé : (?P<name>..)¶
pour obtenir le même effet que les groupes anonymes (..)
mais en leur donnant un nom pour que la recherche soit moins fragile
# la même regexp essentiellement que tout à l'heure
# mais avec deux groupes nommés cette fois
pattern = "(?P<variable>[a-z]+)=(?P<valeur>[a-z0-9]+)"
string = "foo=barbar99"
match = re.match(pattern, string)
match<re.Match object; span=(0, 12), match='foo=barbar99'># et c'est plus lisible pour aller extraire les morceaux
match.group('variable'), match.group('valeur')('foo', 'barbar99')début et fin de chaine : ^ et $¶
parfois on veut s’assurer qu’on filtre bien toute la chaine, pour cela on peut le préciser avec ces deux marques
pour info, on peut aussi utiliser respectivement \A et \Z
# le comportement par défaut de match()
match_all('ab|cd', ['ab', 'abcd'])'ab|cd' ⇆ 'ab' → FULL MATCH
'ab|cd' ⇆ 'abcd' → PARTIAL until char 2
# pour forcer la chaine à matcher jusqu'au bout
# on ajoute un $
match_all('(ab|cd)$', ['ab', 'abcd'])'(ab|cd)$' ⇆ 'ab' → FULL MATCH
'(ab|cd)$' ⇆ 'abcd' → NO
plusieurs occurrences d’un groupe : (?P=name)¶
on peut aussi spécifier que le même groupe apparaisse plusieurs fois
# la deuxième occurrence de <nom> doit être la même que la première
pattern = r'(?P<nom>\w+).*(?P=nom)'
string1 = 'Jean again Jean'
string2 = 'Jean nope Pierre'
match_all(pattern, [string1, string2])'(?P<nom>\w+).*(?P=nom)' ⇆ 'Jean again Jean' → FULL MATCH
'(?P<nom>\w+).*(?P=nom)' ⇆ 'Jean nope Pierre' → NO
quelques conseils¶
chez certaines personnes, il y a un avant et un après les expressions régulières
je ne veux pas vous refroidir, mais ça n’est très clairement pas un outil à utiliser à tour de bras !dès que ça devient un tout petit peu compliqué, pensez à utiliser un testeur en ligne, vous gagnerez du temps
https://pythex.org
https://regex101 .com/ (bien choisir Python) ... et plein d’autres ...
pour aller plus loin¶
un peu de détente, avec ce jeu de mots croisés basé sur les regexps
https://regexcrossword .com vous avez de quoi commencer avec un solide bagage; toutefois il y a encore beaucoup d’autres possibilités, notamment les options de compilation (pour ignorer la chasse par exemple, et j’en passe..) n’hésitez pas à lire le guide introductif dans la doc https://
docs .python .org /fr /3 /howto /regex .html enfin il y a aussi un tour complet de la syntaxe des regexps
https://docs .python .org /fr /3 /library /re .html #regular -expression -syntax