Expressions régulières Python

Les expressions régulières constituent un langage puissant pour établir des correspondances avec des modèles de texte. Cette page présente les principes de base des expressions régulières, suffisantes pour les exercices Python, et explique comment elles fonctionnent dans Python. Le "re" Python permet d'utiliser des expressions régulières.

En Python, une recherche d'expression régulière s'écrit généralement de la façon suivante:

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 immédiatement suivie d'une instruction if pour vérifier si la recherche a abouti, comme illustré dans l'exemple suivant qui recherche le format "word:" suivie d'un mot composé de trois lettres (voir détails 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". Ensuite, l'instruction if teste la correspondance. Si la valeur est "true", la recherche a réussi et match.group() est le texte correspondant (par exemple, "word:cat"). Dans le cas contraire, si la correspondance est "false" (aucun pour être plus précis), cela signifie que la recherche a échoué et qu'aucun texte ne correspond.

Le "r" au début de la chaîne de modèle désigne un Python chaîne 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 beaucoup de cette fonctionnalité !). Je vous recommande de toujours écrire des chaînes de modèle avec le « r » comme une habitude.

Modèles de base

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

  • a, X, 9, < -- les caractères ordinaires correspondent exactement à eux-mêmes. Les méta-caractères qui ne correspondent pas eux-mêmes parce qu'ils ont une signification particulière sont les suivants: . ^ $ * + ? { [ ] \ | ( ) (détails ci-dessous)
  • . (un point) -- correspond à n'importe quel caractère, à l'exception du retour à la ligne "\n"
  • \w -- (w minuscule) correspond à un "mot" caractère: une lettre, un chiffre ou des barres obliques [a-zA-Z0-9_]. Notez que même si le terme "mot" est l'argument mnémotechnique, il ne correspond qu'à un seul mot, pas à un mot entier. \W (W majuscule) correspond à tout caractère qui n'est pas un mot.
  • \b -- limite entre un mot et un autre
  • \s -- (s minuscule) correspond à un seul caractère d'espacement : espace, saut de ligne, retour, tabulation, formulaire [ \n\r\t\f]. \S (S majuscule) correspond à tout caractère autre qu'un espace blanc.
  • \t, \n, \r -- tabulation, retour à la ligne, retour
  • \d : chiffre décimal [0-9] (certains utilitaires d'expression régulière plus anciens ne prennent pas en charge \d, mais tous acceptent \w et \s)
  • ^ = début, $ = fin : correspond au début ou à la fin de la chaîne
  • \ -- empêcher la "spécialité" d'un caractère. Par exemple, utilisez \. pour représenter un point ou \\ pour représenter 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'interrompt et génère une erreur.

Exemples de base

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

Les règles de base de la recherche d'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, en s'arrêtant à la première correspondance trouvée.
  • Tout le modèle doit correspondre, mais pas toute la chaîne
  • Si la requête match = re.search(pat, str) aboutit, 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 format à sa gauche, par exemple "i+" = un ou plusieurs i
  • * -- 0 occurrence ou plus du format à sa gauche
  • ? -- correspond à 0 ou 1 occurrence du modèle à sa gauche

Tout à gauche et Le plus grand

Tout d'abord, la recherche trouve la correspondance la plus à gauche pour le modèle, puis elle essaie d'utiliser autant de chaîne que possible, c'est-à-dire que + et * vont le plus loin possible (les + et * sont considérés comme "cupides").

Exemples de répétitions

  ## 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 dans la chaîne "xyz alice-b@google.com singe violet". Nous allons l'utiliser comme exemple pour illustrer d'autres fonctionnalités d'expression régulière. Voici une tentative avec le format 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 caractère "-". ou "." dans 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 être utilisés pour indiquer un ensemble de caractères. Par exemple, [abc] correspond à "a". ou "b" ou "c". Les codes \w, \s etc. fonctionnent également entre crochets, à l'exception du point (.) qui signifie littéralement un point. Pour le problème des e-mails, les crochets permettent d'ajouter facilement "." et '-' au jeu de caractères qui peut apparaître autour du signe @ avec le format 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'
(Autres fonctionnalités entre crochets) Vous pouvez également utiliser un tiret pour indiquer une plage, de sorte que les caractères [a-z] correspondent à toutes les lettres minuscules. Pour utiliser un tiret sans indiquer de plage, placez-le en dernier, par exemple [abc-]. Un up-hat (^) au début d'un ensemble de crochets l'inverse, donc [^ab] signifie tout caractère sauf "a" ou "b".

Extraction de groupes

Le "groupe" 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 modifient pas la correspondance du modèle, mais établissent des "groupes" logiques. dans le texte de la correspondance. Lors d'une recherche réussie, match.group(1) est le texte correspondant à la première parenthèse gauche et match.group(2) est le texte correspondant à la deuxième parenthèse gauche. La fonction plain match.group() 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 les expressions régulières consiste à écrire un modèle pour l'élément que vous recherchez, en ajoutant des groupes de parenthèses pour extraire les parties souhaitées.

tout trouver

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() recherche *toutes* 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)

trouver avec Fichiers

Pour les fichiers, vous avez peut-être l'habitude d'écrire une boucle pour itérer les lignes du fichier, et vous pouvez ensuite appeler findall() sur chaque ligne. Au lieu de cela, laissez findall() effectuer l'itération à votre place, bien mieux ! Il suffit d'alimenter le texte complet du fichier dans 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 regroupement des parenthèses ( ) peut être associé à findall(). Si le modèle comprend 2 ou plusieurs groupes de parenthèses, 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 trouvent les données group(1), group(2) ... Ainsi, si deux groupes de parenthèses sont ajoutés au format d'e-mail, findall() renvoie une liste de tuples, chaque longueur de 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 avez la liste des tuples, vous pouvez la lire en boucle pour effectuer des calculs pour 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 seul groupe. (Fonctionnalité facultative de type "Obscur" : parfois, le format contient des groupes entre parenthèses ( ), mais que vous ne souhaitez pas extraire. Dans ce cas, écrivez les parenthèses avec un "?:" au début (par exemple, (?: ) et cette parenthèse gauche ne sera pas comptabilisée comme un résultat de groupe.)

Workflow et débogage RE

Les motifs d'expression régulière ont une grande signification en quelques caractères seulement, mais ils sont tellement denses que vous pouvez passer beaucoup de temps à déboguer vos modèles. Configurez votre environnement d'exécution afin de pouvoir exécuter un schéma et imprimer facilement ce qui correspond, 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 et d'en supprimer certaines parties afin d'obtenir trop de correspondances. Si rien ne correspond, vous ne pouvez pas progresser, car vous n'avez rien de concret à examiner. Si la correspondance est trop élevée, vous pouvez travailler à la resserrer progressivement pour obtenir exactement ce que vous voulez.

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(), etc., par exemple re.search(pat, str, re.IGNORECASE).

  • IGNORECASE : ignorer les différences entre les majuscules et les minuscules pour la correspondance, donc "a" correspond à la fois à "a" et "A".
  • DOTALL : permet d'associer le point (.) à une nouvelle ligne. Normalement, il correspond à tout autre élément sauf à une nouvelle ligne. Cela peut vous tromper – vous pensez que .* correspond à tout, mais par défaut, il ne va pas au-delà de la fin d'une ligne. Notez que \s (espace blanc) inclut les sauts de ligne. Par conséquent, si vous souhaitez faire correspondre une série d'espaces blancs pouvant inclure une nouvelle ligne, vous pouvez simplement utiliser \s*
  • MULTILINE : dans une chaîne composée de plusieurs lignes, autorisez les signes ^ et $ à faire correspondre le début et la fin de chaque ligne. Normalement, ^/$ correspondrait simplement au début et à la fin de la chaîne entière.

Contenu glouton ou non (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 votre chaîne comporte du texte avec les balises <b>foo</b>. et <i>et ainsi de suite</i>.

Supposons que vous tentiez de faire correspondre chaque balise au format "(<.*>)" Quelle est la première correspondance ?

Le résultat est un peu surprenant, mais l'aspect glouton du .* lui fait correspondre l'ensemble du '<b>foo</b> et <i>et ainsi de suite</i>. comme une correspondance essentielle. Le problème est que le .* va aussi loin qu'il peut, au lieu de s'arrêter au premier > (c'est-à-dire qu'elle est "cupide").

Une expression régulière comporte une extension permettant d'ajouter un point d'interrogation (?) à la fin, par exemple .*? ou .+?, en les remplaçant pour qu'ils ne soient pas gloutonneurs. Maintenant, ils s’arrêtent dès qu’ils le peuvent. Ainsi, le modèle '(<.*?>)' obtiendra uniquement "<b>" comme première correspondance, et '</b>' comme deuxième correspondance, etc. l'un après l'autre. Le style est généralement que vous utilisez un .*? immédiatement suivi d'un repère de béton (> dans ce cas) auquel le .*? est forcé de s'étendre.

Le caractère *? depuis Perl, et les expressions régulières qui incluent des extensions Perl sont appelées "Expressions régulières compatibles Perl" (pcre). Python est compatible avec pcre. De nombreux utilitaires de ligne de commande, etc., ont un indicateur où ils acceptent les formats pcre.

Technique plus ancienne mais largement utilisée pour coder cette idée de « tous ces caractères sauf s'arrêter à X » utilise le style des crochets. 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 signe ^ initial "inverse" le jeu de crochets, afin qu'il corresponde à tout caractère qui n'est pas entre crochets).

Remplacement (facultatif)

La fonction re.sub(pat, replace, str) recherche toutes les instances de motif dans la chaîne donnée et les remplace. La chaîne de remplacement peut inclure "\1", "\2". qui font référence 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 avoir 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 à utiliser des expressions régulières, consultez l'article Exercice sur les noms de bébé.