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

Plongez au coeur de Python ,De débutant à expert


précédentsommairesuivant

VII. Expressions régulières

Les expressions régulières sont un moyen puissant et standardisé de rechercher, remplacer et analyser du texte à l'aide de motifs complexes de caractères. Si vous avez utilisé les expressions régulières dans d'autres langages (comme Perl), vous pouvez sauter cette section et lire uniquement la présentation du module re pour avoir une vue d'ensemble des fonctions disponibles et de leurs arguments.

VII-A. Plonger

Les objets-chaîne Strings ont des méthodes pour rechercher (index, find et count), remplacer ((index, find et count) et analyser (split) mais elles sont limitées aux cas les plus simples. Les méthodes de recherche tentent de trouver une chaîne unique et prédéfinie et elles sont toujours sensibles à la casse. Pour faire une recherche non sensible à la casse sur une chaîne s, vous devez appeler s.lower() ou s.upper() et vous assurer que vos chaînes de recherche sont dans la casse correspondante. Les méthodes replace et split ont les mêmes restrictions.

Si ce que vous essayez de faire peut être accompli avec les fonctions de chaînes, utilisez-les. Elles sont rapides et faciles à comprendre et il y a beaucoup d'avantages à un code rapide, simple et lisible. Mais si vous vous rendez compte que vous utilisez un grand nombre de fonctions de chaînes différentes avec des instruction if pour les cas particulier, ou si vous les associez à des fonctions split et join et à des list comprehension de manière complexe et illisible, vous devez vous tourner vers les expressions régulières.

Bien que la syntaxe des expressions régulières soit compacte et différente du code ordinaire, le résultat peut être plus lisible qu'une solution à la main avec une longue séquence de fonctions de chaînes. Il y a même une manière d'inclure des commentaires dans les expressions régulières pour les documenter.

VII-B. Exemple : adresses postales

Cette série d'exemples est inspirée d'un problème réel que j'ai eu au cours de mon travail, l'extraction et la standardisation d'adresses postales exportées d'un ancien système avant de les importer dans un nouveau système (vous voyez, je n'invente rien, c'est réellement utile). L'exemple suivant montre comment j'ai abordé ce problème.

Exemple 7.1. Reconnaître la fin d'une chaîne

 
Sélectionnez
>>> s = '100 NORTH MAIN ROAD'
>>> s.replace('ROAD', 'RD.')               ***1***
'100 NORTH MAIN RD.'
>>> s = '100 NORTH BROAD ROAD'
>>> s.replace('ROAD', 'RD.')               ***2***
'100 NORTH BRD. RD.'
>>> s[:-4] + s[-4:].replace('ROAD', 'RD.') ***3***
'100 NORTH BROAD RD.'
>>> import re                              ***4***
>>> re.sub('ROAD$', 'RD.', s)              ***5*** ***6***
'100 NORTH BROAD RD.'

***1*** Mon but était de standardiser les adresses de manière à ce que 'ROAD' soit toujours abrégé en 'RD.'. Au premier abord, je pensais que ce serait assez simple pour utiliser uniquement la méthode de chaîne replace. Après tout, toutes les données étaient déjà en majuscules, donc les erreurs de casses ne seraient pas un problème. De plus, la chaîne de recherche, 'ROAD', était une constante. Pour cet exemple trompeusement simple, s.replace fonctionne effectivement.

***2*** Malheureusement, la vie est pleine de contre-exemples et je découvrais assez rapidemment celui-ci. Le problème ici est que 'ROAD' apparaît deux fois dans l'adresse, d'abord comme partie du nom de la rue 'BROAD' et ensuite comme mot isolé. La méthode replace trouve ces deux occurences et les remplace aveuglément, rendant l'adresse illisible.

***3*** Pour résoudre le problème des adresses comprenant plus d'une sous-chaîne 'ROAD', nous pourrions recourir à quelque chose de ce genre : ne rechercher et remplacer 'ROAD' que dans les 4 derniers caractères de l'adresse (s[-4:]) et ignorer le début de la chaîne (s[:-4]). Mais on voit bien que ça commence à être embrouillé. Par exemple, le motif dépend de la longueur de la chaîne que nous remplaçons (si nous remplaçons 'STREET' par 'ST.', nous devons écrire s[:-6] et s[-6:].replace(...)). Aimeriez-vous revenir à ce code dans six mois et devoir le débugger ? En ce qui me concerne, certainement pas.

***4*** Il est temps de recourir aux expressions régulières. En Python, toutes les fonctionalités en rapport aux expressions régulières sont contenues dans le module re.

***5*** Regardez le premier paramètre, 'ROAD$'. C'est une expression régulière très simple qui ne reconnaît 'ROAD' que s'il apparaît à la fin d'une chaîne. Le symbole $ signifie «fin de la chaîne» (il y a un caractère correspondant, l'accent circonflexe ^, qui signifie «début de la chaîne»).

***6*** A l'aide de la fonction re.sub, nous recherchons dans la chaîne s l'expression régulière 'ROAD$' et la remplaçons par 'RD.'. Cela correspond à ROAD à la fin de la chaîne s, mais ne correspond pas au ROAD faisant partie du mot BROAD, puisqu'il est au milieu de s.

En continuant mon travail de reformatage d'adresses, je decouvrais bientôt que le modèle précédent, reconnaître 'ROAD' à la fin de l'adresse, ne suffisait pas, car toutes les adresses n'incluaient pas d'identifiant pour la rue. Certaines finissaient simplement par le nom de la rue. La plupart du temps, je m'en sortais sans problème, mais si le nom de la rue était 'BROAD', alors l'expression régulière reconnaissait 'ROAD' à la fin de la chaîne dans le mot 'BROAD'. Ce n'était pas ce que je voulais.

Exemple 7.2. Reconnaître des mots entiers

 
Sélectionnez
>>> s = '100 BROAD'
>>> re.sub('ROAD$', 'RD.', s)
'100 BRD.'
>>> re.sub('\\bROAD$', 'RD.', s)  ***1***
'100 BROAD'
>>> re.sub(r'\bROAD$', 'RD.', s)  ***2***
'100 BROAD'
>>> s = '100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD$', 'RD.', s)  ***3***
'100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD\b', 'RD.', s) ***4***
'100 BROAD RD. APT 3'

***1*** Ce que je voulais vraiment était de reconnaître 'ROAD' quand il était à la fin de la chaîne et qu'il était un mot isolé, pas une partie de mot. Pour exprimer cela dans une expressions régulière, on utilise \b, qui signifie «une limite de mot doit apparaître ici». En Python, c'est rendu plus compliqué par le fait que le caractère '\', qui est le caractère d'échappement, doit lui-même être précédé du caractère d'échappement (c'est ce qui est parfois appelé la backslash plague et c'est une des raison pour lesquelles les expressions régulières sont plus faciles à utliser en Perl qu'en Python. Par contre, Perl mélange les expressions régulières et la syntaxe du langage, donc si vous avez un bogue, il peut être difficile de savoir si c'est une erreur dans la syntaxe ou dans l'expression régulière).

***2*** Pour éviter la backslash plague, vous pouvez utiliser ce qu'on appelle une chaîne brute, en préfixant la chaîne par la lettre r. Cela signale à Python que cette chaîne doit être traitée sans échappement, '\t' est un caractère de tabulation, mais r'\t' est réellement un caractère backslash \ suivi de la lettre t. Je vous conseille de toujours utiliser des chaînes brutes lorsque vous employez des expressions régulières, sinon cela devient confus très vite (et les expressions régulières peuvent devenir suffisament confuses par elles-mêmes).

***3*** *soupir* Malheureusement, je découvrais rapidement d'autres cas qui contredisaient mon raisonnement. Dans le cas présent, l'adresse contenait le mot isolé 'ROAD' mais il n'était pas à la fin de la chaîne, car l'adresse avait un numéro d'appartement après l'identifiant de la rue. Comme 'ROAD' n'était pas tout à la fin de la chaîne, il n'était pas identifié, donc l'appel de re.sub s'achèvait sans rien remplacer, j'obtenais en retour la chaîne d'origine, ce qui n'était pas le but recherché.

***4*** Pour résoudre ce problème, j'enlevais le caractère $et ajoutais un deuxième \b. L'expression régulière signifiait alors «reconnaître 'ROAD' lorsqu'il est un mot isolé, n'importe où dans la chaîne», que ce soit à la fin, au début ou quelque part au milieu.

VII-C. Exemple : chiffres romains

Vous avez certainement déjà vu des chiffres romains, par exemple dans Astérix (2)

En chiffres romains, il y a sept caractères qui sont répétés et combinés de différentes manières pour représenter des nombres.

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

Voici les règles générales pour construire des nombres romains :

  • Les caractères sont additifs. I est 1, II est 2 et III est 3. VI est 6 (littéralement «5 et 1»), VII est 7 et VIII est 8.
  • Les caractères en un (I, X, C, and M) peuvent être répétés jusqu'à trois fois. A 4, vous devez soustraire du prochain caractère en cinq. Vous ne pouvez pas représenter 4 par IIII, au lieu de ça il est représenté par IV («1 de moins que 5»). 40 s'écrit XL («10 de moins que 50»), 41 s'écrit XLI, 42 XLII, 43 XLIII et 44 XLIV («10 de moins que 50, puis 1 de moins que 5»).
  • De manière similaire, à 9, vous devez soustraire du prochain caractère en un : 8 est VIII mais 9 est IX («1 de moins que 10»), pas VIIII (puisque le caractère I ne peut être répété quatre fois). 90 est XC et 900 CM.
  • Les caractères en cinq ne peuvent être répétés. 10 est toujours représenté par X, jamais par VV. 100 est toujours C, jamais LL.

VII-C-1. Rechercher les milliers

Qu'est-ce qui serait nécessaire pour vérifier qu'une chaîne de caractères quelconque constitue des chiffres romains valides ? Nous allons opérer caractère par caractère. Puisque les chiffres romains sont toujours écrits du plus grand vers le plus petit, nous allons commencer par les plus grands : les milliers. Pour les nombres supérieurs à 1000, les milliers sont représentés par une série de caractères M.

Exemple 7.3. Rechercher les milliers
 
Sélectionnez
>>> import re
>>> pattern = '^M?M?M?$'       ***1***
>>> re.search(pattern, 'M')    ***2***
<SRE_Match object at 0106FB58>
>>> re.search(pattern, 'MM')   ***3***
<SRE_Match object at 0106C290>
>>> re.search(pattern, 'MMM')  ***4***
<SRE_Match object at 0106AA38>
>>> re.search(pattern, 'MMMM') ***5***
>>> re.search(pattern, '')     ***6***
<SRE_Match object at 0106F4A8>

***1*** Ce motif a trois parties :

  • ^ - reconnaît ce qui suit uniquement en début de chaîne. Si ce n'était pas spécfié, le motif reconnaîtrait les M où qu'ils soient, ce qui n'est pas ce que nous voulons. Nous voulons être sûrs que les M, s'il y en a dans la chaîne, sont à son début.
  • M? - reconnaît un M optionnel. Comme nous le répétons trois fois, nous reconnaissons 0 à 3 M se suivant.
  • $ - reconnaît ce qui précède uniquement à la fin de la chaîne. Lorsqu'il est combiné avec ^ en début de motif, cela signifie que le motif doit correspondre à la chaîne entière, sans autres caractères avant ou après les M.

***2*** L'essence du module re est la fonction search, qui prend une expression régulière (pattern) et une chaîne ('M') qu'elle va tenter de faire correspondre. Si une correspondance est trouvée, search retourne un objet ayant diverses méthodes permettant de décrire la correspondance, sinon, search retourne None, la valeur nulle de Python. Tout ce qui nous intéresse à ce stade est de savoir si le motif est reconnu, ce que nous pouvons dire rien qu'en regardant la valeur retournée par search. 'M' est reconnu par cette expression régulière car le premier M optionnel correspond et que le second et troisième M sont ignorés.

***3*** 'MM' est reconnu puisque les premier et deuxième M optionnels correspondent et que le troisième est ignoré.

***4*** 'MMM' est reconnu puisque les trois M correspondent.

***5*** 'MMMM' n'est pas reconnu. Les trois M correspondent, mais l'expression régulière précise la fin de chaîne (par le caractère $) et la chaîne ne s'arrête pas là (à cause du quatrième M). Donc search retourne None.

***6*** Un élément intéressant est qu'une chaîne vide est reconnue par l'expression régulière, puisque tous les M sont optionnels.

VII-C-2. Rechercher les centaines

Les centaines présentent plus de difficultés que les milliers car elles peuvent être exprimées de plusieurs manières mutuellement exclusives, en fonction de leur valeur.

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

Il y a donc quatre motifs possibles :

  • CM
  • CD
  • 0 à 3 C (0 si les centaines valent 0)
  • D, suivi de 0 à 3 C

Les deux derniers motifs peuvent être combinés en :

  • un D optionnel, suivi de 0 à 3 C

L'exemple suivant montre comment valider les centaines en chiffres romains.

Exemple 7.4. Rechercher les centaines
 
Sélectionnez
>>> import re
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)$' ***1***
>>> re.search(pattern, 'MCM')            ***2***
<SRE_Match object at 01070390>
>>> re.search(pattern, 'MD')             ***3***
<SRE_Match object at 01073A50>
>>> re.search(pattern, 'MMMCCC')         ***4***
<SRE_Match object at 010748A8>
>>> re.search(pattern, 'MCMC')           ***5***
>>> re.search(pattern, '')               ***6***
<SRE_Match object at 01071D98>

***1*** Ce motif commence de la même manière que le précédent, en vérifiant le début de chaîne (^), puis les milliers (M?M?M?). Ensuite vient la nouvelle partie, entre parenthèses, qui définit un ensemble de trois motifs mutuellement exclusifs séparés par des barres verticales : CM, CD, and D?C?C?C? (qui est un D optionnel suivi de 0 à 3 C optionnels). Le processeur d'expressions régulières teste chacun de ces motifs dans l'ordre (de gauche à droite), prend le premier qui correspond et ignore le reste.

***2*** 'MCM' est reconnu car le premier M correspond, que le second et troisième M sont ignorés et que CM correspond (et donc les motifs CD et D?C?C?C? ne sont même pas examinés). MCM est la représentation de 1900.

***3*** 'MD' est reconnu car le premier M correspond, les deuxième et troisième M sont ignorés et que le motif D?C?C?C? reconnaît D (chacun des trois C est optionnel et est ignoré). MD est la représentation de 1500.

***4*** 'MMMCCC' est reconnu car les trois M correspondent et que le motif D?C?C?C? reconnaît CCC (le D est optionnel et est ignoré). MMMCCC est la représentation de 3300.

***5*** 'MCMC' n'est pas reconnu. Le premier M correspond, les deuxième et troisième M sont ignorés et le CM correspond, mais le $ ne correspond pas car nous ne sommes pas encore à la fin de la chaîne (il nous reste le caractère C à évaluer). Le C ne correspond pas comme partie du motif D?C?C?C? car le motif CM a déja été reconnu et qu'ils sont mutuellement exclusifs.

***6*** Fait intéressant, une chaîne vide est toujours reconnue par ce motif, car tous les M sont optionnels et sont ignorés et que la chaîne vide est reconnue par le motif D?C?C?C? dans lequel tous les caractères sont optionnels et sont ignorés.

Ouf ! Vous voyez à quel point les expressions régulières peuvent devenir compliquées ? Et nous n'avons vu que les milliers et les centaines. Heureusement, si vous avez suivi jusque là, les dizaines sont relativement simples puisqu'elles suivent exactement le même motif. Mais continuons en examinant une autre manière d'exprimer ce motif.

VII-D. Utilisation de la syntaxe {n,m}

Dans la section précédente, nous avons vu un motif dans lequel le même caractère pouvait être répété jusqu'à trois fois. Il y a une autre manière d'exprimer cela dans les expressions régulière, que certaines personnes trouvent plus lisible. D'abord, revenons sur la méthode que nous avons utilisé dans l'exemple précédent.

Exemple 7.5. L'ancienne méthode : chaque caractère est optionnel

 
Sélectionnez
>>> import re
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'M')    ***1***
<_sre.SRE_Match object at 0x008EE090>
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'MM')   ***2***
<_sre.SRE_Match object at 0x008EEB48>
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'MMM')  ***3***
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, 'MMMM') ***4***
>>>

***1*** Cette chaîne est reconnue : le motif reconnaît le début de la chaîne, puis le premier M optionnel, mais pas de second ni de troisième M (ce qui est correct puisqu'ils sont optionnels), puis la fin de la chaîne.

***2*** Le motif reconnaît le début de la chaîne, puis le premier et le second M optionnels, mais pas de troisième M (ce qui est correct puisqu'il est optionnel), puis la fin de la chaîne.

***3*** Le motif reconnaît le début de la chaîne, puis les trois M optionnels, puis la fin de la chaîne.

***4*** Le motif reconnaît le début de la chaîne, puis les trois M optionnels, mais pas la fin de la chaîne (puisqu'il reste un M), la chaîne n'est donc pas reconnue et None est retourné.

Exemple 7.6. La nouvelle méthode : de n à m

 
Sélectionnez
>>> pattern = '^M{0,3}$'       ***1***
>>> re.search(pattern, 'M')    ***2***
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MM')   ***3***
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, 'MMM')  ***4***
<_sre.SRE_Match object at 0x008EEDA8>
>>> re.search(pattern, 'MMMM') ***5***
>>>

***1*** Ce motifie signifie : «reconnaître le début de la chaîne, puis de zéro à trois caractères M, puis la fin de la chaîne.» Le 0 et le 3 peuvent être n'importe quel nombre, si nous voulons reconnaître au moins un, mais pas plus de trois caractères M, nous pouvons écrire M{1,3}.

***2*** Le motif reconnaît le début de la chaîne, puis un M sur trois possibles, puis la fin de la chaîne.

***3*** Le motif reconnaît le début de la chaîne, puis deux M sur trois possibles, puis la fin de la chaîne.

***4*** Le motif reconnaît le début de la chaîne, puis trois M sur trois possibles, puis la fin de la chaîne.

***5*** Le motif reconnaît le début de la chaîne, puis trois M sur trois possibles, puis ne reconnaît pas la fin de la chaîne. L'expression régulière permet jusqu'à trois caractères M avant la fin de la chaîne, mais il y en a quatre, donc la chaîne n'est pas reconnue et None est retourné.

Il n'y a aucun moyen de déterminer par un programme que deux expressions régulières sont équivalentes. Le mieux que vous puissiez faire est d'écrire de nombreux cas de test pour vérifier que leur comportements sont identiques pour les entrées pertinentes. Nous discuterons plus en détail l'écriture de cas de tests plus loin dans le livre.

VII-D-3. Rechercher les dizaines et les unités

Maintenant, nous allons étendre l'expression régulière pour prendre en compte les dizaines et les unités. L'exemple suivant montre la recherche des dizaines.

Exemple 7.7. Rechercher les dizaines
 
Sélectionnez
>>> pattern = '^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$'
>>> re.search(pattern, 'MCMXL')    ***1***
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCML')     ***2***
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLX')    ***3***
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXX')  ***4***
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXXX') ***5***
>>>

***1*** Le motif reconnaît le début de la chaîne, le premier M optionnel, puis CM, puis XL, puis la fin de la chaîne. Rappelez-vous que la syntaxe (A|B|C) signifie «reconnaître un seul parmi A, B et C». XL est reconnu, donc XC et L?X?X?X? sont ignorés, puis la find de la chaîne est reconnue. MCML est la représentation en chiffres romains de 1940.

***2*** Le motif reconnaît le début de la chaîne, le premier M optionnel, puis CM, puis L?X?X?X?. Pour L?X?X?X?, il reconnaît L et saute les trois caractères X optionnels. Il reconnaît ensuite la fin de la chaîne. MCML est la représentation en chiffres romains de 1950.

***3*** Le motif reconnaît le début de la chaîne, le premier M optionnel, puis CM, puis le L optionnel et le premier X optionnel, saute les trois caractères X optionnels, puis reconnaît la fin de la chaîne. MCMLX est la représentation en chiffres romains de 1960.

***4*** Le motif reconnaît le début de la chaîne, le premier M optionnel, puis CM, puis le L optionnel et les trois caractères X, puis la fin de la chaîne. MCMLXXX est la représentation en chiffres romains de 1980.

***5*** Le motif reconnaît le début de la chaîne, le premier M optionnel, puis CM, puis le L optionnel et les trois caractères X, puis ne reconnaît pas la fin de la chaîne puisqu'il y a encore un X non pris en charge. Donc la chaîne n'est pas reconnue et None est retourné. MCMLXXXX n'est pas un représentation en chiffres romains valide.

L'expression pour les unités suit le même motif. Je vous épargne les détails et ne vous montre que le résultat final.

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

A quoi est-ce que ça resemble en utilisant la syntaxe alternative avec {n,m} ? L'exemple suivant montre la nouvelle syntaxe.

Exemple 7.8. Validation des chiffres romains avec {n,m}
 
Sélectionnez
>>> pattern = '^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'
>>> re.search(pattern, 'MDLV')             ***1***
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMDCLXVI')         ***2***
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMMMDCCCLXXXVIII') ***3***
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'I')                ***4***
<_sre.SRE_Match object at 0x008EEB48>

***1*** Le motif reconnaît le début de la chaîne, puis un sur un maximum de quatre caractères M, puis D?C{0,3}. Pour cette expression il reconnaît le D optionnel et zéro sur un maximum de trois caractères C. Ensuite il reconnaît L?X{0,3} avec le L optionnel et zéro sur un maximum de trois caractères X. Puis il reconnaît V?I{0,3} avec le V optionnel et zéro sur un maximum de trois caractères I, puis la fin de la chaîne. MDLV est la représentation en chiffres romains de 1555.

***2*** Le motif reconnaît le début de la chaîne, puis deux sur un maximum de quatre caractères M, puis D?C{0,3} avec un D et un sur un maximum de trois caractères C. Ensuite il reconnaît L?X{0,3} avec un L et un sur un maximum de trois caractères X. Puis il reconnaît V?I{0,3} avec un V and et un sur un maximum de trois caractères I, puis la fin de la chaîne. MMDCLXVI est la représentation en chiffres romains de 2666.

***3*** Le motif reconnaît le début de la chaîne, puis quatre sur un maximum de quatre caractères M, puis D?C{0,3} avec un D et trois sur un maximum de trois caractères C. Ensuite il reconnaît L?X{0,3} avec un L et trois sur un maximum de trois caractères X. Puis il reconnaît V?I{0,3} avec un V et trois sur un maximum de trois caractères I, puis la fin de la chaîne. MMMMDCCCLXXXVIII est la représentation en chiffres romains de 3888 et c'est le chiffre romain le plus long que vous pouvez écrire sans syntaxe étendue.

***4*** Regardez bien. Le motif reconnaît le début de la chaîne, puis zéro sur un maximum de quatre M, puis D?C{0,3} en sautant le D optionnel et zéro sur un maximum de trois C. Ensuite il reconnaît L?X{0,3} en sautant le L optionnel et zéro sur un maximum de trois X. Puis il reconnaît V?I{0,3} en sautant le V optionnel et un sur un maximum de trois I, puis la fin de la chaîne.

Si vous avez suivi tout cela et que vous l'avez compris du premier coup, vous vous en sortez mieux que moi au début. Maintenant, imaginez devoir comprendre une expression régulière écrite par quelqu'un d'autre au mileu d'un fonction critique d'un programme de grande taille. Ou imaginez simplement de devoir revenir sur une de vos propres expressions régulières quelques mois plus tard. Je l'ai fait et ce n'est pas une partie de plaisir.

Dans la prochaine section vous explorerez une syntaxe alternative qui rendra possible la maintenance de vos expressions régulières.

VII-E. Expressions régulières détaillées

Jusqu'à maintenant, vous n'avez vu que ce que j'appellerais des expressions régulières «compactes». Comme vous l'avez vu, elles sont difficiles à lire et même si vous comprenez ce qu'une d'entre elles fait, rien n'assure que vous pourrez la comprendre dans six mois. Ce qu'il faut, c'est une documentation intégrée.

Python fournit pour cela les expressions régulières détaillées (verbose regular expressions). Une expression régulière détaillée diffère d'une expression régulière compacte de deux manières :

  • Les espaces sont ignorés. Les espaces, tabulations et retours chariot ne sont pas reconnus comme espaces, tabulations et retours chariot. Il ne sont pas reconnus du tout (si vous voulez reconnaître un espace dans une expression régulière détaillée, vous devez le faire précéder d'un caractère d'échapement «\»).
  • Les commentaires sont ignorés. Un commentaire dans une expression régulière détaillée est comme un commentaire dans du code Python : il commence par un caractère # et se poursuit jusqu'à la fin de la ligne. Dans le cas présent, c'est un commentaire à l'intérieur d'une chaîne de caractères multi-lignes plutôt que dans du code source, mais cela fonctionne de la même manière.

Cela sera plus clair avec un exemple. Revenons à l'expression régulière compacte avec laquelle nous avons travaillé et transformons-la en une expression régulière détaillée. L'exemple suivant montre comment.

Exemple 7.9. Expressions régulières intégrant des commentaires

 
Sélectionnez
>>> pattern = """
    ^                   # beginning of string
    M{0,4}              # thousands - 0 to 4 M's
    (CM|CD|D?C{0,3})    # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 C's),
                        #            or 500-800 (D, followed by 0 to 3 C's)
    (XC|XL|L?X{0,3})    # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 X's),
                        #        or 50-80 (L, followed by 0 to 3 X's)
    (IX|IV|V?I{0,3})    # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 I's),
                        #        or 5-8 (V, followed by 0 to 3 I's)
    $                   # end of string
    """
>>> re.search(pattern, 'M', re.VERBOSE)                ***1***
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXXIX', re.VERBOSE)        ***2***
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMMMDCCCLXXXVIII', re.VERBOSE) ***3***
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'M')                            ***4***

***1*** La chose la plus importante à se rappeler lorsqu'on utilise des expressions régulières détaillées est qu'il faut passer un argument supplémentaire : re.VERBOSE est une constante définie dans le module re qui signale que le motif doit être traité comme une expresion régulière détaillée. Comme vous le voyez, ce motif comprend beaucoup d'espaces (qui sont tous ignorés) et plusieurs commentaires (qui sont tous ignorés). Une fois enlevés les espaces et les commentaires, on obtient exactement la même expression régulière que nous avons vu à la section précédente, mais elle est beaucoup plus lisible.

***2*** Le motif reconnaît le début de la chaîne, puis un sur un maximum de quatre M, puis CM, puis L et trois sur un maximum de trois X. Ensuite IX et la fin de la chaîne.

***3*** Le motif reconnaît le début de la chaîne, puis quatre sur un maximum de quatre M, puis D et trois sur un maximum de trois C. Ensuite L et trois sur un maximum de trois X, puis V et trois sur un maximum de trois I et la fin de la chaîne.

***4*** Rien n'est reconnu. Pourquoi ? Parce que le drapeau re.VERBOSE n'est pas mis et donc la fonction re.search traite le motif comme une expression régulière compacte, dans laquelle les espaces et les commentaires sont pris en compte. Python ne peut pas savoir si une expression régulière est détailée ou non. Python considère que chaque expression régulière est compacte, à moins que vous ne spécifiez qu'elle est détaillée.

VII-F. Etude de cas : reconnaissance de numéros de téléphone

Jusqu'ici nous nous sommes concentrés sur la reconnaissance de motifs complets, le motif est reconnu ou non. Mais les expressions régulières sont beaucoup plus puissantes que cela. Lorsqu'une expression régulière reconnaît un motif, nous pouvons sélectionner certaines parties du motif. Nous pouvons savoir ce qui a été reconnu et à quel endroit.

Cet exemple est tiré d'un autre problème réel que j'ai eu au cours de mon travail précédent. Le problème : la reconnaissance de numéros de téléphone au Etats-Unis. Le client voulait que la saisie se fasse librement (dans un champ unique), mais voulait stocker le code régional, l'indicatif, le numéro et une extension optionnelle séparément dans la base de données. Je parcourais le Web et trouvais de nombreux exemples d'expressions régulières qui avaient pour but de faire cela, mais aucune n'était assez souple.

Voici les numéros de téléphone qu'il fallait que j'accepte :

  • 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

Quelle diversité ! Dans chacun de ces cas, je devais savoir que le code régional était 800, l'indicatif 555 et le reste du numéro 1212. Pour les numéros avec extension, je devais savoir que celle-ci était 1234.

Nous allons développer une solution pour la reconnaissance des numéros de téléphone. Cet exemple montre la première étape.

Exemple 7.10. Trouver des numéros

 
Sélectionnez
>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})$') ***1***
>>> phonePattern.search('800-555-1212').groups()            ***2***
('800', '555', '1212')
>>> phonePattern.search('800-555-1212-1234')                ***3***
>>>

***1*** Lisez toujours les expressions régulières de la gauche vers la droite. Celle-ci reconnaît le début de la chaîne, puis (\d{3}). Que veut dire ce \d{3}? Le {3} signifie «reconnaître exactement trois chiffres», c'est une variante de la syntaxe {n,m} que nous avons vu plus haut. \d signifie «n'importe quel chiffre» (de 0 à 9). En le mettant entre parenthèses, nous disons «reconnais exactement trois chiffres, puis identifie-les comme un groupe que je peux rappeler plus tard». Ensuite, le motif reconnaît un tiret, puis un autre groupe de trois chiffres, un autre tiret, un groupe de quatre chiffres et la fin de la chaîne.

***2*** Pour accéder aux groupes que l'expression régulière à identifiés, utilisez la méthode groups() de l'objet que la fonction search retourne. Elle retournera un tuple du nombre de groupes définis dans l'expression régulières. Dans ce cas, nous avons défini trois groupes, deux de trois chiffres et un de quatre.

***3*** Cette expression régulière n'est pas la réponse finale car elle ne prend pas en compte les numéros de téléphone avec une extension à la fin. Pour cela, nous allons devoir la modifier.

Exemple 7.11. Trouver l'extension

 
Sélectionnez
					>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})-(\d+)$') ***1***
					>>> phonePattern.search('800-555-1212-1234').groups()             ***2***
					('800', '555', '1212', '1234')
					>>> phonePattern.search('800 555 1212 1234')                      ***3***
					>>> 
					>>> phonePattern.search('800-555-1212')                           ***4***
					>>>

***1*** Cette expression régulière est pratiquement identique à la précédente. Elle reconnaît le début de la chaîne, puis un groupe identifié de trois chiffres, puis un tiret, puis un groupe identifié de trois chiffres, puis un tiret, puis un groupe identifié de quatre chiffres. Ce qui est nouveau, c'est qu'elle reconnaît ensuite un autre tiret puis un groupe identifié de un chiffre ou plus, puis la fin de la chaîne.
***2*** La méthode groups() retourne maintenant un tuple de quatre éléments, puisque l'expression régulière définit quatre groupe à identifier.
***3*** Malheureusement cette expression régulière n'est pas la réponse finale non plus, puisqu'elle considère que les différentes parties du numéro de téléphone sont séparées par des tirets. Et si elles étaient séparées par des espaces, des virgules ou des points ? Il nous faut une solution plus générale pour identifier différents types de séparateurs.
***4*** Non seulement cette expression régulière ne fait pas tout ce que nous voulions, elle est en fait un pas en arrière puisqu'elle ne peut pas reconnaître de numéros de téléphone sans extension. Ce n'est pas du tout ce que nous voulions, si l'extension est présente, nous voulons la connaître, mais si elle ne l'est pas, nous voulons tout de même connaître les différentes parties du numéro.

L'exemple suivant montre l'expression régulière qui reconnaît les séparateurs entre les différentes parties d'un numéro de téléphone.

Exemple 7.12. Reconnaissance des séparateurs

 
Sélectionnez
>>> phonePattern = re.compile(r'^(\d{3})\D+(\d{3})\D+(\d{4})\D+(\d+)$') ***1***
>>> phonePattern.search('800 555 1212 1234').groups()                   ***2***
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212-1234').groups()                   ***3***
('800', '555', '1212', '1234')
>>> phonePattern.search('80055512121234')                               ***4***
>>> 
>>> phonePattern.search('800-555-1212')                                 ***5***
>>>

***1*** Faites bien attention. Nous reconnaissons le début de la chaîne, puis un groupe de trois chiffres, puis \D+. Qu'est-ce que c'est que ça ? Et bien, \D reconnaît n'importe quel caractère sauf un chiffre et + signifie «un ou plus». Donc \D+ reconnaît un ou plusieurs caractères n'étant pas des chiffres. C'est ce que nous utilisons pour essayer de reconnaitre les séparateurs.

***2*** Utiliser \D+ au lieu de - nous permet de reconnaître des numéros de téléphone dont les différentes parties sont séparées par des espaces au lieu de tirets.

***3*** Bien sûr, les numéro de téléphone séparés par des tirets sont toujours reconnus.

***4*** Malheureusement ce n'est pas encore la réponse définitive car elle suppose qu'il y a bien un séparateur. Et si le numéro est saisi sans espaces ni tirets ?

***4*** Le problème de l'extension optionnelle n'a toujours pas été réglé. Maintenant nous avons deux problèmes, mais nous pouvons les régler tous les deux grâce à la même technique.

L'exemple suivant montre l'expression régulière qui reconnaît les numéros de téléphone sans séparateurs.

Exemple 7.13. Reconnaissance des numéros de téléphone sans séparateurs

 
Sélectionnez
>>> phonePattern = re.compile(r'^(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$') ***1***
>>> phonePattern.search('80055512121234').groups()                      ***2***
('800', '555', '1212', '1234')
>>> phonePattern.search('800.555.1212 x1234').groups()                  ***3***
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()                        ***4***
('800', '555', '1212', '')
>>> phonePattern.search('(800)5551212 x1234')                           ***5***
>>>

***1*** La seule modification depuis la dernière étape est de remplacer le + par un *. Au lieu de \D+ entre les différentes parties du numéro de téléphone, nous avons maintenant \D*. Vous vous rappelez que + signifie «un ou plus» et bien * signifie «zéro ou plus». Nous devrions donc pouvoir reconnaître des numéros de téléphone qui n'ont pas de séparateur du tout.

***2*** Et ça marche. Nous avons reconnu le début de la chaîne, puis un groupe identifié de trois chiffres (800), puis zéro caractères non numériques, puis un groupe identifié de trois caractères (555), puis zéro caractères non numériques, puis un groupe identifié de quatre caractères (1212), puis zéro caractères non numériques, puis un groupe identifié d'un nombre quelconque de caractères (1234), puis la fin de la chaîne.

***3*** D'autre variantes marchent également maintenant : des points à la place des tirets et un espace et un x avant l'extension.

***4*** Finalement, nous avons trouvé une solution à notre problème, les extension sont vraiment optionnelles. Si aucune extension n'est trouvée la méthode groups() retourne un tuple de quatre éléments, mais le quatrième élément est simplement une chaîne vide.

***5*** La mauvaise nouvelle, c'est que nous n'avons pas terminé. Il y a un caractère supplémentaire avant le code régional, mais l'expression régulière suppose que le code régional est la première chose au début de la chaîne. Bien sûr, nous pouvons utiliser la même technique «zéro ou plus caractères non-numériques» pour sauter les caractères situés avant le code régional.

L'exemple suivant montre comment prendre en compte les caractères au début des numéros de téléphone.

Exemple 7.14. Reconnaissance des caractères de début

 
Sélectionnez
>>> phonePattern = re.compile(r'^\D*(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$') ***1***
>>> phonePattern.search('(800)5551212 ext. 1234').groups()                 ***2***
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()                           ***3***
('800', '555', '1212', '')
>>> phonePattern.search('work 1-(800) 555.1212 #1234')                     ***4***
>>>

***1*** C'est la même chose que dans l'exemple précédent, sauf que nous reconnaissons \D*, c'est à dire zéro ou plus caractères non numériques, avant le premier groupe identifié (le code régional). Notez que nous n'identifions pas ces caractères non numériques (ils ne sont pas entre parenthèses). Si ils sont présents, nous les passons simplement et commençont à identifier le code régional quand nous y arrivons.

***2*** Le numéro de téléphone est correctement reconnu, même avec la parenthèse ouvrante devant le code régional (la parenthèse fermante était déjà prise en compte, elle est considérée comme un séparateur non-numérique et reconnue par le \D* suivant le premier groupe identifié).

***3*** Une simple vérification de cohérence pour nous assurer que nous n'avons rien endommagé de ce qui fonctionnait déjà. Puisque les caractères de début sont entièrement optionnels, le début de la chaîne est reconnu, puis zéro caractères non-numériques, puis un groupe identifié de trois caractères (800), puis un caractère non-numérique (le tiret), puis un groupe identifié de trois caractères (555), puis un caractère non-numérique (le tiret), puis un groupe identifié de quatre caractères (1212), puis zéro caractères non-numériques, puis un groupe identifié de zéro caractères numériques, puis la fin de la chaîne.

***4*** Mais il y a encore un problème. Pourquoi ce numéro ne fonctionne-t-il pas ? Parce qu'il y a un 1 avant le code régional et que nous avons considéré que tous les caractères de début sont non-numériques (\D*).

Faisons le point. Jusqu'à présent, nos expressions régulières commençaient toujours la reconnaissance en début de chaîne, mais maintenant nous voyons qu'il peut y avoir un nombre indeterminé de caractères à ignorer en début de chaîne. Au lieu d'essayer de les identifier pour les sauter, essayons une approche différente : ne pas reconnaître explicitemment le début de la chaîne. C'est ce que nous verrons dans l'exemple suivant.

Exemple 7.15. Un numéro de téléphone, où qu'il soit

 
Sélectionnez
>>> phonePattern = re.compile(r'(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$') ***1***
>>> phonePattern.search('work 1-(800) 555.1212 #1234').groups()        ***2***
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212')                                ***3***
('800', '555', '1212', '')
>>> phonePattern.search('80055512121234')                              ***4***
('800', '555', '1212', '1234')

***1*** Notez l'absence de ^ dans cette expression régulière. Nous ne reconnaissons plus le début de la chaîne. Rien ne nous oblige à reconnaître la chaîne entière, le moteur d'expressions régulières se débrouillera pour trouver où commence la reconnaissance.

***2*** Maintenant, nous pouvons reconnaître un numéro de téléphone précédé de caractères et de chiffres et segmenté par des séparateurs de tout type et de toute taille.

***3*** Contrôle de cohérence, ça fonctionne toujours.

***4*** Cela aussi fonctionne toujours.

Vous avez pu voir comment les expressions régulières peuvent rapidemment échapper à tout contrôle. Parcourez les différentes versions de notre expression régulière, pouvez-vous dire quelles différences les séparent ?

Tant que nous comprenons encore la version finale (et c'est bien la version finale, si vous découvrez un cas qu'elle n'est pas capable de traiter, je ne veux pas en entendre parler), écrivons-la sous la forme d'une expression régulière détaillée avant d'oublier les choix que nous avons fait.

Exemple 7.16. Reconnaissance des numéros de téléphone (version finale)

 
Sélectionnez
>>> phonePattern = re.compile(r'''
                # don't match beginning of string, number can start anywhere
    (\d{3})     # area code is 3 digits (e.g. '800')
    \D*         # optional separator is any number of non-digits
    (\d{3})     # trunk is 3 digits (e.g. '555')
    \D*         # optional separator
    (\d{4})     # rest of number is 4 digits (e.g. '1212')
    \D*         # optional separator
    (\d*)       # extension is optional and can be any number of digits
    $           # end of string
    ''', re.VERBOSE)
>>> phonePattern.search('work 1-(800) 555.1212 #1234').groups()        ***1***
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212')                                ***2***
('800', '555', '1212', '')

***1*** Mis à part le fait qu'elle est distribuée sur plusieurs lignes, c'est exactement la même expression régulière qu'à la dernière étape, il n'est donc pas étonnant qu'elle reconnaisse les mêmes entrées de manière identique.

***2*** Vérification de cohérence finale. Oui, ça marche toujours, nous avons terminé.

Pour en savoir plus sur les expressions régulières

VII-G. Résumé

Nous n'avons vu que la pointe de la partie émergée de l'iceberg des possibilités offertes par les expressions régulières. En d'autres termes, même si vous vous sentez totalement dépassé, vous n'avez encore rien vu.

Vous devez maintenant être familiarisé avec les techniques suivantes :

  • ^ reconnaît le début d'une chaîne.
  • $ reconnaît la fin d'une chaîne.
  • \b reconnaît la limite d'un mot.
  • \d reconnaît un chiffre.
  • \D reconnaît un caractère non-numérique.
  • x? reconnaît un caractère x optionnel (autrement dit, il reconnaît un x zéro ou une fois).
  • x* reconnaît zéro ou plus x.
  • x+ reconnaît un ou plusieurs x.
  • x{n,m} reconnaît un caractère x au moins n fois, mais pas plus de m fois.
  • (a|b|c) reconnaît soit a soit b soit c.
  • (x) en général est un groupe identifié. Vous pouvez obtenir la valeur de ce qui a été reconnu à l'aide de 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 solution correcte pour chaque problème. Vous devriez en apprendre assez sur elles pour savoir quand elles sont appropriées, quand elle peuvent résoudre votre problème et quand elles causent plus de problèmes qu'elles n'en résolvent.

Certaines personnes, face à un problème, se disent «je sais, je vais utiliser une expression régulière.» Maintenant elles ont deux problèmes. --Jamie Zawinski, dans comp.emacs.xemacs


précédentsommairesuivant
Note du traducteur : Je suppose que cet exemple suffit pour les lecteurs francophones.