Licence CC BY-NC-ND Thierry Parmentelat

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 plusieurs a

    • '', 'a', 'aa', …
      sont les mots reconnus

  • (ab)+ : toutes les suites de
    au moins 1 occurrence de ab

    • '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

les objets Match

  • le résultat de re.match() est de type Match

  • 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() et re.finditer() pour trouver toutes les occurences du filtre dans la chaine

  • re.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 de a

  • a{3,} : au moins 3 occurrences

  • a{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