Expressions régulières Python

Les expressions régulières constituent un langage puissant pour faire correspondre des modèles de texte. Cette page présente les expressions régulières de base pour les exercices en Python et explique leur fonctionnement dans Python. Le module "re" de Python permet d'utiliser des expressions régulières.

En Python, une recherche par expression régulière s'écrit généralement comme suit:

match = re.search(pat, str)

La méthode re.search() prend un modèle d'expression régulière et une chaîne et recherche ce modèle dans la chaîne. Si la recherche aboutit, search() renvoie un objet correspondant ou None dans le cas contraire. Par conséquent, la recherche est généralement suivie immédiatement d'une instruction if pour vérifier si la recherche a abouti, comme illustré dans l'exemple suivant, qui recherche le format "word:" suivi d'un mot à trois lettres (voir ci-dessous):

import re

str = 'an example word:cat!!'
match = re.search(r'word:\w\w\w', str)
# If-statement after search() tests if it succeeded
if match:
  print('found', match.group()) ## 'found word:cat'
else:
  print('did not find')

Le code match = re.search(pat, str) stocke le résultat de la recherche dans une variable nommée "match". L'instruction if teste ensuite la correspondance. Si la valeur est "true", la recherche a réussi et match.group() est le texte correspondant (par exemple, "word:cat"). Si la correspondance est "false" (aucune étant plus précise), la recherche n'a pas abouti et il n'y a pas de texte correspondant.

Le "r" au début de la chaîne du modèle désigne une chaîne Python "brute" qui passe par des barres obliques inverses sans modification, ce qui est très pratique pour les expressions régulières (Java a besoin de cette fonctionnalité). Je vous recommande de toujours écrire les chaînes de motif avec le "r" comme habitude.

Motifs de base

L'avantage des expressions régulières est qu'elles peuvent spécifier des modèles, pas seulement des caractères fixes. Voici les modèles les plus basiques qui correspondent à des caractères uniques:

  • a, X, 9, < -- les caractères ordinaires se correspondent exactement. Les méta-caractères qui ne correspondent pas, car ils ont une signification particulière, sont les suivants : ^ $ * + ? { [ ] \ | ( ) (détails ci-dessous)
  • . (un point) -- correspond à n'importe quel caractère, à l'exception du saut de ligne "\n"
  • \w : (w minuscule) correspond à un caractère "mot" : lettre ou chiffre, ou sous-barre [a-zA-Z0-9_]. Bien que le terme "mot" soit mnémotechnique, notez qu'il ne correspond qu'à un seul caractère, et non à un mot entier. \W (W en majuscules) correspond à tout caractère qui n'est pas un mot.
  • \b -- limite entre mot et non-mot
  • \s : (s minuscules) correspond à un seul caractère d'espace blanc : espace, retour à la ligne, retour, tabulation, formulaire [ \n\r\t\f]. \S (S majuscule) correspond à tout caractère qui n'est pas un espace blanc.
  • \t, \n, \r : tabulation, retour à la ligne, retour
  • \d : chiffre décimal [0-9] (certains utilitaires d'expressions régulières plus anciens ne sont pas compatibles avec \d, mais ils sont tous compatibles avec \w et \s)
  • ^ = début, $ = fin -- correspond au début ou à la fin de la chaîne
  • \ -- empêche la "spécialité" d'un caractère. Par exemple, utilisez \. pour remplacer un point ou \\ pour une barre oblique. Si vous n'êtes pas sûr qu'un caractère a une signification particulière, comme "@", vous pouvez essayer de le faire précéder d'une barre oblique, \@. S'il ne s'agit pas d'une séquence d'échappement valide, comme \c, votre programme Python s'arrêtera et générera une erreur.

Exemples de base

Blague: comment appelle-t-on un cochon à trois yeux ? piii !

Les règles de base de la recherche par expression régulière pour un modèle dans une chaîne sont les suivantes:

  • La recherche parcourt la chaîne du début à la fin, s'arrêtant à la première correspondance trouvée.
  • Tout le modèle doit correspondre, mais pas toute la chaîne
  • Si match = re.search(pat, str) réussit, la correspondance n'est pas définie sur None et en particulier, match.group() est le texte correspondant.
  ## Search for pattern 'iii' in string 'piiig'.
  ## All of the pattern must match, but it may appear anywhere.
  ## On success, match.group() is matched text.
  match = re.search(r'iii', 'piiig') # found, match.group() == "iii"
  match = re.search(r'igs', 'piiig') # not found, match == None

  ## . = any char but \n
  match = re.search(r'..g', 'piiig') # found, match.group() == "iig"

  ## \d = digit char, \w = word char
  match = re.search(r'\d\d\d', 'p123g') # found, match.group() == "123"
  match = re.search(r'\w\w\w', '@@abcd!!') # found, match.group() == "abc"

Répétition

Les choses deviennent plus intéressantes lorsque vous utilisez + et * pour spécifier la répétition dans le modèle

  • + -- une ou plusieurs occurrences du modèle à sa gauche, par exemple "i+" = un ou plusieurs i
  • * : aucune occurrence ou plus du format à sa gauche
  • ? -- correspond à 0 ou 1 occurrence du format à sa gauche

À gauche et à la plus grande

Tout d'abord, la recherche trouve la correspondance la plus à gauche pour le modèle, puis elle essaie d'utiliser autant de chaînes que possible, c'est-à-dire que + et * vont le plus loin possible (les + et * sont dits « gourmans »).

Exemples de répétition

  ## i+ = one or more i's, as many as possible.
  match = re.search(r'pi+', 'piiig') # found, match.group() == "piii"

  ## Finds the first/leftmost solution, and within it drives the +
  ## as far as possible (aka 'leftmost and largest').
  ## In this example, note that it does not get to the second set of i's.
  match = re.search(r'i+', 'piigiiii') # found, match.group() == "ii"

  ## \s* = zero or more whitespace chars
  ## Here look for 3 digits, possibly separated by whitespace.
  match = re.search(r'\d\s*\d\s*\d', 'xx1 2   3xx') # found, match.group() == "1 2   3"
  match = re.search(r'\d\s*\d\s*\d', 'xx12  3xx') # found, match.group() == "12  3"
  match = re.search(r'\d\s*\d\s*\d', 'xx123xx') # found, match.group() == "123"

  ## ^ = matches the start of string, so this fails:
  match = re.search(r'^b\w+', 'foobar') # not found, match == None
  ## but without the ^ it succeeds:
  match = re.search(r'b\w+', 'foobar') # found, match.group() == "bar"

Exemple d'e-mails

Supposons que vous souhaitiez trouver l'adresse e-mail à l'intérieur de la chaîne "xyz alice-b@google.com flipper monkey". Nous l'utiliserons comme exemple pour illustrer d'autres fonctionnalités d'expressions régulières. Voici un exemple d'utilisation du schéma r'\w+@\w+':

  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search(r'\w+@\w+', str)
  if match:
    print(match.group())  ## 'b@google'

Dans ce cas, la recherche n'obtient pas l'adresse e-mail complète, car "\w" ne correspond pas au "-" ni au "." de l'adresse. Nous allons résoudre ce problème à l'aide des fonctionnalités d'expression régulière ci-dessous.

Crochets

Les crochets peuvent servir à indiquer un ensemble de caractères, de sorte que [abc] corresponde à "a", "b" ou "c". Les codes \w, \s, etc. fonctionnent aussi entre crochets, à l'exception du point (.) qui signifie littéralement un point. Concernant le problème des e-mails, les crochets sont un moyen facile d'ajouter '.' et '-' à la série de caractères qui peuvent apparaître autour du @ avec le modèle r'[\w.-]+@[\w.-]+' pour obtenir l'adresse e-mail complète:

  match = re.search(r'[\w.-]+@[\w.-]+', str)
  if match:
    print(match.group())  ## 'alice-b@google.com'
(Plus de fonctionnalités entre crochets) Vous pouvez également utiliser un tiret pour indiquer une plage, de sorte que [a-z] corresponde à toutes les lettres minuscules. Pour utiliser un tiret sans indiquer de plage, placez le tiret en dernier, par exemple [abc-]. Un chapeau haut (^) au début d'un ensemble de crochets l'inverse. Ainsi, [^ab] correspond à tout caractère, sauf "a" ou "b".

Extraction de groupes

La fonctionnalité de regroupement d'une expression régulière vous permet de sélectionner des parties du texte correspondant. Supposons que pour le problème des e-mails, nous voulions extraire le nom d'utilisateur et l'hôte séparément. Pour ce faire, ajoutez des parenthèses ( ) autour du nom d'utilisateur et de l'hôte dans le format, comme ceci: r'([\w.-]+)@([\w.-]+)'. Dans ce cas, les parenthèses ne changent pas ce à quoi le modèle va correspondre, mais elles établissent des "groupes" logiques à l'intérieur du texte de correspondance. Lors d'une recherche réussie, match.group(1) est le texte correspondant à la première parenthèse gauche et match.group(2) au texte correspondant à la deuxième parenthèse gauche. La méthode match.group() simple est toujours l'intégralité du texte de correspondance comme d'habitude.

  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search(r'([\w.-]+)@([\w.-]+)', str)
  if match:
    print(match.group())   ## 'alice-b@google.com' (the whole match)
    print(match.group(1))  ## 'alice-b' (the username, group 1)
    print(match.group(2))  ## 'google.com' (the host, group 2)

Un workflow courant avec des expressions régulières consiste à écrire un modèle correspondant à l'élément recherché, en ajoutant des groupes de parenthèses pour extraire les parties souhaitées.

rechercher tout

findall() est probablement la fonction la plus puissante du module re. Ci-dessus, nous avons utilisé re.search() pour trouver la première correspondance d’un modèle. findall() trouve *tous* les correspondances et les renvoie sous forme de liste de chaînes, chaque chaîne représentant une correspondance.
  ## Suppose we have a text with many email addresses
  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'

  ## Here re.findall() returns a list of all the found email strings
  emails = re.findall(r'[\w\.-]+@[\w\.-]+', str) ## ['alice@google.com', 'bob@abc.com']
  for email in emails:
    # do something with each found email string
    print(email)

Findall Avec Fichiers

Pour les fichiers, vous avez peut-être l'habitude d'écrire une boucle pour itérer les lignes du fichier, puis vous pouvez appeler findall() sur chaque ligne. Au lieu de cela, laissez findall() effectuer l'itération pour vous, ce qui est bien mieux ! Il suffit de transmettre l'intégralité du texte du fichier à findall() et de le laisser renvoyer une liste de toutes les correspondances en une seule étape (rappelez-vous que f.read() renvoie l'intégralité du texte d'un fichier dans une seule chaîne):

  # Open file
  f = open('test.txt', encoding='utf-8')
  # Feed the file text into findall(); it returns a list of all the found strings
  strings = re.findall(r'some pattern', f.read())

findall et Groupes

Le mécanisme de groupe de parenthèses ( ) peut être combiné à findall(). Si le modèle comprend 2 groupes de parenthèses ou plus, au lieu de renvoyer une liste de chaînes, findall() renvoie une liste de *tuples*. Chaque tuple représente une correspondance du modèle, et à l'intérieur du tuple se trouve les données group(1), group(2) ... Ainsi, si deux groupes de parenthèses sont ajoutés au modèle d'e-mail, findall() renvoie une liste de tuples, chaque longueur 2 contenant le nom d'utilisateur et l'hôte, par exemple ('alice', 'google.com').

  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  tuples = re.findall(r'([\w\.-]+)@([\w\.-]+)', str)
  print(tuples)  ## [('alice', 'google.com'), ('bob', 'abc.com')]
  for tuple in tuples:
    print(tuple[0])  ## username
    print(tuple[1])  ## host

Une fois que vous disposez de la liste des tuples, vous pouvez la lire en boucle pour effectuer des calculs sur chaque tuple. Si le modèle ne comprend pas de parenthèses, findall() renvoie une liste de chaînes trouvées, comme dans les exemples précédents. Si le modèle comprend un seul ensemble de parenthèses, findall() renvoie une liste de chaînes correspondant à ce groupe unique. (Fonctionnalité facultative masquée: le modèle comporte parfois des groupements paren ( ) que vous ne souhaitez pas extraire. Dans ce cas, écrivez les parenthèses en commençant par un ?: (par exemple, (?: )). Cette parenthèse gauche ne sera pas considérée comme un résultat de groupe.)

Workflow et débogage RE

Les motifs d'expressions régulières ont beaucoup de sens en seulement quelques caractères, mais ils sont si denses que vous pouvez passer beaucoup de temps à les déboguer. Configurez votre environnement d'exécution de sorte que vous puissiez exécuter un schéma et imprimer facilement celui-ci, par exemple en l'exécutant sur un petit texte de test et en imprimant le résultat de findall(). Si le motif ne correspond à rien, essayez de l'affaiblir en en supprimant certaines parties afin d'obtenir trop de correspondances. Lorsqu'elle ne correspond à rien, vous ne pouvez pas progresser, car il n'y a rien de concret à regarder. Une fois que la correspondance est trop importante, vous pouvez travailler à la resserrer progressivement jusqu'à atteindre exactement ce que vous souhaitez.

Options

Les fonctions re utilisent des options pour modifier le comportement de la correspondance de structure. L'indicateur d'option est ajouté en tant qu'argument supplémentaire à search() ou findall(), par exemple re.search(pat, str, re.IGNORECASE).

  • IGNORECASE -- ignore les différences majuscules/minuscules pour la correspondance, de sorte que "a" correspond à la fois à "a" et à "A".
  • DOTALL : permet de faire correspondre le point (.) à la ligne. Normalement, il correspond à n'importe quel élément, à l'exception de la nouvelle ligne. Cela peut vous éloigner car vous pensez que .* correspond à tout, mais par défaut, il ne dépasse pas la fin d’une ligne. Notez que \s (espace blanc) inclut les sauts de ligne. Par conséquent, si vous voulez faire correspondre une série d'espaces blancs pouvant inclure un retour à la ligne, il vous suffit d'utiliser \s*
  • MULTILINE -- Dans une chaîne composée de plusieurs lignes, autorisez ^ et $ à correspondre au début et à la fin de chaque ligne. Normalement, ^/$ correspond simplement au début et à la fin de la chaîne entière.

Greedy ou non gloutonne (facultatif)

Cette section facultative présente une technique d'expression régulière plus avancée qui n'est pas nécessaire pour les exercices.

Supposons que vous ayez du texte comportant des balises: <b>foo</b> et <i>et ainsi de suite</i>.

Supposons que vous tentiez de faire correspondre chaque balise selon le modèle "(<.*>)". Quelle correspondance correspond-elle en premier ?

Le résultat est un peu surprenant, mais l'aspect glouton de l'.* fait qu'il correspond à l'ensemble de <b>foo</b> et <i>et ainsi de suite</i> comme une seule grande correspondance. Le problème est que .* va aussi loin que possible, au lieu de s'arrêter au premier > (également appelé « vorace »).

Il existe une extension à l'expression régulière où vous ajoutez un point d'interrogation (?) à la fin, comme .*? ou .+?, en les remplaçant par des valeurs non gloutonnes. Maintenant, ils s'arrêtent dès que possible. Ainsi, le modèle '(<.*?>)' aura uniquement '<b>' comme première correspondance, '</b>' comme deuxième correspondance, et ainsi de suite, obtenir chaque paire <..> à tour de rôle. Le style consiste généralement à utiliser un élément .*? immédiatement suivi d'un repère en béton (> dans ce cas) auquel l'exécution de .*? est forcée.

L'extension *? provient de Perl. Les expressions régulières qui incluent les extensions de Perl sont connues sous le nom d'expressions régulières compatibles avec Perl (pcre). Python est compatible avec pcre. De nombreux utilitaires de ligne de commande, etc. ont un indicateur où ils acceptent les modèles pcre.

Une technique plus ancienne mais largement utilisée pour coder l'idée de « tous ces caractères sauf s'arrêter à X » utilise le style crochet. Pour ce qui précède, vous pouvez écrire le modèle, mais au lieu de .* pour obtenir tous les caractères, utilisez [^>]* qui ignore tous les caractères qui ne sont pas > (le premier ^ "inverse" l'ensemble de crochets, de sorte qu'il corresponde à tout caractère qui ne se trouve pas entre crochets).

Remplacement (facultatif)

La fonction re.sub(pat, replace, str) recherche toutes les instances du modèle dans la chaîne donnée et les remplace. La chaîne de remplacement peut inclure "\1", "\2" qui se réfèrent au texte de group(1), group(2), etc. du texte correspondant d'origine.

Voici un exemple qui recherche toutes les adresses e-mail et les modifie pour conserver l'utilisateur (\1), mais ayant yo-yo-dyne.com comme hôte.

  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  ## re.sub(pat, replacement, str) -- returns new string with all replacements,
  ## \1 is group(1), \2 group(2) in the replacement
  print(re.sub(r'([\w\.-]+)@([\w\.-]+)', r'\1@yo-yo-dyne.com', str))
  ## purple alice@yo-yo-dyne.com, blah monkey bob@yo-yo-dyne.com blah dishwasher

Exercice

Pour vous entraîner aux expressions régulières, consultez l'exercice sur les noms de bébé.