Skip to article frontmatterSkip to article content

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:

le module re

en Python, les expressions régulières sont accessibles au travers du module re

import re

re.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')
# 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

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

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
# 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()

# 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')

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}

# 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

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

# 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

pour aller plus loin