From 62b2a7c240587e5868ace8abbad650ef5ae08d43 Mon Sep 17 00:00:00 2001 From: Lino Galiana <33896139+linogaliana@users.noreply.github.com> Date: Wed, 28 Dec 2022 15:00:50 +0100 Subject: [PATCH] Suite chapitre regex (#340) * exo1 * Automated changes * Automated changes * note * Automated changes * Automated changes * re * re suite * details * Automated changes * Automated changes * exo * exo * pandas * pandas * pd * pd * Automated changes * Automated changes * truc * details * Automated changes * Automated changes * mise en forme box * Automated changes * Automated changes Co-authored-by: github-actions[bot] --- .../manipulation/04b_regex_TP/index.qmd | 540 +++++++++++++----- 1 file changed, 406 insertions(+), 134 deletions(-) diff --git a/content/course/manipulation/04b_regex_TP/index.qmd b/content/course/manipulation/04b_regex_TP/index.qmd index 8a2f139db..8e17c644a 100644 --- a/content/course/manipulation/04b_regex_TP/index.qmd +++ b/content/course/manipulation/04b_regex_TP/index.qmd @@ -68,6 +68,7 @@ par `Pandas` pour vectoriser les recherches textuelles. ```{python} import re +import pandas as pd ``` @@ -77,7 +78,15 @@ import re

Hint

``` -**Les expressions régulières (*regex*) sont notoirement difficiles à maîtriser.** Il existe des outils qui facilitent le travail avec les expressions régulières. L'outil de référence pour ceci est [https://regex101.com/] qui permet de tester des `regex` en `Python`. +**Les expressions régulières (*regex*) sont notoirement difficiles à maîtriser.** Il existe des outils qui facilitent le travail avec les expressions régulières. + +- L'outil de référence pour ceci est [https://regex101.com/] qui permet de tester des `regex` en `Python` +tout en ayant une explication qui accompagne ce test + +- De même pour [ce site](https://ole.michelsen.dk/tools/regex/) qui comporte une cheat sheet en bas de la page. + +- Les jeux de [Regex Crossword](https://regexcrossword.com/) permettent d'apprendre les expressions régulières en s'amusant + Il peut être pratique de demander à des IA assistantes, comme `Github Copilot` ou `ChatGPT`, une première version d'une regex en expliquant le contenu qu'on veut extraire. @@ -138,7 +147,11 @@ print(re.search(pattern, "J'ai un chiot très mignon.")) ## Classes de caractères -Les classes de caractères peuvent être pratiques pour rechercher du texte. Par +Lors d’une recherche, on s’intéresse aux caractères et souvent aux classes de caractères : on cherche un chiffre, une lettre, un caractère dans un ensemble précis ou un caractère qui n’appartient pas à un ensemble précis. Certains ensembles sont prédéfinis, d’autres doivent être définis à l’aide de crochets. + +Pour définir un ensemble de caractères, il faut écrire cet ensemble entre crochets. Par exemple, `[0123456789]` désigne un chiffre. Comme c’est une séquence de caractères consécutifs, on peut résumer cette écriture en `[0-9]`. + +Par exemple, si on désire trouver tous les _pattern_ qui commencent par un `c` suivi d'un `h` puis d'une voyelle (a, e, i, o, u), on peut essayer cette expression régulière. @@ -187,7 +200,8 @@ Ces quantifiers peuvent bien-sûr être associés à d'autres types de caractères, notamment les classes de caractères. Cela peut être extrèmement pratique. Par exemple, `\d+` permettra de capturer un ou plusieurs chiffres, `\s?` -permettra d'ajouter en option un espace... +permettra d'ajouter en option un espace, +`[\w]{6,8}` un mot entre six et huit lettres qu’on écrira... Il est aussi possible de définir le nombre de répétitions avec `{}`: @@ -217,6 +231,44 @@ print(re.match("(toc){5}","toctoctoctoc")) print(re.match("(toc){2,4}","toctoctoctoc")) ``` +::: {.cell .markdown} +```{=html} + +``` +::: ## Aide-mémoire @@ -272,267 +324,487 @@ elles sont très fréquentes. Parmi-celles: | `\W` | N'importe quel ensemble qui n'est pas un mot (lettres et nombres) +Dans l'exercice suivant, vous allez pouvoir mettre en pratique +les exemples précédents sur une `regex` un peu plus complète. +Cet exercice ne nécessite pas la connaissance des subtilités +du _package_ `re`, vous n'aurez besoin que de `re.findall`. -# Principales fonctions de `re` +Cet exercice utilisera la chaine de caractère suivante: -TO BE COMPLETED +```{python} +s = """date 0 : 14/9/2000 +date 1 : 20/04/1971 date 2 : 14/09/1913 date 3 : 2/3/1978 +date 4 : 1/7/1986 date 5 : 7/3/47 date 6 : 15/10/1914 +date 7 : 08/03/1941 date 8 : 8/1/1980 date 9 : 30/6/1976""" +s +``` - -# Pour en savoir plus +::: {.cell .markdown} +```{=html} + +``` +::: +A l'issue de la question 1, vous devriez avoir ce résultat : -# OLD +```{python} +#| echo: false +re.findall("[0-3]?[0-9]/", s) +``` -Ce TD est directement issu du contenu de [Xavier Dupré](http://www.xavierdupre.fr/app/teachpyx/helpsphinx/c_regex/regex.html), -l'ancien professeur de ce cours. Il sera mis-à-jour dans le futur. +A l'issue de la question 2, vous devriez avoir ce résultat, qui +commence à prendre forme: +```{python} +#| echo: false +re.findall("[0-3]?[0-9]/[0-1]?[0-9]", s) +``` -Par exemple, lorsqu'on remplit un formulaire, on voit souvent le format ``"MM/JJ/AAAA"`` qui précise sous quelle forme on s'attend à ce qu’une date soit écrite. Les expressions régulières permettent de définir également ce format et de chercher dans un texte toutes les chaînes de caractères qui sont conformes à ce format. +A l'issue de la question 3, on parvient bien +à extraire les dates : -La liste qui suit contient des dates de naissance. On cherche à obtenir toutes les dates de cet exemple sachant que les jours ou les mois contiennent un ou deux chiffres, et les années deux ou quatre. +```{python} +#| echo: false +# Question 3 +re.findall("[0-3]?[0-9]/[0-1]?[0-9]/[0-2]?[0-9]?[0-9][0-9]", s) +``` ```{python} -s = """date 0 : 14/9/2000 -date 1 : 20/04/1971 date 2 : 14/09/1913 date 3 : 2/3/1978 -date 4 : 1/7/1986 date 5 : 7/3/47 date 6 : 15/10/1914 -date 7 : 08/03/1941 date 8 : 8/1/1980 date 9 : 30/6/1976""" -print(s) +#| echo: false +#| output: false +# Question 4 +re.findall("\d{1,2}/\d{1,2}/\d{2,4}", s) ``` -#### Exemple introductif : Format de date +Si tout va bien, à la question 5, votre regex devrait +fonctionner: +```{python} +#| echo: false +# Question 5 +re.findall("\d{1,4}/\d{1,2}/\d{1,4}", s + "\n 1998/07/12") +``` -Le premier chiffre du ``jour`` est soit 0, 1, 2, ou 3 ; ceci se traduit par ``[0-3]``. -Le second chiffre est compris entre 0 et 9, soit ``[0-9]``. Le format des jours est traduit par ``[0-3][0-9]``. Mais le premier chiffre du jour est facultatif, ce qu'on précise avec le symbole ? : ``[0-3]?[0-9]``. +# Principales fonctions de `re` -Les ``mois`` suivent le même principe : ``[0-1]?[0-9]``. +Voici un tableau récapitulatif des principales +fonctions du package `re` suivi d'exemples. +Nous avons principalement +utilisé jusqu'à présent `re.findall` qui est +l'une des fonctions les plus pratiques du _package_. +`re.sub` et `re.search` sont également bien pratiques. +Les autres sont moins vitales mais peuvent dans des +cas précis être utiles. -Pour les ``années``, ce sont les deux premiers chiffres qui sont facultatifs, le symbole ? s'appliquent alors sur les deux premiers chiffres : ``[0-2]?[0-9]?[0-9][0-9]``. Le format final d'une date devient alors : +| Fonction | Objectif | +|------------------|-----------------| +| `re.match(, s)` | Trouver et renvoyer le __premier__ _match_ de l'expression régulière `` __à partir du début__ du _string_ `s` | +| `re.search(, s)` | Trouver et renvoyer le __premier__ _match_ de l'expression régulière `` __quelle que soit sa position__ dans le _string_ `s` | +| `re.finditer(, s)` | Trouver et renvoyer un itérateur stockant tous les _matches_ de l'expression régulière `` __quelle que soit leur(s) position(s)__ dans le _string_ `s`. En général, on effectue ensuite une boucle sur cet itérateur | +| `re.findall(, s)` | Trouver et renvoyer **tous les _matches_** de l'expression régulière `` __quelle que soit leur(s) position(s)__ dans le _string_ `s` sous forme de __liste__ | +| `re.sub(, new_text, s)` | Trouver et __remplacer tous__ les _matches_ de l'expression régulière `` __quelle que soit leur(s) position(s)__ dans le _string_ `s` | -``[0-3]?[0-9]/[0-1]?[0-9]/[0-2]?[0-9]?[0-9][0-9]`` +Pour illustrer ces fonctions, voici quelques exemples: -Le module ``re`` gère les expressions régulières, celui-ci traite différemment les parties de l'expression régulière qui sont entre parenthèses de celles qui ne le sont pas : c'est un moyen de dire au module ``re`` que nous nous intéressons à telle partie de l'expression qui est signalée entre parenthèses. Comme la partie qui nous intéresse - une date - concerne l'intégralité de l'expression régulière, il faut insérer celle-ci intégralement entre parenthèses. +::: {.cell .markdown} +```{=html} +
Exemple de re.match 👇 +``` -La première étape consiste à construire l'expression régulière, la seconde à rechercher toutes les fois qu'un morceau de la chaîne `s` définie plus haut correspond à l’expression régulière. +`re.match` ne peut servir qu'à capturer un _pattern_ en début +de _string_. Son utilité est donc limitée. +Capturons néanmoins `toto` : ```{python} -import re -# première étape : construction -expression = re.compile("([0-3]?[0-9]/[0-1]?[0-9]/[0-2]?[0-9]?[0-9][0-9])") -# seconde étape : recherche -res = expression.findall(s) -print(res) +re.match("(to){2}", "toto à la plage") ``` -Le résultat est une liste dont chaque élément correspond aux parties comprises entre parenthèses qu'on appelle des groupes. +```{=html} +
+``` +::: -Lorsque les expressions régulières sont utilisées, on doit d'abord se demander comment définir ce qu’on cherche puis quelles fonctions utiliser pour obtenir les résultats de cette recherche. Les deux paragraphes qui suivent y répondent. +::: {.cell .markdown} +```{=html} +
Exemple de re.search 👇 +``` -## Syntaxe +`re.search` est plus puissant que `re.match`, on peut +capturer des termes quelle que soit leur position +dans un _string_. Par exemple, pour capturer _age_: -La syntaxe des expressions régulières est décrite sur le site officiel de python. La page [Regular Expression Syntax](https://docs.python.org/3/library/re.html) décrit comment se servir des expressions régulières, les deux pages sont en anglais. Comme toute grammaire, celle des expressions régulières est susceptible d’évoluer au fur et à mesure des versions du langage python. +```{python} +re.search("age", "toto a l'age d'aller à la plage") +``` +Et pour capturer exclusivement _"age"_ en fin +de _string_: -## Les ensembles de caractères +```{python} +re.search("age$", "toto a l'age d'aller à la plage") +``` -Lors d’une recherche, on s’intéresse aux caractères et souvent aux classes de caractères : on cherche un chiffre, une lettre, un caractère dans un ensemble précis ou un caractère qui n’appartient pas à un ensemble précis. Certains ensembles sont prédéfinis, d’autres doivent être définis à l’aide de crochets. -Pour définir un ensemble de caractères, il faut écrire cet ensemble entre crochets. Par exemple, ``[0123456789]`` désigne un chiffre. Comme c’est une séquence de caractères consécutifs, on peut résumer cette écriture en ``[0-9]``. Pour inclure les symboles ``+`` et ``-``, il suffit d’écrire : ``[-0-9+]``. La subtilité est qu'il faut penser à mettre le symbole ``-`` au début pour éviter qu’il ne désigne une séquence. +```{=html} +
+``` +::: -Le caractère `^` inséré au début du groupe signifie que le caractère cherché ne doit pas être un de ceux qui suivent. Le tableau suivant décrit les ensembles prédéfinis et leur équivalent en terme d’ensemble de caractères : +::: {.cell .markdown} +```{=html} +
Exemple de re.finditer 👇 +``` -* ``.`` désigne tout caractère non spécial quel qu'il soit. -* ``\d`` désigne tout chiffre, est équivalent à ``[0-9]``. -* ``\D`` désigne tout caractère différent d'un chiffre, est équivalent à ``[^0-9]``. -* ``\s`` désigne tout espace ou caractère approché, est équivalent à ``[\; \t\n\r\f\v]``. Ces caractères sont spéciaux, les plus utilisés sont ``\t`` qui est une tabulation, ``\n`` qui est une fin de ligne et qui ``\r`` qui est un retour à la ligne. -* ``\S`` désigne tout caractère différent d'un espace, est équivalent à ``[^ \t\n\r\f\v]``. -* ``\w`` désigne toute lettre ou tout chiffre, est équivalent à ``[a-zA-Z0-9_]``. -* ``\W`` désigne tout caractère différent d'une lettre ou d'un chiffre, est équivalent à ``[^a-zA-Z0-9_]``. -* ``^`` désigne le début d'un mot sauf s'il est placé entre crochets. -* ``$`` désigne la fin d'un mot sauf s'il est placé entre crochets. +`re.finditer` est, à mon avis, +moins pratique que `re.findall`. Son utilité +principale par rapport à `re.findall` +est de capturer la position dans un champ textuel: +```{python} +s = "toto a l'age d'aller à la plage" +for match in re.finditer("age", s): + start = match.start() + end = match.end() + print(f'String match "{s[start:end]}" at {start}:{end}') +``` -A l'instar des chaînes de caractères, comme le caractère ``\`` est un caractère spécial, il faut le doubler : ``[\\]``. +```{=html} +
+``` +::: -## Les multiplicateurs -Les multiplicateurs permettent de définir des expressions régulières comme : un mot entre six et huit lettres qu’on écrira ``[\w]{6,8}``. Le tableau suivant donne la liste des multiplicateurs principaux : +::: {.cell .markdown} +```{=html} +
Exemple de re.sub 👇 +``` +`re.sub` permet de capturer et remplacer des expressions. +Par exemple, remplaçons _"age"_ par _"âge"_. Mais attention, +il ne faut pas le faire lorsque le motif est présent dans _"plage"_. +On va donc mettre une condition négative: capturer _"age"_ seulement +s'il n'est pas en fin de _string_ (ce qui se traduit en _regex_ par `?!$`) -* ``*`` présence de l'ensemble de caractères qui précède entre 0 fois et l'infini -* ``+`` présence de l'ensemble de caractères qui précède entre 1 fois et l'infini -* ``?`` présence de l'ensemble de caractères qui précède entre 0 et 1 fois -* ``{m,n}`` présence de l'ensemble de caractères qui précède entre *m* et *n* fois, si *m=n*, cette expression peut être résumée par ``{n}``. -* ``(?!(...))`` absence du groupe désigné par les points de suspensions. +```{python} +re.sub("age(?!$)", "âge", "toto a l'age d'aller à la plage") +``` -L’algorithme des expressions régulières essaye toujours de faire correspondre le plus grand morceau à l’expression régulière. +```{=html} +
+``` +::: -Par exemple, pour la chaîne de charactère ``

mot

``, l'expression régulière ``<.*>`` correspond à trois morceaux : -* ``

`` -* ``

`` -* ``

mot

`` => le plus grand qui sera choisi. -Pour choisir les plus petits, il faudra écrire les multiplicateurs comme ceci : ``*?``, ``+?`` +::: {.cell .markdown} +```{=html} + +``` +::: + +# Généralisation avec `Pandas` + +Les méthodes de `Pandas` sont des extensions de celles de `re` +qui évitent de faire une boucle pour regarder, +ligne à ligne, une regex. En pratique, lorsqu'on traite des +`DataFrames`, on utilise plutôt l'API Pandas que `re`. Les +codes de la forme `df.apply(lambda x: re.(,x), axis = 1)` +sont à bannir car très peu efficaces. +Les noms changent parfois légèrement par rapport à leur +équivalent `re`. -print(re.compile("(<.*>)").match(s).groups()) +| Méthode | Description | +|------------------|---------------| +| `str.count()` | Compter le nombre d'occurrences du _pattern_ dans chaque ligne | +| `str.replace()` | Remplacer le _pattern_ par une autre valeur. Version vectorisée de `re.sub()` | +| `str.contains()` | Tester si le _pattern_ apparaît, ligne à ligne. Version vectorisée de `re.search()` | +| `str.extract()` | Extraire les groupes qui répondent à un _pattern_ et les renvoyer dans une colonne | +| `str.findall()` | Trouver et renvoyer toutes les occurrences d'un _pattern_. Si une ligne comporte plusieurs échos, une liste est renvoyée. Version vectorisée de `re.findall()` | +A ces fonctions, s'ajoutent les méthodes `str.split()` et `str.rsplit()` qui sont bien pratiques. + + +::: {.cell .markdown} +```{=html} +
Exemple de str.count 👇 ``` +On peut compter le nombre de fois qu'un _pattern_ apparaît avec +`str.count` + ```{python} -print(re.compile("(.*)").match(s)) +df = pd.DataFrame({"a": ["toto", "titi"]}) +df['a'].str.count("to") ``` -```{python} -print(re.compile(".*(<.h1>)").match(s).groups()) -print(re.compile("(<.?h1>)").findall(s)) +```{=html} +
``` +::: -Un exemple pour trouver des adresses emails : +::: {.cell .markdown} +```{=html} +
Exemple de str.replace 👇 +``` + +Remplaçons le motif _"ti"_ en fin de phrase ```{python} -text_emails = 'Hello from shubhamg199630@gmail.com to priya@yahoo.com about the meeting @2PM' +df = pd.DataFrame({"a": ["toto", "titi"]}) +df['a'].str.replace("ti$", " punch") ``` -```{python} -# \S` désigne tout caractère différent d'un espace -# `+` présence de l'ensemble de caractères qui précède entre 1 fois et l'infini -liste_emails = re.findall('\S+@\S+', text_emails) +```{=html} +
``` +::: -```{python} -print(liste_emails) + +::: {.cell .markdown} +```{=html} +
Exemple de str.contains 👇 ``` -## Exercices +Vérifions les cas où notre ligne termine par _"ti"_: -{{% box status="exercise" title="Exercice" icon="fas fa-pencil-alt" %}} +```{python} +df = pd.DataFrame({"a": ["toto", "titi"]}) +df['a'].str.contains("ti$") +``` -**Exercice 1: Application directe** +```{=html} +
+``` +::: -Recherchez les dates présentes dans la phrase suivante. +::: {.cell .markdown} +```{=html} +
Exemple de str.findall 👇 +``` ```{python} -texte1 = """Je suis né le 28/12/1903 et je suis mort le 08/02/1957. Ma seconde femme est morte le 10/11/1963. -J'ai écrit un livre intitulé 'Comprendre les fractions : les exemples en page 12/46/83' """ +df = pd.DataFrame({"a": ["toto", "titi"]}) +df['a'].str.findall("to") +``` + +```{=html} +
``` +::: -Puis dans celle-ci : +::: {.cell .markdown} +```{=html} + ``` +::: -```{python, show = FALSE, include = FALSE} -# Correction -import re -expression = re.compile("([0-3]?[0-9]/[0-1]?[0-9]/[0-2]?[0-9]?[0-9][0-9])") -print(expression.findall(texte1)) -print(expression.findall(texte2)) +# Pour en savoir plus + +- [documentation collaborative sur `R` nommée `utilitR`](https://www.book.utilitr.org/textdata.html#regex) +- [_R for Data Science_](https://r4ds.hadley.nz/regexps.html) +- [_Regular Expression HOWTO_ dans la documentation officielle de `Python`](https://docs.python.org/3/howto/regex.html) +- L'outil de référence [https://regex101.com/] pour tester des expressions régulières +- [Ce site](https://ole.michelsen.dk/tools/regex/) qui comporte une cheat sheet en bas de la page. +- Les jeux de [Regex Crossword](https://regexcrossword.com/) permettent d'apprendre les expressions régulières en s'amusant + + +## Exercices supplémentaires + +### Extraction d'adresses email + +Il s'agit d'un usage classique des _regex_ + +```{python} +text_emails = 'Hello from toto@gmail.com to titi.grominet@yahoo.com about the meeting @2PM' ``` +::: {.cell .markdown} +```{=html} + +``` +::: -{{% box status="exercise" title="Exercice" icon="fas fa-pencil-alt" %}} -**Exercice 2: Nettoyer une colonne de date de publication** +```{python} +#| echo: false + +# \S` désigne tout caractère différent d'un espace +# `+` présence de l'ensemble de caractères qui précède entre 1 fois et l'infini +liste_emails = re.findall('\S+@\S+', text_emails) +print(liste_emails) +``` + +### Extraire des années depuis un `DataFrame` `Pandas` L'objectif général de l'exercice est de nettoyer des colonnes d'un DataFrame en utilisant des expressions régulières. +::: {.cell .markdown} +```{=html} + +``` +::: + + ```{python} -import pandas +#| echo: false -data_books = pandas.read_csv('https://raw.githubusercontent.com/realpython/python-data-cleaning/master/Datasets/BL-Flickr-Images-Book.csv',sep=',') +# Question 1 +data_books = pd.read_csv('https://raw.githubusercontent.com/realpython/python-data-cleaning/master/Datasets/BL-Flickr-Images-Book.csv',sep=',') ``` ```{python} +#| echo: false + +# Question 2 data_books=data_books[['Identifier', 'Place of Publication', 'Date of Publication', 'Publisher', 'Title', 'Author']] ``` -En regardant la base, on réalise que cette colonne ne correspond pas toujours à une année, par exemple en lignes 13 et 16. +Voici par exemple le problème qu'on demande de détecter à la question 3 : ```{python} +#| echo: false + +# Question 3 data_books[['Date of Publication',"Title"]].iloc[13:20] ``` -1. Commencez par regarder le nombre d'informations manquantes. On ne pourra pas avoir mieux après la regex, et normalement on ne devrait pas avoir moins... +```{python} +#| echo: false -```{python, include = FALSE, eval = FALSE} +# Question 4 data_books['Date of Publication'].isna().sum() ``` -2. Déterminer la forme de la regex pour une date de publication. A priori, il y a 4 chiffres qui forment une année. +Grâce à notre regex (question 5), on obtient ainsi un `DataFrame` plus conforme à nos attentes -```{python, include = FALSE, eval = FALSE} -import re -expression = "([0-2][0-9][0-9][0-9])" -``` - -3. Quelles lignes sont changées quand on applique notre regex à la colonne qui nous intéresse avec la fonction `str.extract()` ? +```{python} +#| echo: false -```{python, include = FALSE, eval = FALSE} -data_books['Date of Publication 2'] = data_books['Date of Publication'].str.extract(expression, expand=False) -data_books[~(data_books['Date of Publication'] == data_books['Date of Publication 2'])][['Date of Publication', 'Date of Publication 2']] +# Question 5 +expression = "([0-2][0-9][0-9][0-9])" +data_books['year'] = data_books['Date of Publication'].str.extract(expression, expand=False) +data_books.loc[~(data_books['Date of Publication'] == data_books['year']), ['Date of Publication', 'year']] ``` -4. On a 2 `NaN` qui n'étaient pas présents au début de l'exercice. Quels sont-ils et pourquoi ? +Quant aux nouveaux `NaN`, +il s'agit de lignes qui ne contenaient pas de chaînes de caractères qui ressemblaient à des années: -```{python, include = FALSE, eval = FALSE} -# Il s'agit de lignes qui ne contenaient pas de chaînes de caractères qui ressemblaient à des années. -data_books[~data_books['Date of Publication'].isna() & data_books['Date of Publication 2'].isna()][['Date of Publication', 'Date of Publication 2']] +```{python} +#| echo: false +data_books.loc[~data_books['Date of Publication'].isna() & data_books['year'].isna(), ['Date of Publication', 'year']] ``` -5. Quelle est la répartition des dates de publications dans le jeu de données ? Vous pouvez par exemple afficher un histogramme. +Enfin, on obtient l'histogramme suivant des dates de publications: -```{python, include = FALSE, eval = FALSE} -pandas.to_numeric(data_books['Date of Publication 2'], downcast='integer').plot(kind ="hist") +```{python} +#| echo: false +pd.to_numeric(data_books['year'], downcast='integer').plot(kind ="hist") ``` -{{% /box %}} -## Aller plus loin pour apprendre les regex -- Le site [regex101](https://regex101.com/) permet de tester les expressions régulières utilisées tout en ayant une explication qui accompagne ce test -- De même pour [ce site](https://ole.michelsen.dk/tools/regex/) qui comporte une cheat sheet en bas de la page. -- Les jeux de [Regex Crossword](https://regexcrossword.com/) permettent d'apprendre les expressions régulières en s'amusant