|
Cours Python avec exercices écrit par Christophe Darmangeat dans le cadre du M2 PiSE |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Les expressions régulièresQue signifie ce terme un peu abscons ? Il désigne, de manière générale, un ensemble de « jokers » pouvant servir à vérifier si une donnée correspond à une certaine forme. Tout bêtement, si c'est un nom propre, commence-t-il effectivement par une majuscule ? Si c'est un code postal, comprend-il effectivement cinq chiffres ? Si c'est un mot de passe, fait-il au minimum 12 caractères, dont au moins une majuscule et un chiffre ? Etc. Les expressions régulières ne sont absolument pas une particularité spécifique à Python : on les retrouve dans beaucoup de langages ou de codages informatiques – les plus anciens se souviennent, émus, du ms-dos, de ses * et de ses ? pour désigner des noms de fichiers. On peut même en trouver quelques équivalents (rudimentaires) dans d'autres contextes : ainsi, au Scrabble, où le jeton vierge (joker) remplace n'importe quelle lettre de l'alphabet. La seconde chose est que comme très souvent en informatique, le principe de base est extrêmement simple, mais en combinant ces éléments simples, on arrive assez vite à des problèmes d'une complexité redoutable. Avertissement : quand on travaille avec les expressions régulières, il faut toujours veiller à deux possibilités d'erreurs : la première, à laquelle on pense en premier, est celle de l'expression qui « n'attrape pas » toutes les informations qu'elle est censée coder. Mais il ne faut jamais perdre de vue l'erreur inverse : celle de l'expression qui « attrape » des informations qui ne sont pas pertinentes. On prendra donc toujours soin de tester ses expressions régulières par rapport à ces deux erreurs possibles. Avertissement bis : faut-il le préciser, en matière d'erreurs, il est toujours possible d'avoir « fromage et dessert », et ainsi d'écrire une expression qui parvient à la fois à ne pas récupérer toutes les informations pertinentes et à récuperer certaines informations qui ne le sont pas... Premiers élémentsPour jouer avec les expression régulières, il est indispensable d'utiliser la bibliothèque adéquate, à savoir re (comme regular expression) : import re Cette bibliothèque offre diverses méthodes. Les deux principales sont search et findall. Search renvoie un résultat booléen indiquant si l'expression est présente ou non dans un texte donné (en fait, search renvoie un objet qui possède différentes propriétés indiquant le résultat de la recherche, mais qui peut être utilisé comme un simple booléen : c'est ce que nous ferons ici). Findall renvoie, sous forme de liste, toutes les occurrences de l'expression dans le texte. Ainsi, dans une version très basique qui n'utilise pas les epressions régulières, si l'on écrit : toto = re.search("monsieur", "Bonjour monsieur") toto vaudra True tandis que tutu vaudra False. Et si l'on écrit : tata = re.findall("on", "Bonjour monsieur") Le contenu de tata sera ["on", "on"] dans la mesure où ces deux caractères apparaissent deux fois dans la chaîne dans laquelle on les recherche. Naturellement, sans expression régulière, tout ceci n'est pas très intéressant... mais cela va rapidement le devenir, dans la mesure où il est possible d'utiliser des « jokers » qui vont tout changer et qui, bien maniés, vont donner à ces instructions une incroyable puissance. Les bases
Les quantificateurs
Deux raffinements
Autres utilisationsLes expressions régulières ne sont pas uniquement utilisables lorsqu'il s'agit de récupérer de l'information, comme on l'a fait avec search et findall. On peut également les mobiliser pour traiter / modifier une chaîne, en particulier avec la méthode re.sub(), qui est en quelque sorte une version améliorée de replace : tout comme celle-ci, elle effectue un rechercher / remplacer systématique au sein d'une chaîne, mais elle admet que le motif à remplacer soit une expression régulière. Cette méthode est donc extrêmement puissante pour nettoyer ou transformer des données textuelles. À noter la variante re.subn() qui, en plus de la chaîne modifiée, renvoie également le nombre de remplacements effectués. La méthode split(), que nous avons déjà vue et qui découpe la chaîne de caractère en liste, admet elle aussi qu'une expression régulière lui soit fournie en argument. Si dans le cas le plus simple, celui d'un fichier csv, cette expression se limite à un caractère (virgule, point-virgule, etc.), on peut donc imaginer de gérer des situations plus complexes. Quelques questions-réponsesPour commencer, avec search :
Et maintenant, avec findall. La chaîne dans laquelle on recherche l'expression est :
Les séquences d'échappementCertains codes dits d'échappement (à cause de l'antislash qui les précède) remplacent avantageusement une information plus lourde, voire ouvrent des possibilités spécifiques. Voici les plus courantes :
!!! Syntaxe, priez pour nous : Si aucune précaution n'est prise, alors le caractère \ sera considéré pour lui-même, et l'expression \bchat\b, au lieu de correspondre au mot « chat », matchera avec la suite antislash – b – c – h – a – t – b – antislash. Dès lors, pour faire comprendre à python que l'antislash d'une expression régulière n'est pas un caractère ordinaire mais un code à interpréter avec ce qui suit, la solution la plus simple consiste à placer un r (minuscule) juste avant la chaîne pattern de l'expression régulière, comme dans re.search(r"\bchat\b").
Et maintenant, c'est parti, nous allons écrire les expressions régulières nous-mêmes ! Ex 1 : Premiers pas Pour cette série de questions, on écrit un petit programme qui demande une saisie au clavier et qui dit ensuite à l'utilisateur si cette saisie correspond ou non à l'expression régulière concernée. Il faut donc vérifier successivement si la chaîne entrée au clavier est bel et bien :
Quelques armes supplémentairesLes groupesUn outil supplémentaire très précieux est celui des groupes. Dans une expression régulière, un groupe définit un sous-ensemble (comme qui dirait : un groupe !) de caractères, auquel on peut appliquer des quantificateurs ou des traitements particuliers. Ainsi, (bof) désigne la suite de caractères « bof » – à ne surtout pas confondre avec [bof], qui signifie : « un b, un o ou un f ». Si l'on écrit (bof)+, le quantificateur + s'applique à l'ensemble du groupe ; autrement dit, l'expression désigne les séries de caractères qui incluent au moins une fois « bof », et donc « bof », « bofbof », « bofbofbof », etc. Les groupes donnent donc davantage de souplesse aux quantificateurs. Mais ce n'est pas leur seul intérêt. Une expression peut en effet comprendre plusieurs groupes, soit à la suite les uns des autres, soit de manière imbriquée. Le grand intérêt de cette affaire est que ces groupes sont implicitement numérotés (indicés), dans l'ordre de l'ouverture des parenthèses. On peut ainsi faire du découpage, et récupérer ainsi les morceaux de la chaîne qui correspondent aux différents groupes. Prenons l'exemple d'une date au format jj/mm/aaaa. Normalement, sa recherche dans un texte ressemblera à : [0-9]{2}/[0-9]{2}/[0-9]{4} Ce qu'on peut également écrire sous la forme : \d{2}/\d{2}/\d{4} Ajoutons des groupes, et nous pourrons ensuite récupérer chaque information individuellement :
match = re.search("(\d{2})/(\d{2})/(\d{4})", texte) On remarque au passage que les groupes constituent ici des propriétés de l'objet match, qui est lui-même de type booléen – plus exactement,n dont la propriété par défaut est de type booléen. Attention ! Pour qu'une information soit effectivement capturée dans un groupe et qu'elle puisse être récupérée, il faut impérativement que ce groupe apparaisse de manière explicite, c'est-à-dire sans quantificateur, dans la regex. Ainsi, l'expression (\d\d){5} ne récupèrera pas cinq groupes... mais un seul, correspondant au dernier duo de chiffres qui aura été repéré.
Le « OU »Maintenant que nous connaissonbs le groupe, nous pouvons employer l'opérateur pipe (prononcer « païpe »), soit |, fonctionne de la même manière que le OU booléen. Ainsi : re.search("bijou(s|x)", "bijous") vaut Truere.search("bijou(s|x)", "bijoux") vaut True re.search("bijou(s|x)", "bijour") vaut False Les backreferencesLa présence des groupes permet également d'utiliser ces indices dans l'expression régulière elle-même. Imaginons par exemple que nous souhaitions détecter, dans un texte, la présence de mots qui se suivent à l'identique (comme dans « Je suis très très content »). Pour faire très simple, nous pouvons considérer qu'un mot est une suite composée d'un nombre quelconque de caractères, suivi d'une espace, ce qui s'écrit '[a-zA-Z]+ '. Comment repérer le fait que ce mot soit répété ? En en faisant un groupe, puis en demandant si ce groupe est suivi par lui-même (ici, le groupe étant le premier et seul de l'expression, il porte l'indice 1 : '([a-zA-Z]+) \1' Les assertions positives anticipées (positive lookahead)Cette appellation barbare désigne une idée assez simple : la possibilité de repérer des éléments dans une chaîne en quelque sorte « à l'avance », c'est-à-dire sans la parcourir. Le cas classique est celui où l'on recherche si la chaîne comporte certains éléments sans savoir dans quel ordre ceux se présentent. Imaginons le cas où nous voulons savoir si une phrase contient certains mots, dans un ordre quelconque. Avec l'approche standard, on pourrait imaginer de s'en sortir avec un OU – mais cette solution devient rapidement intenable si le nombre de mots augmente : pour 2 mots, il faut un OU, pour 3 mots il en faut 6, pour 4 mots il en faut 24, etc. C'est pourquoi le recours aux « APA » est fort utile. Pour obtenir ce résultat, la syntaxe consiste à définir l'ensemble recherché comme un groupe, et à placer au début de celui-ci la séquence ?=.*. Ainsi, si nous cherchons si un texte contient, comme celui de Rabelais, « veaux », « vaches », « cochons », « poulets », et ceci dans n'importe quel ordre, l'expression régulière correspondante sera : (?=.*\bveaux\b)(?=.*\bvaches\b)(?=.*\bcochons\b)(?=.*\bpoulets\b) Et maintenant, à vous de jouer. Ex 2 : Après les premiers pas, les suivants Dans la même lignée que l'exercice précédent, écrivez les expressions régulières permettant d'identifier :
Ex 3 : Travail en série Cette fois, pour varier un peu les plaisirs, l'application devra aller chercher un fichier texte qui contiendra les diverses propositions à raison d'une par ligne. Le programme affichera à l'écran la liste des propositions valides, et celle des propositions invalides. Pour la série concernant les mots de passe, on pourra utiliser ce fichier, et pour celle concernant les numéros de téléphone celui-ci.
Ex 4 : Nettoyage de données Vous travaillez dans une entreprise qui reçoit des milliers d’enregistrements de formulaires clients chaque mois. Ces données sont parfois mal formatées ou contiennent des erreurs. On vous demande d'écrire un code capable de nettoyer et de valider certains champs essentiels (email, numéro de téléphone, code postal, nom, etc.) afin que les données puissent être intégrées dans une base. Les données se présentent sous la forme d'un fichier texte de type csv (délimiteur : point-virgule), où chaque ligne représente un client, les champs étant dans l'ordre le nom, le prénom, le téléphone, l'adresse et le code postal. Pour tester votre code, on met à votre disposition un extrait de ce fichier. Il s'agit donc à la fois de nettoyer et de vérifier les données, qui obéiront aux conditions suivantes :
Le code devra produire deux fichiers .csv, l'un constitué des enregistrements valides (et mis au propre), l'autre des enregistrements invalides. |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||