Expressions Régulières
Généralités
Caractères spéciaux et opérateurs :
La liste des caractères spéciaux en matière de Regex est la suivante :
^ $ / . \ ? + * ( ) [ ] { } |
Il faut les protéger avec des \ si on les cherche en tant que caractères normaux.
Leur utilité, en résumé :
- ^ : Début de phrase
- [^] différent de : [^r] signifie "un caractère qui n'est pas r"
- $ : Fin de phrase
- / : ?
- . : N'importe quel caractère
- ? : 0 ou 1 fois
- + : 1 fois ou plus
- : N'importe quel nombre de fois (0, 1, ou plus)
- () : isoler un pattern
- [] : Donner une liste des caractères autorisés
- [abc] équivaut à a, b, ou c
- {} : Donner un nombre d'occurences plus ou moins précis
- | : ou
- (x|y) signifie x ou y mais pas les deux
On compte d'autres opérateurs particuliers ;
- \d : Uniquement des chiffres
- Equivaut à [0-9]
- \D : Le segment ne contient pas de chiffres
- Equivaut à [^0-9]
- \s : Un séparateur (espace, tab, retourà la ligne), ce qui équivaut à [ \t\n\r\f\v]
- \S : Pas d'espace, ce qui équivaut à [^ \t\n\r\f\v]
- \w : Présence alphanumérique, ce qui équivaut à [a-zA-Z0-9_].
- \W : Pas de présence alphanumérique [^a-zA-Z0-9_].
- \b : s'arrête à une séparation (sa longueur est nulle, c'est juste un endroit précis) entre deux mots. Cette séparation correspond à un espace entre deux lettres, ou une lettre suivie d'une non-lettre. Attention, les éèà... ne sont pas vus comme des lettres.
- Pour résumer :
There are three different positions that qualify as word boundaries:
- Before the first character in the string, if the first character is a word character.
- After the last character in the string, if the last character is a word character.
- Between two characters in the string, where one is a word character and the other is not a word character.
- Issu de : https://www.regular-expressions.info/wordboundaries.html
Et d'autres...
Module re
En python, les regex s'utilisent avec le module re. La fonction la plus essentiel est re.search, qui permet de rechercher à l'aide d'une regex :
re.search(PATTERN, TEXT, [OPTIONS])
Cette fonction renvoie True si match ou False sinon.
Quotes ou pas quotes?
On note généralement, mais pas forcément, les regex entre quotes : 'regex'
En python, c'est expliqué plus bas, on peut faire ça ma on peut aussi utiliser r"regex", ce qui est pas mal mieux, à mon avis (ça évite que python vienne fourrer son nez dans nos caractères d'échappement, au hasard)
Utilisation et exemples
Recherche simple
import re # le module re gère les expressions régulières # la liste des 4 chaines à tester L = [ \ "J'ai tout compris", \ "J'ai rien compris", \ "Fromage ou dessert", \ "Tu vas où dimanche" ] for ch in L : # Pour chaque chaine ch de la liste ... if re.search( 'ou' , ch ) : # ... si la chaine ch contient 'ou' ... print (ch) # ... on l'affiche.
Recherche en début de chaîne
Pour rechercher une chaîne qui débute par un U majuscule :
# la liste des 4 chaines à tester L = [ \ "Un beau paysage", \ "Tu comptes ? Un, deux, trois...", \ "une voiture rouge", \ "U123456789" ] for ch in L : # Pour chaque chaine ch de la liste ... if re.search( '^U' , ch ) : # ... si la chaine ch débute par U ... print (ch) # ... on l'affiche.
Recherche en fin de chaîne
Pour rechercher une chaîne qui finit par un e minuscule
# la liste des 4 chaines à tester L = [ \ "Un beau paysage", \ "Tu comptes ? Un, deux, trois...", \ "une voiture rouge", \ "U123456789" ] for ch in L : # Pour chaque chaine ch de la liste ... if re.search( 'e$' , ch ) : # ... si la chaine ch termine par e... print (ch) # ... on l'affiche.
Rechercher entre deux caractères (utilisation du . )
On cherche un caractère situé entre d et t :
# la liste des 4 chaines à tester L = [ \ "Je bois du thé", \ "Il apprend très vite", \ "Il m'a dit de prendre le train", \ "Nous avons pris date" ] for ch in L : # Pour chaque chaine ch de la liste ... if re.search( 'd.t' , ch ) : # ... si la chaine ch contient un d et un t séparés par un caractère quelconque ... print (ch) # ... on l'affiche.
Recherche d'une chose ou d'une autre
# la liste des 4 chaines à tester L = [ \ "Laisse tomber !", \ "Ce code respecte les bonnes pratiques", \ "J'aime cet endroit", \ "Cochez la bonne case" ] for ch in L : # Pour chaque chaine ch de la liste ... if re.search( 'ce|se' , ch ) : # ... si la chaine ch contient ce OU se... print (ch) # ... on l'affiche.
Ignorer la casse
Pour ignorer la casse avec re, on emploie en option re.IGNORECASE :
for ch in L : # Pour chaque chaine ch de la liste ... if re.search( 'ce' , ch , re.IGNORECASE) : # ... si la chaine ch contient ce OU Ce ou cE ou CE... print (ch) # ... on l'affiche.
Utilisation d'une liste de caractères
On utilise les crochets :
for ch in L : # Pour chaque chaine ch de la liste ... if re.search( 's[aeiouy]t' , ch ) : # ... si la chaine ch contient une voyelle quelconque entourée par un s et un t... print (ch) # ... on l'affiche.
Utilisation de listes de caractères préétablies
On dispose des listes suivantes :
- [a-z]
- [A-Z]
- [0-9]
- On peut aussi faire du [G-L] par exemple
- Ou même : [a-zA-Z0-9]
for ch in L : # Pour chaque chaine ch de la liste ... if re.search( '^[A-Z][0-9]' , ch ) : # ... si la chaine ch débute par une majuscule suivie d'un chiffre... print (ch) # ... on l'affiche.
Utilisation du [^]
Les [] débutant par ^ signifient que la liste qui suit est composée de caractères interdits (un NOT, en gros)
for ch in L : # Pour chaque chaine ch de la liste ... if re.search( 'o[^i]' , ch ) : # ... si la chaine ch contient un o qui n'est pas suivi d'un i ... print (ch) # ... on l'affiche.
Ici, la chaîne "Les oiseaux volent dans le ciel" matche, car même si on a "oiseaux", on a aussi "volent".
Quantificateurs
Les quantificateurs servent à déterminer le nombre d'occurences : pour rappel, on a : ? + * et les {}
ATTENTION : Les quantificateurs quantifient ce qui les précède et pas ce qui les suit ! Il quantifient sur l'élément précédent, que l'on peut séparer par des parenthèses :
\w\d([j-m]){5,10}
Ici, on cherche un mot suivi d'un nombre, suivi d'une des lettres entre j et m, et cette (ces) lettre doivent apparaître entre 5 et 10 fois.
#Avec un ? for ch in L : # Pour chaque chaine ch de la liste ... if re.search( '\d[A-Z]?\d' , ch ) : # ... si la chaine ch contient deux chiffres encadrant une lettre majuscule au plus ... print (ch) # ... on l'affiche. #Avec un + for ch in L : # Pour chaque chaine ch de la liste ... if re.search( '\d[A-Z]+\d' , ch ) : # ... si la chaine ch contient deux chiffres séparés par une ou plusieurs majuscules ... print (ch) # ... on l'affiche. #Avec un * for ch in L : # Pour chaque chaine ch de la liste ... if re.search( '[a-z]-*\d' , ch ) : # ... si la chaine ch contient une minuscule et un chiffre séparés par zéro, un ou plusieurs tirets ... print (ch) # ... on l'affiche.
Accolades {}
L'utilisation des accolades est un peu spéciale. Ici avec k un nombre quelconque :
- {k} : on cherche k fois le pattern précédent
- Ja{3}J
- {k,} : on cherche le pattern précédent au moins k fois
- Ja{1, }J : matchera JaaaaaJ ou JaJ par exemple
- {,k} : on cherche le pattern auy maximum k fois
- Ja{,3}J matchera JaaJ mais pas JaaaaaaaJ
- {k,l} : On cherche le pattern entre k et l fois :
- \w\d([j-m]){5,10}
# la liste des 6 chaines à tester L = [ \ "La référence @1@ n'est pas valable", \ "La référence @12@ est valable", \ "La référence @123@ est valable", \ "La référence @1234@ est valable", \ "La référence @12345@ n'est pas valable", \ "La référence @1@2@3@ n'est pas valable", ] for ch in L : # Pour chaque chaine ch de la liste ... if re.search( '@\d{2,4}@' , ch ) : # ... si la chaine ch contient une séquence de 2 à 4 chiffres encadrée par des @ ... print (ch) # ... on l'affiche.
Utilisation du point
Le . est un joker, qui peut représenter n'importe quel caractère :
for ch in L : # Pour chaque chaine ch de la liste ... if re.search( '^[A-Z].*[a-z]$' , ch ) : # ... si la chaine ch débute par une majuscule et termine par une minuscule. ... print (ch) # ... on l'affiche.
.*
Le joker ultime est donc .*, il matchera n'importe quoi, puisqu'il signifie : "N'importe quel caractère, n'importe quel nombre de fois".
Parenthèses
Comme précisé plus haut, les parenthèses servent à isoler un pattern afin de lui appliquer un traitement :
for ch in L : # Pour chaque chaine ch de la liste ... if re.search( '(to){2,}' , ch ) : # ... si la chaine ch contient au moins 2 syllabes "to" successives. ... print (ch) # ... on l'affiche.
D'autres exemples
- Chercher un numéro de téléphone au format français : '(\d\d ){4}\d\d'
- ...
Pièges et subtilités
Protection
Il ne faut pas oublier de protéger ses caractères spéciaux, sans quoi on a des problèmes.
re.search( 'iutsf\.org' , ad ) #Et pas : re.search( 'iutsf.org' , ad )
Le rôle de l dans {k,l}
Ici, on cherche une occurence entre k et l fois, ça matche. Avec {3,5}, ça matchera avec 3, 4, 5 fois, mais aussi avec plus ! En effet, la regex matche sans se poser de question dès qu'elle a compté 5 occurences. {k,l} utilisé SEUL est équivalent à {k,}.
Pour que ce soit utile, il faut que {k,l} soit encadré par d'autres caractères que {k,l} interdit. Par exemple, on comprend bien que \D0{3,5}\D le principe : on veut deux non-chiffres avec 3 à 5 zéros au milieu : on matchera a0000c mais pas a0000000c.
Chercher une chaîne qui ne contient pas un caractère
On peut croire que la regexp [^r] ne matchera pas sur "brrrrrr". Et pourtant si : On matche sur le premier b. Forcément, ce n'est pas un r ! C'est toujours la même chose : la regexp ne se préoccupe pas de savoir si on fait des mots ou des phrases... Elle se fait une petite fenêtre (d'un seul caractère de large, en l'occurence) et parcours la chaîne, caractère après caractère. Dès que sa petite fenêtre contient quelque chose qui matche l'expression, elle s'arrête et nous prévient : "Bingo ! J'ai trouvé un truc qui n'est pas un r !".
"Trouve une chaine sans r" est différent de "Trouve une chaine contenant autre chose que r". Pour trouver les chaînes qui ne contiennent pas de r, il faut les délimiter :
'^[^r]*$' "Commence et finit par n'importe quel nombre de choses qui ne sont pas des r".
J'aurais tendance à faire : \s[^r]*\s pour trouver les mots sans r, mais il faut bien noter que le truc s'arrête au premier match sans chercher plus loin !
Les espaces dans les regex...
Il ne faut pas mettre d'espaces dans une regex "pour que ce soit plus lisible". En effet, mettre un espace, c'est chercher un espace ! Il faut tout coller. C'est moche, mais sinon, il va falloir échapper les espaces ( \ ) et c'est encore plus moche...
Regex du type "ne contient que"
Parce que ce serait trop simple d'avoir un signe pour "ne contient que truc ou bidule", il faut à la place se farcir quelque chose du genre :
- La chaîne commence par truc (ou bidule)
- suivi de n'importe quel nombre de trucs ou de bidules
- et se termine par truc ou bidule. (pfiou !)
Exemple : pour trouver une chaîne ne contenant que des u et des t : $[ut](t*u*)[ut]$
Des méthodes Python en lien avec les regex
group
On peut récupérer les groupes formés avec des () dans une regex pour les réutiliser, ils sont numérotés :
import re # le module re gère les expressions régulières ch = "je suis né le 12-04-2007 à 10 heures" # avec une regexp, on cherche la date de naissance au format jj-mm-aaaa retour = re.search('(\d\d)-(\d\d)-(\d\d)(\d\d)',ch) # récupération du résultat de la regexp dans l'objet retour print (retour.group(0)) # group(0) retourne la correspondance complète, c'est à dire la date 12-04-2007 print (retour.group(1)) # group(1) retourne le premier groupe, c'est à dire le jour : 12 print (retour.group(2)) # group(2) retourne le deuxième groupe, c'est à dire le mois : 04 print (retour.group(3)) # group(3) retourne le troisième groupe, c'est à dire les deux premiers chiffres de l'année : 20 print (retour.group(4)) # group(4) retourne le quatrième groupe, c'est à dire les deux derniers chiffres de l'année : 07
sub
Cette méthode permet de remplacer une regex par une autre dans la string .
Usage : sub(REGEX_A_REMPLACE, REGEX_DE_REMPLACEMENT, CHAINE INITIALE)
import re # le module re gère les expressions régulières ch = "Il est beau ce bateau mais le capitaine chante faux." ch2 = re.sub('eau','au',ch) print(ch) print(ch2) #Affiche : Il est beau ce bateau mais le capitaine chante faux. Il est bau ce batau mais le capitaine chante faux.
Détail important (mais relou)
Les groupes déterminés par des () sont numérotés, même sans utiliser la méthode group... Ils sont identifiés par \1, \2 ... Mais comme ils commencent par un \, il faut l'échapper par un deuxième \, ce qui donne un résultat immonde comme ça : \\1, \\2...
Par exemple, on va prendre une date avec des tirets (12-04-2007) et remplacer les tirets par des slashes (donc remplacer la date par : '\\1/\\2/\\3' )
import re # le module re gère les expressions régulières ch = "Je m'appelle Jean-Luc, je suis né le 12-04-2007 à 10 heures." ch2 = re.sub('(\d\d)-(\d\d)-(\d\d\d\d)','\\1/\\2/\\3',ch) print(ch) print(ch2) #On a alors : Je m'appelle Jean-Luc, je suis né le 12/04/2007 à 10 heures.
Nommer ses groupes
On peut aussi, si vraiment c'est nécessaire, nommer ses groupes au lieu d'utiliser des numéros, en utilisant pour cela :
- ?
- P
- <nom du groupe>
Dans l'expression de remplacement, on utilise \g<nom du groupe>
On met tout ce bazar au début de la parenthèse suivi du pattern à chercher. Exemple :
import re # le module re gère les expressions régulières ch = "Je m'appelle Jean-Luc, je suis né le 12-04-2007 à 10 heures." ch2 = re.sub('(?P<jour>\d\d)-(?P<mois>\d\d)-(?P<annee>\d\d\d\d)','\g<jour>/\g<mois>/\g<annee>',ch) print(ch) print(ch2)
findall
Cette méthode renvoie toutes les occurences de la regexp rencontrées dans une string sous forme de liste.
import re # le module re gère les expressions régulières ch = "je suis né le 12-04-2007 à 10 heures. J'ai marché le 20-03-2008 et appris à nager le 30-06-2012" retour = re.findall('-\d\d-',ch) # récupération du résultat de la regexp dans l'objet retour # avec findall, on récupère la liste de toutes les occurrences du motif '-\d\d-' print (retour) # affichage de toutes les occurrences #Affiche : ['-04-', '-03-', '-06-']
compile (et une méthode plus simple pour écrire des regex)
On peut compiler une regex pour l'utiliser plusieurs fois. C'est utile quand on doit s'en resservir :
chn_mdp = r"^[A-Za-z0-9]{6,}$" exp_mdp = re.compile(chn_mdp) mot_de_passe = "" while exp_mdp.search(mot_de_passe) is None: mot_de_passe = input("Tapez votre mot de passe : ")
Ici l'encodage r"" signifie "raw" : on passe la chaîne brute, sans y interpréter quoi que ce soit : python ne cherche pas à interpréter les \ ou quoi, il prend la chaîne et la passe directement à re.search. Vachement mieux que de compter les ' et les " ...