Manipulating texts with Regex
En pratique, les textes que nous récupérons ne sont jamais totalement ‘propre’. Il est possible par exemple que l’ OCR ne soit pas parfaite ou que le texte comporte plusieurs coquilles. Certaines informations ne sont pas harmonisés et il est nécessaire de retravailler les données afin de les rendre utilisable. Par exemple, dans notre corpus \(C\)
, les titres des journaux n’étaient initialement pas harmonisés. Si je cherche dans ma base de donnée les articles qui ont été publiés dans “l’American Economic Review” je ne trouveras pas ceux avec l’acronyme “(published by the AER)”.
doc_id | source | year |
---|---|---|
text1 | This is the first document, American Economic Review | 1950 |
text2 | This document is the second document (published by the AER) | 1955 |
text3 | This document is the third document, Quaterly Journal of Economics | 1950 |
Afin de corriger ce problème, nous avons besoin de manipuler le texte afin de supprimer les informations que nous jugeons non pertinentes, ici “(published by the)”, et harmoniser celles qui nous intéressent. La réponse classique à ce type de problème est d’utiliser des RegEx.
Qu’est-ce que RegEx ?
RegEx est le diminutif de Regular Expression. Ce langage permet d’identifier une séquence de caractères spécifique au sein d’un texte. On appelle cette séquence une expression régulière. RegEx est un outil particulièrement puissant pour nettoyer des données, mais aussi pour extraire des informations importantes dans vos documents. La ReGex la plus simple est un mot. Explorons l’utilisation des RegEx les plus simples avec le texte suivant:
This is the first document, American Economic Review
RegEx | Match | Do not Match |
---|---|---|
American | American | |
is | ‘is’ et ‘this’ | |
the | the | The |
Le Regex “American” permet d’identifier toutes les occurrences de la suite de caractère “A” puis “m” puis “e” puis “c” etc. Cette séquence de caractères apparaît qu’une fois dans notre exemple texte. En revanche, la RegEx “is” est un expression régulière que l’on retrouve trois fois: le mot “is” est identifié mais aussi le mot “his” dans lequel on retrouve la séquence “i” puis “s”. RegEx distingue les lettres en minuscules et en majuscules. Ainsi la RegEx ‘the’ ne corresponds pas “The” mais uniquement au “the” de other”. Il est possible de composer des ReGex avec n’importe quel type de caractère.
Evidemment, il est possible d’effectuer des manipulations de textes bien plus complexe que les exemples précédents. Pour cela nous devons utiliser la syntaxe de RegEx. Afin de manipuler cette syntaxe, nous devons utiliser des “caractères spéciaux” qui dans le langage RegEx ne renvoi pas à eux-mêmes mais à une instruction particulière. Voyons ensemble la liste des principaux caractères spéciaux.
Les caractères spéciaux
Les principaux caractères spéciaux sont les suivants ^ . [ ] $ ( ) * + ? | { } \
.
-
Les quantificateurs permettent de faire répéter un caractère ou une suite de caractères. Ils sont aux nombres de trois.
?
: se répète zéro ou une fois (The?
correspond à “Th” et “The”).*
: se répète zéro, une fois ou plusieurs fois (The*
correspond à “Th”, “The”, Thee”, “Theee”, etc.).+
: se répète une fois ou plusieurs fois(The+
correspond à “The”, “Thee”). -
{}
: les crochets permettent une quantification personnalisée. Le quantificateur personalisé s’utilise de la manière suivante:{X}
ou{X,Y}
où X et Y sont des chiffres. Ainsi, la RegExa{0,1}
est identique à la RegExa?
. L’utilisation d’un tirets-
entre X et Y ({X-Y}
) désigne un ensemble borné par X et Y. -
Les
[]
sont des opérateurs de classes. Ils permettent de définir un ensemble de caractères que nous cherchons à trouver. Par exemple,[abc]
corresponds à “a”, “b”, “c”, ou “+”. Comme pour les quantificateurs, il est possible de lister individuellement l’ensemble des caractères qui nous intéressent ou de définir une suite de caractères avec un tiret-
. Ainsi,[a-z]
correspond à l’ensemble des 26 lettres de l’alphabet,[1-5a]
les 5 premiers chiffres ou “a”. Il est aussi possible d’appeler une classe entière directement. Par exemple, il est possible d’appeler l’ensemble des lettres et des chiffres avec le RegEx[:alnum:]
. On peut également définir un ensemble de caractères que nous ne voulons pas trouver en rajoutant le méta caractère^
.[^a]
cherchera tout sauf le caractère “a”. -
()
sont des opérateurs de groupes. Ils permettent de regrouper un ensemble de caractères. Cela est très utile lorsque l’on veut leur appliquer une opération à part. Par exemple, il est possible de grouper un ensemble de caractères puis d’appliquer un quantificateur. La RegEx(ha)* c'est drôle !
corresponds à “c’est drôle”, “ha c’est drôle”, “haha c’est drôle”, etc. -
|
est un opérateur d’union.a|b
corresponds à “a” ou “b”. Notez qu’il est possible de l’utiliser entre des groupes()
. -
^
indique que la correspondance se fait au début du texte. Par exemple, dans notre exemple précèdent, la ReGex^A
cherche la lettre “A”au début et uniquement au début. -
$
est la contraposée de^
. Il indique la correspondance se fait à la fin du texte. -
.
désigne l’ensemble des caractères possibles. L’expression.*
désigne donc l’ensemble des combinaisons de caractères possibles.
Sur la base de cet syntaxe, il déjà possible de réaliser la plupart des opérations nécessaires pour nettoyer notre corpus \(C\)
:
doc_id | text | year |
---|---|---|
text1 | This is the first document, American Economic Review | 1950 |
text2 | This document is the second document (published by the AER) | 1955 |
text3 | This document is the third document, Quaterly Journal of Economics | 1950 |
Par exemple, pour le premier et troisième document, nous pourrions sélectionner l’ensemble des caractères qui suivent une virgule “,” pour séparer le texte du document de sa source. Une RegEx possible serait la suivante: ,.*
qui corresponds à une “,” puis n’importe quel caractère, 0, 1 ou plusieurs fois.
library(stringr)
string <- "This is the first document, American Economic Review"
str_extract(string, ",.*")
Le deuxième document pose cependant encore un problème. Nous aimerions sélectionner ce qu’il y a dans la parenthèse. On pourrait écrire la RegEx suivante (.*)
qui indique à première vue capturer à n’importe quoi qui se situe au sein d’une parenthèse. Cependant, les caractères “(” et “)” sont des méta-caractères, il désigne des groupes dans le langage RegEx. Si nous appliquons notre ReGex dans l’état, nous ne capturerions pas le texte dans la parenthèse mais l’ensemble du texte. Si les caractères spéciaux permettent des manipulations très sophistiquées d’un texte, leur existence pose donc un problème évident: puisque ces caractères spéciaux ne renvoient pas à eux-même, comment les identifier dans un texte ?
L’échappement
Afin de palier à cette difficulté, RegEx utilise un caractère spécial \
afin de permettre aux caractères spéciaux de se renvoyer de nouveau à eux-même. Autrement dit si ?
est un quantificateur O ou 1, \?
désigne le point d’interrogation. On désigne cette opération comme l’échappement d’un caractère. Dans notre exemple précèdent, il possible de capturer la le texte entre parenthèse avec la RegEx suivante \(.*\)
qui échappe correctement les deux parenthèses. Notez que pour obtenir l’antislash “\”, vous pouvez devez échapper le caractère antislash lui-même (\\
). Vous pouvez déroulez le code suivant si vous voulez voir comment on peut implémenter cette ReGex dans R
library(stringr)
string <- "This document is the second document (published by the AER)"
str_extract(string, "\\(.*\\)")
Combiner avec des caractères non spéciaux, l’échappement permet aussi de construire des nouveaux caractères spéciaux qui permettent de formuler des instructions plus précises. Par exemple, \n
est un caractère spécial qui désigne la fin d’une ligne, \t
désigne une tabulation. Ces caractères permettent de distinguer les différents types d’espaces qui composent un texte et qui, selon la nature du texte, peuvent apporter des informations pertinentes.
Look ahead et behind
L’un des problèmes de ma RegEx \(.*\)
est qu’elle capture aussi les parenthèses, ce qui ne m’intéressent pas spécialement. Les look ahead et une look behind permettent de résoudre ce type de problème.
Les look ahead et behind sont des RegEx avancés qui permettent de sélectionner une expression régulière après une autre expression régulière. On en dénombre quatre. Pour des expressions régulières A
et B
:
A(=?B)
: look ahead positif. Il cherche les expressions “A” après les expressions “B”.A(!?B)
: look ahead négatif. Il cherche les expressions “A” qui ne sont pas après les expressions “B”.(?<=B)A
: look behind positif. Il cherche les expressions “A” avant les expressions “B”.(?<!B)A
: look behind négatif. Il cherche les expressions “A” qui ne sont pas avant les expressions “B”.
Il est important noter ici que le méta caractère ?
ne désigne plus un quantificateur car il est associé à d’autres caractères spéciaux. Les groupes ()
sont nécessaire ici pour définir et mémoriser l’expression B
.
Je peux rajouter une look ahead et une look behind positives pour que RegEx sélectionne le texte à l’intérieur des parenthèses mais sans capturer ces dernières. Ma RegEx précèdente devient alors: (?<=\\()(.*)(?=\\))
.
library(stringr)
string <- "This document is the second document (published by the AER)"
str_extract(string, "(?<=\\()(.*)(?=\\))")
Vous trouverez un guide complet du fonctionnement des look ahead et behind ici (en anglais).
Les quantificateurs gloutons et paresseux
Par défaut, les quantificateurs sont dits “gloutons” (greedy): ils vont essayer de trouver la séquence de caractère la plus longue possible de l’expression choisie. Reprenons notre exemple précèdent et ajoutons lui une nouvelle information entre parenthèse.
This document is the second document (published by the AER) by Thomas Delcey (first author) and Julien Gradoz
Par défaut, notre Regex va \\(.*\\)
correspondra à la séquence de caractères suivante:
(published by the AER) by Thomas Delcey (first author)
Plutôt que de sélectionner la séquence de caractère dans la première paire de parenthèses, ma RegEx sélectionne la séquence de caractères entre la première et la dernière parenthèse. Elle séléctionne la correspondance la plus longue possible.1
Il est possible d’inverser le fonctionnement du quantificateur est de le rendre est de rendre paresseux (lazy). Pour rendre le quantificateur paresseux, il suffit de lui ajouter le méta caractère ?
. Notre RegEx devient "(?<=\\()(.*?)(?=\\))"
. Avec un quantificateur paresseux, .*?
va chercher la correspondance la plus petite possible, mais autant que cela est nécessaire, c’est-à-dire, dans notre exemple la séquence de caractère voulu:
published by the AER
library(stringr)
string <- "This document is the second document (published by the AER) by Thomas Delcey (first author) and Julien Gradoz"
str_extract(string, "(?<=\\()(.*?)(?=\\))")
library(dplyr)
library(knitr)
library(kableExtra)
corpus <- tibble(doc_id = c("text1", "text2", "text3", "..."),
source = c("This is the first document, American Economic Review",
"This document is the second document (published by the AER)",
"This document is the third document, Quaterly Journal of Economics",
"..."), year = c(1950, 1955, 1950, "..."))
corpus %>%
kbl() %>%
kable_minimal() %>%
kable_styling(full_width = F)
doc_id | source | year |
---|---|---|
text1 | This is the first document, American Economic Review | 1950 |
text2 | This document is the second document (published by the AER) | 1955 |
text3 | This document is the third document, Quaterly Journal of Economics | 1950 |
… | … | … |
RegEx en pratique
En pratique, ReGex s’utilise dans des programme informatique comme R
ou Python
.2 Les implémentations de RegEx dans un langage peuvent être légèrement différente. Vous trouverez ici une liste des différences qui peuvent exister entre les implémentation de RegEx dans les programmes.
Si une RegEx identifie une expression régulière dans un texte, il faut un programme informatique pour nous permettre de manipuler l’expression trouvée (extraire, supprimer, transformer, etc.). Le langage de base de R
offre une gamme complète de fonction permettant de manipuler un texte avec des RegEx. Toute fois dans ce langage, je vous conseille plutôt d’utiliser le package stringr
](https://stringr.tidyverse.org/) de la collection tidyverse
. Les fonctions proposées sont plus intuitives à utiliser et offre des manipulations plus avancées que les fonctions de base.3. Sur Python
, le module re
offre un ensemble de fonctions complètes pour utiliser les RegEx.
Pour terminer cette section, notez qu’une RegEx peut rapidement devenir difficile à lire à mesure que vous essayer de faire correspondre une séquence de caractère de plus en plus longues. Par exemple, une RegEx qui a pour objectif de collecter l’ensemble des adresses email possibles peut devenir extrêmement laborieuse à mesure que vous prenez en compte l’ensemble des possibilités. Voici un exemple de RegEx pour les emails construite par les utilisateurs du forum Stackoverflow:
(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])
Il est ainsi conseillé de commenter les RegEx longues afin de faciliter la relecture de votre travail (par vous même ou par vos lecteurs).
References
-
Pour comprendre pourquoi, il est nécessaire de comprendre le fonctionnement des quantificateurs de RegEx. Notre quantificateur
*
recherche l’expression régulière associée (ici.
, donc n’importe quel caractère) dans la séquence de caractères sélectionnée et cette recherche ne s’arrête pas tant qu’elle n’a pas atteint le dernier caractère de notre séquence, c’est-à-dire, dans notre exemple la lettre “z”. Une fois qu’elle a analysé l’ensemble des caractères, elle revient en arrière et essayer de retrouver la suite de notre RegEx. ↩︎ -
Ces langages utilisent déjà le caractère
\
comme un échappement. Il est donc nécessaire d’échapper deux fois\
lorsque vous utilisez une ReGex dansR
ouPython
. ↩︎ -
De manière générale, si vous utilisez
R
il est fortement conseillé d’utiliser la collectiontidyverse
↩︎