IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Plongée dans Python


précédentsommaire

VI. Chapitre 5 – Expressions Régulières

VI-A. PLONGER DANS

Extraire un petit bout de texte d’un grand bloc de texte est un défi. Avec Python, les chaînes de caractères ont des méthodes pour remplacer et rechercher : index(), find(), split(), count(), replace(), etc. Mais l’utilisation de ces méthodes est limitée aux cas simples. Par exemple, la méthode index() permet de chercher une seule sous-chaîne codée en dur, et la recherche est toujours sensible à la casse. Pour effectuer des recherches insensibles à la casse d'une chaîne s, vous devez appeler s.lower() ou s.upper() et vous assurer que vos chaînes de recherche correspondent à la casse appropriée. Les méthodes replace() et split() ont les mêmes limitations.

Si votre objectif peut être réalisé avec les méthodes de chaînes de caractères, vous devez les utiliser. Elles sont rapides et simples, et il y a beaucoup de choses à dire pour un code rapide, simple et lisible. Si vous vous trouvez en train d’utiliser plusieurs fonctions de chaînes de caractères avec des blocs de if pour gérer des cas particuliers ou si vous avez une suite d’appels de split() et de join() pour découper des chaînes de caractères, vous aurez peut-être besoin de passer aux expressions régulières.

Les expressions régulières sont une méthode puissante et souvent standardisée pour la recherche, le remplacement et le parcours d’un texte avec des motifs très compliqués. Quoique la syntaxe des expressions régulières soit rigide et ne ressemble pas à un code normal, le résultat peut finir par être plus lisible qu’une suite de fonctions des chaînes de caractères.

Il existe même des moyens d'inclure des commentaires dans des expressions régulières. Vous pouvez donc y ajouter une documentation affinée.

Si vous avez déjà utilisé des expressions régulières dans d’autres langages (tels que Perl, JavaScript, ou PHP), la syntaxe de Python sera familière. Lisez le résumé du module re « https://docs.python.org/dev/library/re.html#module-contents » pour avoir une idée générale des fonctions disponibles et leurs arguments.

VI-A-1. Étude de cas : les adresses de rues

Cette série d'exemples est tirée d’un vrai problème vécu dans mon boulot cela fait des années, quand j’avais besoin de retraiter et normaliser les adresses des rues exportées d’un ancien système avant d’être importées au nouveau système. (Écoutez, je ne mets pas ce truc uniquement pour le mettre ; c’est vraiment utile.) Cet exemple montre comment j’ai abordé le problème.

 
Sélectionnez
>>>s = '100 NORTH MAIN ROAD'
>>>s.replace('ROAD', 'RD.')①
'100 NORTH MAIN RD.'
>>>s = '100 NORTH BROAD ROAD'
>>>s.replace('ROAD', 'RD.')②
'100 NORTH BRD. RD.'
>>>s[:-4] + s[-4:].replace('ROAD', 'RD.')③
'100 NORTH BROAD RD.'
>>>importre④
>>>re.sub('ROAD$', 'RD.', s)⑤
'100 NORTH BROAD RD.'

1. Mon objectif est de normaliser les adresses des rues de telle sorte que les routes 'ROAD' soient toujours abrégées en 'RD'. Heureusement, toutes les données étaient en majuscules, donc il n'y avait pas de soucis de la casse. La chaîne  'ROAD' à chercher était une constante. Du coup, l’utilisation de s.replace() dans cet exemple semble bien fonctionner.

2. Malheureusement, ce n’était pas aussi simple que cela, et je l’ai constaté rapidement. Le problème c’est que 'ROAD' apparaît deux fois dans une adresse, la première fois dans le nom de la rue 'BROAD' et la deuxième fois pour désigner la route 'ROAD'. En l'occurrence, la fonction replace() remplace les deux chaînes sans faire aucune distinction ; et voilà comment mes adresses ne ressemblaient plus à rien !

3. Pour résoudre ce problème de plusieurs sous-chaînes 'ROAD' dans une adresse, vous pouvez procéder ainsi : chercher et remplacer uniquement 'ROAD' dans les quatre caractères de l’adresse (s[-4:]), et laisser simplement la chaîne (s[:-4]). Mais comme vous le voyez, ce n’est pas évident de le faire. Par exemple, le motif dépend de la taille de la chaîne à remplacer. (Si vous remplaciez 'STREET' par 'ST.', vous utiliseriez s[: - 6] et s[-6:].replace(…).) Pourriez-vous revenir dans six mois et chercher où est le problème ? Personnellement, je ne le ferais pas.

4. C’est le temps d’aller vers les expressions régulières. En Python, le module re rassemble tout ce qui concerne les expressions régulières.

5. Prenons le premier paramètre : 'ROAD$'. C’est un exemple simple d’une expression régulière permettant d’identifier 'ROAD' à la fin d’une chaîne de caractères. Le $ signifie « la fin de la chaîne de caractères. » Il y a aussi le caractère lambda ^, qui permet d’identifier le début de la chaîne de caractères. En utilisant la fonction re.sub(), avec l’expression régulière 'ROAD$', on cherche le mot dans s et on le remplace par 'RD.'. Ceci permet de détecter 'ROAD' à la fin de la chaîne de caractères, mais non pas la sous-chaîne 'ROAD' dans 'BROAD', celle-ci étant au milieu de la chaîne s.

En avançant sur mon histoire de traitement des adresses, j’ai ensuite découvert que dans l’exemple précédent, chercher 'ROAD' à la fin de la chaîne n’était pas une bonne idée, car il y a des adresses qui ne désignent pas du tout la rue et se terminent simplement par son nom. Je m’en échappais, mais si la rue se termine par 'BROAD' comme nom, dans ce cas l’expression régulière identifiera 'ROAD' à la fin de la chaîne dans la sous-chaîne 'BROAD', ce que je ne souhaitais pas avoir.

 

^ reconnaît le début de la chaîne, $ reconnaît la fin de la chaîne.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
>>>s = '100 BROAD'
>>>re.sub('ROAD$', 'RD.', s)
'100 BRD.'
>>>re.sub('\\bROAD$', 'RD.', s)   ①
'100 BROAD'
>>>re.sub(r'\bROAD$', 'RD.', s)   ②
'100 BROAD'
>>>s = '100 BROAD ROAD APT. 3'
>>>re.sub(r'\bROAD$', 'RD.', s)   ③
'100 BROAD ROAD APT. 3'
>>>re.sub(r'\bROAD\b', 'RD.', s)  ④
'100 BROAD RD. APT 3'
  • Mon but était vraiment de trouver 'ROAD' quand il se situe à la fin de la chaîne et s’utilise pour indiquer le mot rue (et non pas la partie d’un autre mot). Pour décrire ceci dans une expression régulière, vous utilisez \b, qui veut dire « l’extrémité du mot doit être ici. » En Python, ceci n’est pas évident, car le caractère '\' en lui-même doit être ignoré. Il est parfois désigné comme un caractère antislash, et c’est l’une des raisons pour lesquelles les expressions régulières sont plus faciles en Perl qu’en Python. Mais d’un autre côté, Perl ne fait pas de distinction entre les expressions régulières et les autres syntaxes, du coup si vous avez un bogue, il vous sera difficile de savoir s'il vient de la syntaxe ou de l’expression régulière.
  • Pour contourner le problème de l’antislash, vous pouvez utiliser ce que l’on appelle la chaîne 'raw', en mettant devant la chaîne de caractères le préfixe r. Ceci permet de dire à Python qu’il n'y a rien à échapper dans cette chaîne ; '\t' c’est le caractère de tabulation, mais le r'\t' est un antislash '\' suivi de la lettre t. Je recommande toujours l’utilisation de la chaîne 'raw' pour gérer les expressions régulières ; sinon les choses deviennent plus compliquées (alors que les expressions régulières ne manquent pas de complexité).
  • Soupir ! Malheureusement, j’ai bientôt découvert pas mal de cas qui contredisent mon analyse. Dans le cas où l’adresse contient le mot entier 'ROAD', mais non pas dans la fin de l’adresse comme prévu, parce que l’adresse se termine par le numéro d’un appartement. Comme ilo n’est pas à la fin de la chaîne, il n’est pas reconnu, et donc l’appel de re.sub() ne remplace rien en fait, et vous avez toujours la chaîne initiale, ce que vous ne souhaitez pas.
  • Pour pallier ce problème, j’ai supprimé le caractère $ et ajouté un autre \b. Maintenant l’expression régulière permet de « reconnaître le mot entier 'ROAD' n’importe où dans la chaîne, » que ce soit à la fin, au début, ou quelque part au milieu.

VI-B. Case Study: Roman Numerals

Vous avez sûrement vu des chiffres romains, même si vous ne les avez pas reconnus. Peut-être que vous les avez vus dans les anciens films et les émissions télévisées (« Copyright MCMXLVI » au lieu de « Copyright 1946 »), ou sur les murs de dédicace des bibliothèques ou des universités (« established MDCCCLXXXVIII » au lieu de « established 1888 »). Vous pouvez aussi les avoir vus dans les plans et références de bibliothèques. C’est un système de représentation des nombres qui date vraiment de l’ancien Empire romain (d’où le nom). Dans la numérotation romaine, il y a sept caractères qui se répètent et se combinent de différentes manières pour représenter les nombres.

  • I = 1
  • V = 5
  • X = 10
  • L = 50
  • C = 100
  • D = 500
  • M = 1000

Voici quelques règles générales pour constituer des nombres romains :

  • parfois les caractères s’additionnent : I est 1, II est 2, et III est 3. VI est 6 (littérairement, « 5 et 1 »), VII est 7 et VIII est 8 ;
  • les caractères des dizaines (I, X, C, and M) peuvent se répéter jusqu’à trois fois. Pour 4, il vous faudra soustraire du plus grand prochain caractère. Vous ne pouvez pas représenter 4 comme IIII ; par contre, il est représenté comme IV (« 1 soustrait de 5 »). 40 est représenté par XL (« 10 soustrait de 50 »), 41 par XLI, 42 par XLII, 43 par XLIII, et 44 par XLIV (« 10 soustrait de 50, ensuite 1 soustrait de 5 ») ;
  • parfois les caractères ne s’additionnent pas. En mettant certains caractères devant les autres, vous soustrairez de la valeur finale. Par exemple, pour 9, vous devez soustraire du plus grand prochain caractère : 8 est VIII, mais 9 est IX (« 1 soustrait de 10 »), et non VIIII (puisque le caractère I ne peut se répéter quatre fois). 90 est XC, 900 est CM ;
  • le caractère 5 ne peut pas être répété. 10 est toujours représenté par X, jamais par VV. 100 est toujours représenté par C, jamais LL ;
  • les nombres romains sont lus de gauche à droite, du coup l’ordre des caractères compte beaucoup. DC est 600 ; CD est un nombre totalement différent (400, « 100 soustrait de 500 »). CI est 101 ; IC n’est même pas un nombre romain valide (parce que vous ne pouvez pas soustraire 1 directement de 100 ; vous devez écrire XCIX, « 10 soustrait de 100, ensuite 1 soustrait de 10 »).

VI-B-1. Recherche de milliers

Qu’est-ce qu’il faudrait faire pour dire qu’une quelconque chaîne de caractères correspond à un nombre romain ? Prenons cela chiffre par chiffre. Comme les nombres romains sont toujours écrits du plus grand au plus petit, nous allons alors commencer par le plus grand : les milliers. Pour les nombres 1000 et plus, les milliers sont représentés par une suite de caractères M.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
>>>import re
>>>pattern = '^M?M?M?$'>>>re.search(pattern, 'M')     ②
<re.Match object; span=(0, 1), match='M'>
>>>re.search(pattern, 'MM')    ③
<re.Match object; span=(0, 2), match='MM'>
>>>re.search(pattern, 'MMM')   ④
<re.Match object; span=(0, 3), match='MMM'>
>>>re.search(pattern, 'MMMM')  ⑤
>>>re.search(pattern, '')      ⑥
<re.Match object; span=(0, 0), match=''>
  1. Ce motif est constitué de trois parties. ^ reconnaît uniquement ce qui suit au début de la chaîne. Si ce caractère n’est pas spécifié, le caractère M sera identifié n’importe où dans la chaîne, ce que vous ne souhaitez pas. Vous voulez être sûr que les caractères M, s’il y en a, sont au début de la chaîne. M? optionnellement reconnaît un seul caractère M. Puisqu'il est répété trois fois, vous identifiez le caractère de zéro à trois fois dans la chaîne. Le $ reconnaît la fin de la chaîne. Mais combiné avec le caractère ^ en début, cela signifie que le motif doit identifier la suite des caractères M, sans aucun autre caractère avant ou après.
  2. L’essence du module re est la fonction search(), qui prend l’expression régulière (le motif) et le caractère ('M') afin d’identifier le motif spécifié. Si la recherche aboutit, la fonction search() retourne un objet ayant plusieurs méthodes décrivant le résultat de la recherche. Si la recherche n’aboutit pas, la fonction search() retourne None, la valeur nulle de Python. Tout ce qui vous intéresse pour le moment c’est si la recherche aboutit, ce que vous pouvez détecter en regardant juste la valeur de retour de search().'M' est reconnue par cette expression régulière, car le premier optionnel M est identifié et le deuxième et le troisième optionnels M sont ignorés.
  3. 'MM' est reconnue, car le premier et le deuxième optionnels M sont identifiés et le troisième optionnel M est ignoré.
  4. 'MMM' est reconnue, car tous les trois caractères M sont identifiés.
  5. 'MMMM' n’est pas reconnue. Les trois caractères M sont identifiés, mais ensuite, l’expression régulière insiste sur la fin de la chaîne (grâce au caractère $), et la chaîne ne se termine pas comme souhaité (à cause du quatrième M). Ainsi search() retourne None.
  6. De manière très intéressante, une chaîne vide est aussi reconnue par cette expression régulière, comme tous les M sont optionnels.

VI-B-2. Vérification des centaines

La recherche des centaines est beaucoup plus difficile que celle des milliers, car il y a plusieurs manières de les représenter qui dépendent de leur propre valeur.

? rendre un motif optionnel.

  • 100 = C
  • 200 = CC
  • 300 = CCC
  • 400 = CD
  • 500 = D
  • 600 = DC
  • 700 = DCC
  • 800 = DCCC
  • 900 = CM

Ainsi il est possible d’utiliser quatre motifs :

  • CM ;
  • CD ;
  • Zéro à trois caractères C (zéro s’il n'y en a pas) ;
  • D, suivi de zéro à trois caractères C.

Les deux derniers motifs peuvent être combinés :

  • un D optionnel, suivi de zéro à trois caractères C.

Cet exemple montre comment valider l’emplacement des centaines dans les nombres romains.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
>>>import re
>>>pattern = '^M?M?M?(CM|CD|D?C?C?C?)$'>>>re.search(pattern, 'MCM')②
<re.Match object; span=(0, 3), match='MCM'>
>>>re.search(pattern, 'MD')③
<re.Match object; span=(0, 2), match='MD'>
>>>re.search(pattern, 'MMMCCC')④
<re.Match object; span=(0, 6), match='MMMCCC'>
>>>re.search(pattern, 'MCMC')⑤
>>>re.search(pattern, '')⑥
<re.Match object; span=(0, 0), match=''>
  1. Ce motif procède au début comme le précédent, en vérifiant le début de la chaîne (^), et l’emplacement des milliers (M?M?M?). Ensuite il contient la nouvelle partie, entre parenthèses, qui définit un ensemble de trois motifs, séparés par des barres verticales : CM, CD, et D?C?C?C? (qui correspond à un D optionnel, suivi de zéro à trois caractères C). Le parcours de l’expression régulière permet de reconnaître dans l’ordre (de gauche à droite) tous ces motifs, de considérer le premier identifié et ignorer le reste.
  2. 'MCM' reconnue, car le premier M est identifié, le deuxième et le troisième M sont ignorés, et la chaîne CM reconnue (du coup les motifs CD et D?C?C?C? ne seront jamais évalués). MCM est le nombre romain représentant 1900,
  3. 'MD' reconnue, car le premier M est identifié, le deuxième et le troisième M sont ignorés, et le motif D?C?C?C? identifie le D (tous les caractères C sont optionnels et donc ignorés). MD est le nombre romain représentant 1500.
  4. 'MMMCCC' est reconnue, car tous les trois caractères M sont identifiés, et le motif D?C?C?C? identifie CCC (le D est optionnel et donc ignoré). MMMCCC est le nombre romain représentant 3300.
  5. 'MCMC' n’est pas reconnue. Le premier caractère M est identifié, le deuxième et le troisième M sont ignorés, mais ensuite le $ n’identifie rien, car vous n’êtes pas encore arrivé à la fin de la chaîne (vous avez toujours un caractère C qui n’est pas reconnu). Le C n’est pas évalué par le motif D?C?C?C?, car le motif CM est déjà identifié.
  6. De manière très intéressante, une chaîne vide est aussi reconnue par cette expression régulière, parce que tous les M sont optionnels et ignorés et le motif D?C?C?C?, où tous les caractères sont optionnels et ignorés, reconnaît la chaîne vide.

Ouf ! Regardez comment les expressions régulières peuvent rapidement devenir compliquées ? Et vous n’avez couvert que la vérification des milliers et des centaines. Mais si vous avez suivi jusqu’ici, l’identification des dizaines et des unités est simple, car elle est faite exactement par le même motif. Mais allons regarder une autre manière d’exprimer le motif.

VI-C. Utilisation de la syntaxe {n, m}

{1,4} reconnaît entre 1 à 4 occurrences du motif.

Dans la section précédente, vous avez utilisé des motifs où un caractère peut se répéter jusqu’à trois fois. Il y a un autre moyen d’exprimer cela dans les expressions régulières, que quelques-uns trouvent plus lisible. D’abord, regardons la première méthode que nous avons utilisée dans l’exemple précédent.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
>>>import re
>>>pattern = '^M?M?M?$'
>>>re.search(pattern, 'M')①
<re.Match object; span=(0, 1), match='M'>
>>>re.search(pattern, 'MM')②
<re.Match object; span=(0, 2), match='MM'>
>>>re.search(pattern, 'MMM')③
<re.Match object; span=(0, 3), match='MMM'>
>>>re.search(pattern, 'MMMM')④
>>>
  1. Il reconnaît le début de la chaîne, et ensuite le premier optionnel M, mais ignore le deuxième et troisième M (mais ce n’est pas méchant, car ils sont optionnels), puis la fin de la chaîne.
  2. Il reconnaît le début de la chaîne, et ensuite le premier et deuxième M optionnels, mais ignore le troisième M (mais ce n’est pas méchant, car il est optionnel), puis la fin de la chaîne.
  3. Il reconnaît le début de la chaîne, et ensuite tous les M optionnels, puis la fin de la chaîne.
  4. Il reconnaît le début de la chaîne, et ensuite tous les M optionnels, mais ne reconnaît pas la fin de la chaîne (car il y a encore un M qui n’est pas identifié), du coup le motif ne correspond pas à ce que l’on cherche et retourne None.
 
Sélectionnez
>>> import re
>>> pattern = '^M{0,3}$'>>> re.search(pattern, 'M') ② 
<re.Match object; span=(0, 1), match='M'>
>>> re.search(pattern, 'MM') ③
<re.Match object; span=(0, 2), match='MM'>
>>> re.search(pattern, 'MMM') ④
<re.Match object; span=(0, 3), match='MMM'>
>>> re.search(pattern, 'MMMM') ⑤
>>>

① Ce modèle dit : « Faites correspondre le début de la chaîne, puis de zéro à trois M caractères, puis la fin de la chaîne. » Le 0 et le 3 peuvent être n'importe quel nombre ; si vous voulez faire correspondre au moins un, mais pas plus de trois caractères M, vous pouvez dire M {1,3}.

② Cela correspond au début de la chaîne, puis à un M sur trois possibles, puis à la fin de la chaîne.

③ Cela correspond au début de la chaîne, puis deux M sur trois possibles, puis à la fin de la chaîne.

④ Cela correspond au début de la chaîne, puis trois M sur trois possibles, puis la fin de la chaîne.

⑤ Cela correspond au début de la chaîne, puis trois M sur trois possibles, mais ne correspond pas à la fin de la chaîne. L'expression régulière n'autorise que trois caractères M avant la fin de la chaîne, mais vous en avez quatre, de sorte que le modèle ne correspond pas et renvoie Nul.

VI-C-1. Vérification des dizaines et des unités

  1. Maintenant, étalons l’expression régulière d’identification des nombres romains pour couvrir les dizaines et unités également. Cet exemple montre la vérification des dizaines.

     
    Sélectionnez
    1.
    2.
    3.
    4.
    5.
    6.
    7.
    8.
    9.
    10.
    11.
    >>>pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$'
    >>>re.search(pattern, 'MCMXL')①
    <re.Match object; span=(0, 5), match='MCMXL'>
    >>>re.search(pattern, 'MCML')②
    <re.Match object; span=(0, 4), match='MCML'>
    >>>re.search(pattern, 'MCMLX')③
    <re.Match object; span=(0, 5), match='MCMLX'>
    >>>re.search(pattern, 'MCMLXXX')④
    <re.Match object; span=(0, 7), match='MCMLXXX'>
    >>>re.search(pattern, 'MCMLXXXX')⑤
    >>>
    
  2. Ce motif identifie le début de la chaîne, le premier M optionnel, CM et L?X?X?X?. Dans L?X?X?X?, il reconnaît le L et ignore les trois caractères X optionnels. Ensuite, aller à la fin de la chaîne. MCMXL est le nombre romain représentant 1950.

  3. Ce motif identifie le début de la chaîne, le premier M optionnel, CM et il reconnaît le L, le premier M optionnel et ignore les deux caractères X optionnels, ensuite la fin de la chaîne MCMLX est le nombre romain représentant 1960.

  4. Ce motif identifie le début de la chaîne, le premier M optionnel, CM et il reconnaît le L et les trois caractères X optionnels, ensuite la fin de la chaîne. MCMLXXX est le nombre romain représentant 1980

  5. Ce motif identifie le début de la chaîne, le premier M optionnel, CM et il reconnaît le L et les trois caractères X optionnels, mais ne réussit pas à détecter la fin de la chaîne, car il y a toujours un x non identifié et du coup la recherche retourne None. MCMLXXXX n’est pas un nombre romain.
  1. Ce motif identifie le début de la chaîne, le premier M optionnel, CM, XL et la fin de la chaîne. Rappelons que la syntaxe (A|B|C) signifie « détecter exactement A ou B ou C ». Vous détectez XL, donc vous ignorez XC et les choix L?X?X?X?, ensuite allez à la fin de la chaîne. MCMXL est le nombre romain représentant 1940.

(A|B) reconnaît soit A soit B mais jamais les deux à la fois.

La recherche des unités suit le même raisonnement. Je ne rentrerai pas dans les détails et vous montrerai le résultat final.

 
Sélectionnez
1.
>>>pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$'

Qu'est-ce que cela va devenir en utilisant cette syntaxe {n,m} ? Cet exemple montre la nouvelle syntaxe.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
>>>pattern = '^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'
>>>re.search(pattern, 'MDLV')①
<re.Match object; span=(0, 4), match='MDLV'>
>>>re.search(pattern, 'MMDCLXVI')②
<re.Match object; span=(0, 8), match='MMDCLXVI'>
>>>re.search(pattern, 'MMMDCCCLXXXVIII')③
<re.Match object; span=(0, 15), match='MMMDCCCLXXXVIII'>
>>>re.search(pattern, 'I')④
<re.Match object; span=(0, 1), match='I'>
  1. Ce motif reconnaît le début de la chaîne, après l’un des trois possibles M, ensuite D?C{0,3}. Dans ce dernier, il reconnaît le D optionnel et l’un des trois possibles C. En continuant, il évalue L?X{0,3} en détectant le L et l’un des trois possibles X. Ensuite, il évalue V?I{0,3} en détectant le V et l’un des trois possibles I, et finalement la fin de la chaîne. MDLV est le nombre romain représentant 1555.
  2. Ce motif reconnaît le début de la chaîne, après l’un des deux M possibles, après D?C{0,3} avec un D et l’un des trois possibles C ; ensuite L?X{0,3} avec un L et l’un des trois X possibles, et V?I{0,3} avec un V et l’un des trois possibles I, et finalement la fin de la chaîne. MMDCLXVI est le nombre romain représentant 2666.
  3. Ce motif reconnaît le début de la chaîne, après l’un des trois possibles M, après D?C{0,3} avec un D et tous les trois possibles C ; ensuite L?X{0,3} avec un L et tous les trois possibles X, et V?I{0,3} avec un V, et tous les trois possibles I, et finalement la fin de la chaîne. MMMDCCCLXXXVIII est le nombre romain représentant 3888. C’est le nombre le plus long des nombres romains que vous pouvez rencontrer.
  4. Regardez attentivement. (J’ai l’impression que je fais un tour de magie. « Les enfants, regardez, je vais faire sortir un lapin de mon chapeau »). Ce motif reconnaît le début de la chaîne, après aucun des trois M possibles, après D?C{0,3} en ignorant D et en ne détectant aucun C des trois C possibles, après L?X{0,3} en ignorant L et ne détectant aucun des trois X possibles, après V?I{0,3} en ignorant V et détectant un I parmi les trois I possibles, et finalement la fin de la chaîne. Whoa !

Si vous avez suivi tout cela et l’avez compris dès la première fois, vous êtes mieux que moi. Maintenant, imaginez le fait de comprendre une expression régulière d’une autre personne, dans une fonction faisant partie d’un gros programme. Ou simplement, imaginez que vous reviendrez vers votre propre expression régulière quelques mois après. Je l’ai fait, et ce n’est pas du tout sympa.

Alors, allons explorer une nouvelle syntaxe permettant d’avoir une expression maintenable.

VI-C-2. Expressions régulières verbeuses

Au début vous étiez en train de manipuler ce que j’appelle des expressions régulières « compactes ». Comme vous l’avez vu, elles sont difficilement lisibles, et même si vous comprenez ce qui était fait, rien ne vous assure que dans six mois vous le comprendrez toujours. Ce dont vous aurez vraiment besoin c’est d’une documentation en ligne.

Python vous donne cette possibilité en utilisant ce que l’on appelle les expressions régulières détaillées « verbose ». Les expressions régulières détaillées se distinguent des expressions régulières compactes par deux choses :

  • tous les types d’espaces sont ignorés. Les espaces, tabulations et les retours chariots ne sont pas considérés comme des espaces, des tabulations et des retours chariot. Ils ne sont pas pris en considération du tout. (Pour reconnaître une espace dans une expression régulière détaillée, vous devez la déspécifier en la précédant d'un antislash.)

Les commentaires sont ignorés. Le commentaire dans l’expression régulière détaillée est exactement comme le commentaire dans le code Python : il commence par le caractère # et se termine à la fin de la ligne. Dans ce cas, le commentaire est inséré dans une chaîne multiligne au lieu d’un commentaire pour une ligne de code, mais cela fonctionne de la même manière.

Cela va être plus clair avec un exemple. Revenons sur l’expression régulière compacte avec laquelle vous avez déjà travaillé, et transformons-la en une expression régulière détaillée. L’exemple suivant explique comment le faire.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
>>>pattern = '''
    ^                   # début de la chaîne
M{0,3}              # milliers - 0 to 3 Ms
    (CM|CD|D?C{0,3})    # centaines - 900 (CM), 400 (CD), 0-300 (0 to 3 Cs),
                        #            or 500-800 (D, suivi de 0 à 3 Cs)
    (XC|XL|L?X{0,3})    # dizaines - 90 (XC), 40 (XL), 0-30 (0 à 3 Xs),
                        #        or 50-80 (L, suivi de 0 à 3 Xs)
    (IX|IV|V?I{0,3})    # unités - 9 (IX), 4 (IV), 0-3 (0 à 3 Is),
                        #        or 5-8 (V, suivi de 0 à 3 Is)
    $                   # fin de la chaîne
    '''
>>>re.search(pattern, 'M', re.VERBOSE)                 ①
<re.Match object; span=(0, 1), match='M'>
>>>re.search(pattern, 'MCMLXXXIX', re.VERBOSE)         ②<re.Match object; span=(0, 9), match='MCMLXXXIX'>
>>>re.search(pattern, 'MMMDCCCLXXXVIII', re.VERBOSE)   ③
<re.Match object; span=(0, 15), match='MMMDCCCLXXXVIII'>
>>>re.search(pattern, 'M')                             ④
>>>
  1. Le plus important à retenir avec les expressions régulières détaillées, c’est l’argument extra re.VERBOSE qui est une constante définie dans le module re indiquant que le motif doit être traité comme expression régulière détaillée. Comme vous pouvez le voir, le motif contient plusieurs espaces (qui sont toutes ignorées) et plusieurs commentaires (qui sont tous ignorés). Une fois que vous ignorez toutes les espaces et tous les commentaires, vous tombez exactement sur la même expression régulière que vous avez dans la section précédente, mais beaucoup plus lisible.
  2. Ce motif identifie le début de la chaîne et un parmi les trois caractères M optionnels, ensuite CM, ensuite L et trois sur les trois caractères X, ensuite IX, puis la fin de la chaîne.
  3. Ce motif identifie le début de la chaîne et trois sur les trois caractères M optionnels, ensuite D et trois sur les trois caractères C, L et trois sur les trois caractères X, V et trois sur les trois caractères I, puis la fin de la chaîne.
  4. Ce motif ne reconnaît rien. Pourquoi ? Parce que l’argument re.VERBOSE n’est pas passé, du coup la fonction re.search traite le motif comme une expression régulière compacte, en prenant en considération les espaces et les commentaires. Python ne détecte pas si l’expression régulière est détaillée systématiquement ou non. Par défaut, il considère toute expression régulière compacte sauf si vous spécifiez explicitement qu’elle est détaillée.

VI-C-3. Étude de cas : analyse des numéros de téléphone

\d reconnaît tous les numéros de (0–9), \D reconnaît tout sauf les numéros.

Avant vous étiez concentré sur l’identification du motif tout entier. Que le motif soit reconnu ou non. Mais les expressions régulières sont plus puissantes que cela. Quand une expression régulière est reconnue, vous pouvez récupérer des informations plus spécifiques. Vous pouvez savoir qu’est-ce qui a été reconnu et où.

Cet exemple vient d’un vrai problème vécu dans mon ancien boulot. Le problème de parcours d’un répertoire téléphonique américain. Le client souhaitait pouvoir entrer le numéro sous forme libre (en un seul champ), mais souhaitait ensuite stocker l’indicatif, la ligne réseau, le numéro et éventuellement une extension dans la base de données de la société. J’ai cherché sur Internet et trouvé un ensemble des expressions régulières supposées répondre à ce problème, mais aucune d’entre elles n’était efficace.

Ci-dessous les numéros de téléphone que j’ai besoin de gérer :

  • 800-555-1212
  • 800 555 1212
  • 800.555.1212
  • (800) 555-1212
  • 1-800-555-1212
  • 800-555-1212-1234
  • 800-555-1212x1234
  • 800-555-1212 ext. 1234
  • work 1-(800) 555.1212 #1234

Pas mal de choix ! Pour tous ces cas, je dois savoir que l’indicatif est 800, la ligne est 555 et le reste du numéro de téléphone est 1212. Pour ceux contenant une extension, je devais savoir que l’extension est 1234.

Nous allons travailler sur une solution pour parcourir un répertoire téléphonique. Cet exemple montre la première étape.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
>>>phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})$')  ①
>>>phonePattern.search('800-555-1212').groups()             ②
('800', '555', '1212')
>>>phonePattern.search('800-555-1212-1234')                 ③
>>>phonePattern.search('800-555-1212-1234').groups()        ④
Traceback (mostrecent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'groups'
  1. Il faut toujours lire les expressions régulières de gauche à droite. Ce motif reconnaît le début de la chaîne, ensuite (\d{3}). À quoi correspond \d{3} ? Bon, \d correspond à « tout caractère numérique » (0 à 9). Le {3} permet de « reconnaître exactement trois caractères numériques » ; c’est une alternative de la syntaxe {n,m} que vous avez déjà vue. Mettre tout entre parenthèses permet de « reconnaître exactement trois numériques, et ensuite les mémoriser sous forme d’un groupe que je reviendrai récupérer ensuite ». Ensuite un tiret et reconnaître exactement trois numériques. Ensuite un tiret et reconnaître exactement quatre numériques, puis reconnaître la fin de la chaîne.
  2. Pour accéder aux groupes dont l’expression régulière se souvient en parcourant la chaîne, utilisez la méthode groups() avec l’objet retourné par la méthode search(). Elle va retourner un tuple de groupes, peu importe où ils sont définis dans l’expression régulière. Dans ce cas vous avez trois groupes, le premier et le deuxième avec trois numériques et le dernier avec quatre numériques.
  3. Cette expression régulière ne répond pas à tous les groupes, car il ne gère pas le cas d’un numéro de téléphone avec une extension à la fin. Pour ceci, on doit étendre notre expression régulière.
  4. Et c’est pour cela qu’il ne faut jamais utiliser les méthodes search() et groups() ensemble. Si search() ne reconnaît rien, elle retourne None, et non un objet reconnu par l’expression régulière. Appeler None.groups() produit parfaitement une exception : None doesn’t have a groups() method. (Certes, c’est un peu moins évident quand vous obtiendrez cette erreur dans un code plus compliqué. Oui, là je parle vraiment de mon expérience.)
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
>>>phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})-(\d+)$')  ①
>>>phonePattern.search('800-555-1212-1234').groups()              ②
('800', '555', '1212', '1234')
>>>phonePattern.search('800 555 1212 1234')                       ③
>>>
>>>phonePattern.search('800-555-1212')                            ④
>>>
  1. Cette expression régulière est quasiment identique à la première. Comme avant, vous reconnaissez le début de la chaîne, ensuite mémorisez le groupe de trois caractères numériques, puis un tiret, mémorisez le groupe de trois caractères numériques, puis un tiret, et mémorisez un groupe de quatre caractères numériques. Ce qui est nouveau c’est que vous reconnaissez un autre tiret et, ensuite mémorisez un groupe de trois caractères numériques, puis la fin de la chaîne.
  2. Comme l’expression régulière définit quatre groupes, la méthode groups() retourne maintenant un tuple de quatre éléments.
  3. Malheureusement, cette expression régulière n’est pas non plus la réponse finale au problème, car elle suppose que les différentes parties du numéro du téléphone sont séparées par des tirets. Et si elles sont séparées par des espaces, des virgules ou des points ? Vous aurez besoin d’une solution plus générale afin de reconnaître plusieurs types de séparateurs.
  4. Oups ! Non seulement cette expression régulière ne fait pas ce que l’on souhaite, mais de plus, c’est un pas en arrière, parce que maintenant vous ne pouvez plus parcourir un numéro de téléphone ne contenant pas une extension. Ce n’est pas du tout ce que l’on veut faire ; si l’extension existe, on veut la reconnaître, mais si elle n’existe pas nous voulons toujours savoir les autres différentes parties du numéro de téléphone.

L’exemple suivant montre l’expression régulière pour gérer les séparateurs entre les autres différentes parties du numéro de téléphone.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
>>>phonePattern = re.compile(r'^(\d{3})\D+(\d{3})\D+(\d{4})\D+(\d+)$')  ①
>>>phonePattern.search('800 555 1212 1234').groups()  ②
('800', '555', '1212', '1234')
>>>phonePattern.search('800-555-1212-1234').groups()  ③
('800', '555', '1212', '1234')
>>>phonePattern.search('80055512121234')              ④
>>>
>>>phonePattern.search('800-555-1212')                ⑤
>>>
  1. Accrochez-vous. Vous reconnaissez le début de la chaîne, ensuite un groupe de trois numéros, ensuite \D+. Mais c’est quoi ce truc ? Bon, \D reconnaît tout caractère sauf un caractère numérique et + spécifie « 1 ou plus ». Du coup \D+ reconnaît un ou plusieurs caractères qui ne sont pas des numériques. C’est ce qu’il faut utiliser au lieu du tiret pour pouvoir reconnaître les différents types de séparateurs.
  2. Utiliser \D+ au lieu de - permet de reconnaître les numéros de téléphone dont les différentes parties sont séparées par une espace au lieu du tiret.
  3. Bien évidemment cela fonctionnera aussi avec les tirets.
  4. Malheureusement, ceci n’est toujours pas la réponse finale, parce que cela suppose qu’il y a forcément un séparateur. Qu’est-ce qu’il en serait si le numéro était saisi sans tirets ni espaces ?
  5. Oups ! Ceci ne résout pas le problème des extensions non plus. Maintenant on a deux problèmes, mais on peut pallier les deux avec la même technique.

L’exemple suivant montre l’expression régulière pour gérer les numéros de téléphone sans séparateurs.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
>>>phonePattern = re.compile(r'^(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  ①
>>>phonePattern.search('80055512121234').groups()      ②
('800', '555', '1212', '1234')
>>>phonePattern.search('800.555.1212 x1234').groups()  ③
('800', '555', '1212', '1234')
>>>phonePattern.search('800-555-1212').groups()        ④
('800', '555', '1212', '')
>>>phonePattern.search('800-555-1212').groups()           ⑤
>>>
  1. Le seul changement qui a été fait c’est d’avoir changé tous les + par *. Au lieu de \D+ entre les parties du numéro de téléphone, nous reconnaissons maintenant \D*. Vous vous rappelez que + veut dire « 1 ou plus » ? Bon, * veut dire « zéro ou plus ». Alors maintenant vous serez capable de parcourir les numéros de téléphone même quand ils ne contiennent pas de séparateurs.
  2. Mais voilà, cela fonctionne bien. Pourquoi ? Vous reconnaissez le début de la chaîne ensuite un groupe de trois numéros (800), puis zéro caractère non numérique, un groupe de trois numéros (555), zéro caractère non numérique, un groupe de quatre numéros (1212), zéro caractère non numérique, un groupe de quatre numéros (1234), et finalement la fin de la chaîne.
  3. D’autres combinaisons fonctionnent aussi : des points au lieu de tirets, et pour chacun d'eux une espace ou un x précédant une extension.
  4. Et finalement, vous avez résolu le problème des extensions ; elles sont de nouveau facultatives. Si on ne trouve pas d’extension, la méthode groups() retourne toujours un tuple de quatre éléments, mais le quatrième est tout simplement une chaîne vide.
  5. Je n’aime pas être le porteur de mauvaises nouvelles, mais vous n’avez pas encore fini. Quel est le problème ici ? Il y a un caractère de plus précédant l’indicatif. Mais l’expression régulière suppose que l’indicatif soit toujours le début de la chaîne. Pas de problème, vous pouvez utiliser la même technique du “zéro caractère non numérique ou plus” pour se passer du caractère précédant l’indicatif.

L’exemple suivant montre comment gérer le caractère précédant l’indicatif dans le numéro du téléphone.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
>>>phonePattern = re.compile(r'^\D*(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  ①
>>>phonePattern.search('(800)5551212 ext. 1234').groups()                  ②
('800', '555', '1212', '1234')
>>>phonePattern.search('800-555-1212').groups()                            ③
('800', '555', '1212', '')
>>>phonePattern.search('work 1-(800) 555.1212 #1234')                      ④
>>>
  1. C’est la même chose que l’exemple précédent, sauf que maintenant vous reconnaissez \D*, zéro ou plus d’un caractère non numérique, avant le premier groupe mémorisé (l’indicatif). Il faut noter que vous ne mémorisez pas ce caractère non numérique (il n’est pas mis entre parenthèses). Si vous le trouvez, vous allez l'ignorer et ensuite commencer à mémoriser l’indicatif une fois et vous y arrivez.
  2. Vous pouvez brillamment parcourir le numéro de téléphone, même s'il y a une parenthèse avant l’indicatif. (La parenthèse fermante qui vient après l’indicatif est déjà réglée, elle est traitée comme un séparateur non numérique et identifiée par le \D* après le premier groupe mémorisé.)
  3. Juste une vérification pour s’assurer que vous n’avez pas altéré quelque chose qui fonctionnait bien. Comme le premier caractère est totalement optionnel, cela reconnaît le début de la chaîne, ensuite zéro caractère non numérique, puis le groupe mémorisé de trois caractères (800), un caractère non numérique (la virgule), le groupe mémorisé de trois caractères (555), un caractère non numérique (la virgule), le groupe mémorisé de quatre caractères (1212), zéro caractère non numérique, le groupe mémorisé de zéro numéro, et finalement la fin de la chaîne.
  4. C’est là où les expressions régulières m’ont poussé à vouloir m’arracher les yeux. Pourquoi ce numéro de téléphone n’est pas reconnu ? Parce qu'il y a un 1 avant l’indicatif, mais vous avez supposé que tous les caractères au début allaient être des non numériques (\D*). Aargh !

Revenons dessus pour une seconde. Avant l’expression régulière reconnaissait tout depuis le début de la chaîne. Mais maintenant vous réalisez que la chaîne peut commencer par un nombre indéterminé de choses que vous devez ignorer. Mis à part essayer de les reconnaître tous, vous devez aussi pouvoir les ignorer, on va adopter une nouvelle approche : ne pas reconnaître le début de la chaîne explicitement. Le deuxième exemple montre cette approche.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
>>>phonePattern = re.compile(r'(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  ①
>>>phonePattern.search('work 1-(800) 555.1212 #1234').groups()         ②
('800', '555', '1212', '1234')
>>>phonePattern.search('800-555-1212').groups()                        ③
('800', '555', '1212', '')
>>>phonePattern.search('80055512121234').groups()                      ④
('800', '555', '1212', '1234')
  1. Il faut noter le manque du ^ au début de l’expression régulière. Vous ne reconnaissez plus le début de la chaîne. Il n’y a rien qui spécifie que vous devez reconnaître la totalité de votre entrée avec votre expression régulière. Le moteur de l’expression régulière fera l’effort d’identifier où se trouve le début de ce qu’il faudrait reconnaître et partir dessus.
  2. Maintenant vous pouvez bien parcourir le numéro de téléphone qui commence par un caractère spécial ou un chiffre, plus n’importe quel nombre et n’importe quel type de séparateurs utilisés entre chaque partie du numéro de téléphone.
  3. Un simple test. Cela fonctionne toujours.
  4. Cela marche aussi.

Voyez comment on peut rapidement perdre le contrôle d’une expression régulière. Jetez rapidement un coup d'œil sur toutes les versions précédentes. Pourriez-vous dire la différence entre la première et la suivante ?

Puisque vous comprenez encore la réponse finale (oui c’est la dernière réponse ; et si vous avez découvert un cas où cela ne fonctionne pas, je n’en veux rien savoir), on va réécrire l’expression régulière en mode détaillé, avant que vous n’oubliiez pourquoi vous avez pris les décisions que vous avez prises.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
>>>phonePattern = re.compile(r'''
                # n’identifie pas le début de la chaîne, les nombres peuvent démarrer n’importe où.
    (\d{3})     # le code indicatif est de 3 chiffres (e.g. '800')
    \D*         # le séparateur optionnel peut être tout non-numérique
    (\d{3})     # la ligne est de 3 chiffres (e.g. '555')
    \D*         # un séparateur optionnel
    (\d{4})     # le reste du numéro est 4 chiffres (e.g. '1212')
    \D*         # un séparateur optionnel
    (\d*)       # l’extension est optionnelle et peut être tout numérique 
    $           # la fin de la chaîne
    ''', re.VERBOSE)
>>>phonePattern.search('work 1-(800) 555.1212 #1234').groups()  ①
('800', '555', '1212', '1234')
>>>phonePattern.search('800-555-1212')                          ②
('800', '555', '1212', '')
  1. Mis à part qu’elle est étendue en plusieurs lignes, c’est exactement la même expression régulière, donc ce n’est pas étonnant qu’elle parcoure les mêmes entrées.
  2. Une dernière vérification, oui, cela fonctionne encore. Vous avez fini.

VI-C-4. Résumé

Ceci n’est qu’un minimum de ce que les expressions régulières peuvent faire. En d’autres termes, bien que vous pensiez les avoir complètement cernées, croyez-moi vous n’avez encore rien vu.

Maintenant vous vous êtes familiarisé avec les techniques suivantes :

  • ^ reconnaît le début de la chaîne ;
  • $ reconnaît la fin de la chaîne ;
  • \b reconnaît les limites d’un mot ;
  • \d reconnaît tout caractère numérique ;
  • \D reconnaît tout caractère non numérique ;
  • x? reconnaît un caractère x optionnel (autrement dit, il reconnaît x zéro ou une fois) ;
  • x* reconnaît x zéro fois ou plus ;
  • x+ reconnaît x une fois ou plus ;
  • x{n,m} reconnaît un caractère x au moins n fois, mais pas plus que m fois ;
  • (a|b|c) reconnaît exactement l’une des trois possibilités a, b ou c ;
  • (x) en général c’est un groupe mémorisé. Vous pouvez avoir la valeur de ce qui a été identifié en utilisant la méthode groups() de l’objet retourné par re.search.

Les expressions régulières sont extrêmement puissantes, mais elles ne sont pas la bonne solution pour chaque problème. Vous devez en apprendre suffisamment sur elles pour savoir quand elles sont appropriées, quand elles résoudront vos problèmes et quand elles en causeront plus qu'elles n'en résolvent.


précédentsommaire

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2019 Mark Pilgrim. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.