expressions régulières¶
notion transverse aux langages de programmation
présente dans la plupart d’entre eux
en particulier historiquement Perl
qui en avait fait un first-class citizen
exemples¶
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 re
# en anglais on dit pattern
# en français on dit filtre,
# ou encore parfois motif
pattern = "a*"
# la fonction `match`
re.match(pattern, '')
<re.Match object; span=(0, 0), match=''>
re.match(pattern, 'a')
<re.Match object; span=(0, 1), match='a'>
re.match(pattern, 'aa')
<re.Match object; span=(0, 2), match='aa'>
re.match('(ab)+', 'ab')
<re.Match object; span=(0, 2), match='ab'>
# pas conforme : retourne None
re.match('(ab)+', 'ba')
re.match()
¶
ATTENTION car
re.match()
vérifie si l’expression régulière peut être trouvée au début de la chaine
# ici seulement LE DÉBUT du mot est reconnu
match = re.match('(ab)+', 'ababzzz')
match
<re.Match object; span=(0, 4), match='abab'>
# commence au début
match.start()
0
# mais pas jusque la fin
match.end()
4
re.search()
¶
re.search()
cherche la première occurrence de l’expression régulièrepas 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')
re.search('abzz', 'ababzzz')
<re.Match object; span=(2, 6), match='abzz'>
les objets Match
¶
le résultat de
re.match()
est de typeMatch
pour les détails de ce qui a été trouvé
(par exemple quelle partie de la chaine)et aussi les sous-chaines
correspondant aux groupes
(on en reparlera)
autres façons de chercher¶
re.findall()
etre.finditer()
pour trouver toutes les occurences du filtre dans la chainere.sub()
pour remplacer …
notre sujet
ici nous nous intéressons surtout à la façon de construire les regexps
se reporter à la documentation du module pour ces variantes
pour visualiser¶
# digression : un utilitaire pour montrer
# le comportement d'un pattern / filtre
def match_all(pattern, strings):
"""
match a pattern against a set of strings and shows result
"""
col_width = max(len(x) for x in strings) + 2 # for the quotes
for string in strings:
string_repr = f"'{string}'"
print(f"'{pattern}' ⇆ {string_repr:<{col_width}} → ", end="")
match = re.match(pattern, string)
if not match:
print("NO")
elif match.end() != len(string):
# start() is always 0
print(f"BEGINNING ONLY (until {match.end()})")
else:
print("FULL MATCH")
match_all('(ab)+', ['ab', 'abab', 'ababzzz', ''])
'(ab)+' ⇆ 'ab' → FULL MATCH
'(ab)+' ⇆ 'abab' → FULL MATCH
'(ab)+' ⇆ 'ababzzz' → BEGINNING ONLY (until 4)
'(ab)+' ⇆ '' → NO
construire un pattern¶
n’importe quel caractère : .
¶
match_all('.', ['', 'a', '.', 'Θ', 'ab'])
'.' ⇆ '' → NO
'.' ⇆ 'a' → FULL MATCH
'.' ⇆ '.' → FULL MATCH
'.' ⇆ 'Θ' → FULL MATCH
'.' ⇆ 'ab' → BEGINNING ONLY (until 1)
filtrer un seul caractère : [..]
¶
avec les
[]
on peut désigner un ensemble de caractères :[a-z]
les lettres minuscules[a-zA-Z0-9_]
les lettres et chiffres et 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 '-', le mettre à la fin
# sinon ça va être compris comme un intervalle
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 à l’envers : [^..]
¶
si l’ensemble de caractères entre
[]
commence par un^
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
0 ou plusieurs occurrences : ..*
¶
match_all('[a-z]*', ['', 'cba', 'xyz9'])
'[a-z]*' ⇆ '' → FULL MATCH
'[a-z]*' ⇆ 'cba' → FULL MATCH
'[a-z]*' ⇆ 'xyz9' → BEGINNING ONLY (until 3)
match_all('(ab)*', ['', 'ab', 'abab'])
'(ab)*' ⇆ '' → FULL MATCH
'(ab)*' ⇆ 'ab' → FULL MATCH
'(ab)*' ⇆ 'abab' → FULL MATCH
1 ou plusieurs occurrences : ..+
¶
match_all('[a-z]+', ['', 'cba', 'xyz9'])
'[a-z]+' ⇆ '' → NO
'[a-z]+' ⇆ 'cba' → FULL MATCH
'[a-z]+' ⇆ 'xyz9' → BEGINNING ONLY (until 3)
match_all('(ab)+', ['', 'ab', 'abab'])
'(ab)+' ⇆ '' → NO
'(ab)+' ⇆ 'ab' → FULL MATCH
'(ab)+' ⇆ 'abab' → FULL MATCH
concaténation¶
quand on concatène deux filtres, la chaine doit matcher l’un puis l’autre
# 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 : (..)
¶
permet d’appliquer un opérateur sur une regexp
comme déjà vu avec
(ab)+
cela définit un groupe qui peut être retrouvé dans le match
grâce à la méthode
groups()
# groupes anonymes
pattern = "([a-z]+)=([a-z0-9]+)"
string = "foo=barbar99"
match = re.match(pattern, string)
match
<re.Match object; span=(0, 12), match='foo=barbar99'>
# dans l'ordre où ils apparaissent
match.groups()
('foo', 'barbar99')
alternative : ..|..
¶
pour filtrer avec une regexp ou une autre :
match_all('ab|cd', ['ab', 'cd', 'abcd'])
'ab|cd' ⇆ 'ab' → FULL MATCH
'ab|cd' ⇆ 'cd' → FULL MATCH
'ab|cd' ⇆ 'abcd' → BEGINNING ONLY (until 2)
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
match_all('ab|(cd)*', ['ab', 'c', 'cd', 'cdd'])
'ab|(cd)*' ⇆ 'ab' → FULL MATCH
'ab|(cd)*' ⇆ 'c' → BEGINNING ONLY (until 0)
'ab|(cd)*' ⇆ 'cd' → FULL MATCH
'ab|(cd)*' ⇆ 'cdd' → BEGINNING ONLY (until 2)
match_all('(ab|cd)*', ['ab', 'c', 'cd', 'cdd', 'abcd'])
'(ab|cd)*' ⇆ 'ab' → FULL MATCH
'(ab|cd)*' ⇆ 'c' → BEGINNING ONLY (until 0)
'(ab|cd)*' ⇆ 'cd' → FULL MATCH
'(ab|cd)*' ⇆ 'cdd' → BEGINNING ONLY (until 2)
'(ab|cd)*' ⇆ 'abcd' → FULL MATCH
0 ou 1 occurrences : ..?
¶
match_all('[a-z]?', ['', 'b', 'xy'])
'[a-z]?' ⇆ '' → FULL MATCH
'[a-z]?' ⇆ 'b' → FULL MATCH
'[a-z]?' ⇆ 'xy' → BEGINNING ONLY (until 1)
nombre d’occurrences dans un intervalle : ..{n,m}
¶
a{3}
: exactement 3 occurrences dea
a{3,}
: au moins 3 occurrencesa{3,6}
: entre 3 et 6 occurrences
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' → BEGINNING ONLY (until 6)
classes de caractères¶
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
,\W
et\D
: les complémentaires
match_all('\w+', ['eFç0', 'été', ' ta98'])
'\w+' ⇆ 'eFç0' → FULL MATCH
'\w+' ⇆ 'été' → FULL MATCH
'\w+' ⇆ ' ta98' → NO
match_all('\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>..)
¶
le même effet que les groupes anonymes,
mais on peut retrouver le contenu depuis le nom du groupe
plutôt que le rang (numéro) du groupe
qui peut rapidement devenir une notion fragile / peu maintenable
# groupes nommés
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'>
match.group('variable')
'foo'
match.group('valeur')
'barbar99'
plusieurs occurrences du même groupe : (?P=name)
¶
on peut spécifier qu’un groupe doit apparaître plusieurs fois
# la deuxième occurrence de <nom> doit être la même que la première
pattern = '(?P<nom>\w+).*(?P=nom)'
string1 = 'Jean again Jean'
string2 = 'Jean nope Pierre'
string3 = 'assez comme ça'
match_all(pattern, [string1, string2, string3])
'(?P<nom>\w+).*(?P=nom)' ⇆ 'Jean again Jean' → FULL MATCH
'(?P<nom>\w+).*(?P=nom)' ⇆ 'Jean nope Pierre' → NO
'(?P<nom>\w+).*(?P=nom)' ⇆ 'assez comme ça' → FULL MATCH
début et fin de chaine : ^
et $
¶
match_all('ab|cd', ['ab', 'abcd'])
'ab|cd' ⇆ 'ab' → FULL MATCH
'ab|cd' ⇆ 'abcd' → BEGINNING ONLY (until 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
pour aller plus loin¶
beaucoup d’autres possibilités
testeurs en ligne
https://pythex.org
https://regex101.com/ (bien choisir Python)un peu de détente, avec ce jeu de mots croisés basé sur les regexps https://regexcrossword.com
commencer par le tutorialtour complet de la syntaxe des regexps
https://docs.python.org/3/library/re.html#regular-expression-syntax