7.2. 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 = '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 = '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.