Nous utilisons la plupart du temps des données structurées, c’est à dire qu’elles se présentent sous forme de tableaux. Toutefois, beaucoup de données ne sont pas structurées. En effet, les données peuvent se présenter sous forme de banque d’images, de textes littéraires, d’articles sur internet, etc… Pouvoir exploiter ces données constitue le graal des analystes des prochaines années, bien que la quête soit déjà lancée, surtout par les grandes sociétés du web.
Quand on travaille avec des données sous la forme de texte, on fait du text mining. On fait du text mining pour diverses raisons, pour de nombreux objectifs. On peut réaliser une analyse sémantique, il existe de beaux exemples avec la dernière campagne présidentielle américaine. On peut également avoir pour but de constituer une base de données structurée à partir d’un texte. Dans nos démarches, il y a autant d’outils à notre disposition qu’il y a de buts à atteindre.
Ce post a pour objectif d’aider les utilisateurs R débutants qui souhaitent utiliser les expressions régulières pour du text mining afin d’obtenir des données structurées.
Mais qu’est-ce qu’une expression régulière (regex)? C’est un motif de caractères qui permet de décrire des portions de texte (des chaînes de caractères).
Pour de plus amples détails, voici le début de l’article wikipedia sur les expressions régulières, que nous utiliserons ultérieurement pour différents exemples:
« En informatique, une expression régulière ou expression normale1 ou expression rationnelle ou motif, est une chaîne de caractères, qui décrit, selon une syntaxe précise, un ensemble de chaînes de caractères possibles. Les expressions régulières sont issues des théories mathématiques des langages formels des années 1940. Leur capacité à décrire avec concision des ensembles réguliers explique qu’elles se retrouvent dans plusieurs domaines scientifiques dans les années d’après-guerre et justifie leur adoption en informatique. Les expressions régulières sont aujourd’hui utilisées par les informaticiens dans l’édition et le contrôle de texte ainsi que dans la manipulation des langues formelles que sont les langages informatiques.«
Caractères littéraux et méta-caractères
Tout d’abord, il faut faire un peu de théorie car à syntaxe précise, règles précises. Plusieurs types de caractères sont à prendre en compte, les caractères littéraux et les méta-caractères.
- les caractères littéraux (ou signes littéraux): c’est un caractère que l’on comprend dans son sens premier. Par exemple, les caractères littéraux que sont les lettres b o n constitue le mot bon. Un caractère littéral s’identifie à lui-même. Dans l’exemple ci-dessous le terme Nacher n’est présent que dans la première chaîne de caractères.
1 2 3 |
Str <- c("R. Nacher-Könitz", "A.-A. $Koziv", "C. Röss", "C. Reichl", "W. Wheider", "T. Ihnt", "K. Ein" ) grep("Nacher", Str) [1] 1 |
Attention, une expression régulière est sensible à la casse (le fait d’être en majuscule ou minuscule)!
1 2 |
grep("nacher", Str) integer(0) |
- les méta-caractères sont des caractères qui ne sont pas considérés dans leur sens premier. Un bon exemple c’est le caractère $ qui ne va pas identifier le signe $, mais chercher des caractères depuis la fin de la chaine de caractère. Dans l’exemple ci dessous, R trouve des caractères dès la fin de chaque chaîne de caractères.
1 2 |
grep("$", Str) [1] 1 2 3 4 5 6 7 |
Par ce qu’il est utile de pouvoir rechercher ces méta-caractères à coup sûr, on peut les échapper, c’est à dire les rendre littéraux en utilisant le \. ATTENTION, avec R, contrairement à d’autres langages, pour faire comprendre que \ sert à échapper un méta-caractère il faut l’échapper lui même dans cette chaîne de caractère. Ici dans « \\$ » le premier \ sert à échapper le second \ afin que R échappe le $ (NB: ce post est dédié aux expressions régulières utilisées avec R, des variations existent avec un autre langage que R). R trouve dans l’exemple ci-dessous le caractère dollars dans la seconde chaîne de caractères.
1 2 |
grep("\\$", Str) [1] 2 |
Les méta-caractères sont les suivants: [ ] \ – ^ $ . ? * + { } ( ) |
Pour conclure, rechercher un \ dans une chaîne de caractère sera donc possible comme suit.
1 2 |
grep("\\\\" , "A.-A. \\Koziv") [1] 1 |
Les classes de caractères
Nous savons donc rechercher des caractères littéraux, des méta-caractères échappés, c’est un bon début, mais le force (la magie) des expressions régulières vient des classes de caractères combinées aux méta-caractères. Une classe de caractère est définie par [ ] dans lequel un contenu est défini.
Voici un premier exemple très simple. Comment rechercher les mots composés de la séquence de caractère littéraux « ex » qui peuvent se trouver au début du mot ou juste après la première lettre.
1 2 3 4 5 |
library(stringr) Str <- "En informatique, une expression régulière ou expression normale ou expression rationnelle ou motif, est une chaîne de caractères, qui décrit, selon une syntaxe précise, un ensemble de chaînes de caractères possibles. Les expressions régulières sont issues des théories mathématiques des langages formels des années 1940. Leur capacité à décrire avec concision des ensembles réguliers explique qu’elles se retrouvent dans plusieurs domaines scientifiques dans les années d’après-guerre et justifie leur adoption en informatique. Les expressions régulières sont aujourd’hui utilisées par les informaticiens dans l’édition et le contrôle de texte ainsi que dans la manipulation des langues formelles que sont les langages informatiques." str_extract_all( Str,"[a-zA-Z]?ex[a-z]+") [[1]] [1] "expression" "expression" "expression" "expressions" "explique" "expressions" "texte" |
- La classe de caractère [a-zA-Z] est définie par [ ] et son contenu est a-zA-Z. Ici, on test la présence d’une lettre minuscule ou majuscule dans les gammes de lettres « a » à (-) « z » et « A » à (-) « Z » (rappelez-vous une expression régulière est sensible à la casse). Si je cherche un mot en début de phrase ce mot commence par une majuscule. En fait ici ce test est réalisé pour chaque caractère rencontré.
- Mais la présence du « ? » rend cette classe de caractères optionnelle. Elle peut être présente ou non.
- Ensuite qu’une lettre ait été détectée ou pas, on recherche les caractères littéraux « ex », dans cet ordre. Si le test invoqué par « [a-zA-Z] » est réalisé pour l’ensemble des caractères rencontrés, « ex », cette nouvelle portion de l’expression régulière, va contraindre le premier test en ciblant la lettre présente (ou non) avant la séquence de caractères littéraux « ex ».
- enfin une nouvelle classe de caractère ([a-z]) est recherchée avec un indice de quantité « + » qui signifie que cette classe est présente au moins une fois ou plusieurs fois (c’est à dire qu’il y a une ou plusieurs lettres) juste après « ex ».
Attention
Les gammes citées ici « a-z » sont à titre d’exemple, « a-d » fonctionne également et désigne les lettres a b c d. La gamme « 0-9 » est également utilisable.
Autre exemple, cette fois on cherche les mots contenant la séquence de caractères littéraux « re » quelque soit leur emplacement dans le mot.
1 2 3 4 5 |
library(stringr) Str <- "En informatique, une expression régulière ou expression normale ou expression rationnelle ou motif, est une chaîne de caractères, qui décrit, selon une syntaxe précise, un ensemble de chaînes de caractères possibles. Les expressions régulières sont issues des théories mathématiques des langages formels des années 1940. Leur capacité à décrire avec concision des ensembles réguliers explique qu’elles se retrouvent dans plusieurs domaines scientifiques dans les années d’après-guerre et justifie leur adoption en informatique. Les expressions régulières sont aujourd’hui utilisées par les informaticiens dans l’édition et le contrôle de texte ainsi que dans la manipulation des langues formelles que sont les langages informatiques." str_extract_all( Str,"[[:alpha:]]*re([a-z]+)?") [[1]] [1] "expression" "régulière" "expression" "expression" "caractères" "caractères" "expressions" "régulières" "décrire" "retrouvent" "guerre" "expressions" "régulières" |
- La classe de caractère [[:alpha:]] cherche la présence de toute lettre, majuscule ou minuscule.
Attention
L’expression [:alpha:] seule n’a pas le même sens que dans une classe de caractères telle que [[:alpha:]]. L’expression [:alpha:] seule recherche les caractère littéraux « : a l p h ». Les cas présentés ci-dessous explicitent cette différence, mais il est à noter que pour l’exemple ci-dessus il n’y aurait pas d’incidence du fait du quantifieur « * » (voir point suivant). De nombreux groupes de caractères ont été créés et sont présentés ici.
1 2 3 4 5 6 7 8 9 |
grep("[:alpha:]", c("Hier,", "nous", "sommes", "partis", "à", "la" ,"plage")) [1] 4 6 7 grep("[[:alpha:]]", c("Hier,", "nous", "sommes", "partis", "à", "la" ,"plage")) [1] 1 2 3 4 5 6 7 grep("[:punct:]", c("Hier,", "nous", "sommes", "partis", "à", "la" ,"plage")) [1] 2 4 7 grep("[[:punct:]]", c("Hier,", "nous", "sommes", "partis", "à", "la" ,"plage")) [1] 1 |
- Le « * » qui fait suite à la classe de caractère [[:alpha:]] signifie que ce qu’elle désigne peut être absent ou présent une fois ou plus d’une fois.
- Ensuite, qu’une lettre ou plusieurs lettres aient été précédemment détectées ou non, on recherche les caractères littéraux « re ». En d’autres termes, on peut aussi identifier un mot commençant par re.
- Enfin nous trouvons l’expression « ([a-z]+)? » qui n’est volontairement pas la plus simple mais nous permet de comprendre l’emploi de certains méta-caractères. Cette expression est équivalente à « [a-z]* ». Dans un premier temps « [a-z]+ » cible une ou plusieurs lettres (présentes à la suite) dans la gamme de caractères littéraux comprenant les lettres de « a » à (-) « z ». Contrairement à l’expression « [a-z]* », si on en restait là, on ne pourrait pas sélectionner un mot se terminant par « re » car au moins une lettre minuscule devrait être présente après « re ». Ceci est contourné en englobant l’expression « [a-z]+ » entre ( ) ce qui permet de constituer un groupe syntaxique auquel il est possible d’ajouter un quantifieur « ? » rendant ce groupe optionnel.
On peut chercher à soustraire des caractères d’une recherche. Cette objectif est réalisé en utilisant le méta-caractère « ^ » à l’intérieur d’une classe de caractères comme ceci [^….].
1 2 3 4 5 6 7 |
str_extract_all("maintenant ou jamais","[^aeiouy]") [[1]] [1] "m" "n" "t" "n" "n" "t" " " " " "j" "m" "s" str_extract_all("maintenant ou jamais","[^aeiouy ]") [[1]] [1] "m" "n" "t" "n" "n" "t" "j" "m" "s" |
- Dans cet exemple je soustraie dans un premier temps les voyelles, ce qui conserve les espaces entre les lettres.
- Dans un deuxième temps je soustraie les voyelles et les espaces.
Il est possible de rechercher une expression régulière en début ou en fin d’une chaîne de caractères et c’est possible avec respectivement les deux méta-caractères suivant « ^ » et « $ ».
1 2 3 4 |
grep("^[aeiouy]", c("Hier,", "nous", "sommes", "allés", "au", "cinéma")) [1] 4 5 grep("[aeiouy]$", c("Hier,", "nous", "sommes", "allés", "au", "cinéma")) [1] 5 6 |
- Dans le premier exemple nous recherchons les mots commençant par une voyelle avec le méta-caractère « ^ » placé au début de l’expression régulière.
- Dans le second exemple nous recherchons les mots se terminant par une voyelle avec le méta-caractère « $ » placé à la fin de l’expression régulière.
En terme de positionnement, un dernier méta-caractère doit être cité il s’agit du « . ». Celui-ci désigne tout caractère présent à une position définie, c’est à dire après une lettre ciblée par exemple. Quelques exemples pour bien comprendre:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
str_extract_all("Regular expression",".") [[1]] [1] "R" "e" "g" "u" "l" "a" "r" " " "e" "x" "p" "r" "e" "s" "s" "i" "o" "n" str_extract_all("12.- €/kg",".") [[1]] [1] "1" "2" "." "-" " " "€" "/" "k" "g" str_extract_all("Regular expression","^.") [[1]] [1] "R" str_extract_all("Regular expression","\\b.") [[1]] [1] "R" " " "e" str_extract_all("Regular expression","[g].") [[1]] [1] "gu" |
- Dans les deux premiers exemples, comme aucune expression n’est mentionnée avant le « . », celui-ci teste toute les positions et retourne le caractère rencontré.
- Le troisième exemple se positionne en début de la chaîne de caractères avec le méta-caractère « ^ » et « . » cible le premier caractère rencontré.
- Le quatrième exemple fait intervenir un autre méta-caractère que je ne vais pas formaliser complètement, je vous incite à visiter ce site pour plus d’informations. « \\b » permet de se positionner aux limites des mots (début et fin), « . » va donc cibler le caractère présent à ces limites. On identifie donc pour le premier mot la lettre « R » et » » (l’espace). Pour le second mot, au début de celui-ci « e », mais comme aucun caractère n’est présent à la suite de ce mot, aucun caractère n’est retourné.
- Le dernier exemple cible le caractère littéral « g » présent dans le premier mot et le méta-caractère « . » identifie le caractère qui suit directement c’est à dire la lettre « u ».
Le ciblage alternatif est également possible avec le méta-caractère « | ». pour comprendre le fonctionnement du ciblage alternatif, voici un exemple présenté de manière segmentée.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
library(stringr) Str <- "En informatique, une expression régulière ou expression normale ou expression rationnelle ou motif, est une chaîne de caractères, qui décrit, selon une syntaxe précise, un ensemble de chaînes de caractères possibles. Les expressions régulières sont issues des théories mathématiques des langages formels des années 1940. Leur capacité à décrire avec concision des ensembles réguliers explique qu’elles se retrouvent dans plusieurs domaines scientifiques dans les années d’après-guerre et justifie leur adoption en informatique. Les expressions régulières sont aujourd’hui utilisées par les informaticiens dans l’édition et le contrôle de texte ainsi que dans la manipulation des langues formelles que sont les langages informatiques." str_extract_all( Str,"[[:lower:]]*eu[a-z]*") [[1]] [1] "eur" "plusieurs" "leur" str_extract_all( Str,"[[:upper:]]?eu[a-z]*") [[1]] [1] "Leur" "eurs" "eur" str_extract_all( Str,"([[:upper:]]?|[[:lower:]]*)eu[a-z]*") [[1]] [1] "Leur" "plusieurs" "leur" |
- Dans la première partie de l’exemple, dans un premier temps l’expression régulière « [[:lower:]]*eu[a-z]* » que nous reconnaissons en partie cible des lettres minuscules présentes zéro à plusieurs fois suivit de « eu » qui est suivit par zéro ou plusieurs lettres minuscules.
- Dans la seconde partie de l’exemple, dans un premier temps avec l’expression régulière « [[:upper:]]?eu[a-z]* » nous ciblons des lettres majuscules présentes zéro à une fois suivit de « eu » puis par zéro ou plusieurs lettres minuscules.
Comme nous le voyons les deux premières parties de notre exemple ne permettent pas d’extraire les mots en entier du fait qu’ils débutent soit par une lettre majuscule soit pas une ou des lettres minuscules. Le groupe syntaxique suivant ([[:upper:]]?|[[:lower:]]*) séparé par « | » permet lors du ciblage de chercher soit une lettre majuscule précédent « eu » soit zéro ou plusieurs lettres minuscules. Mais attention on ne cible pas toutes les solutions possibles comme par exemple un mot commençant pas une majuscule, suivit par une ou plusieurs lettre minuscule puis « eu » etc… comme les exemples suivant le montrent.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
str_extract_all(c("Lueur", "lueur"),"([[:upper:]]?|[[:lower:]]*)eu[a-z]*") [[1]] [1] "ueur" [[2]] [1] "lueur" str_extract_all(c("Lueur", "lueur"),"([[:upper:]]?.*|[[:lower:]]*)eu[a-z]*") [[1]] [1] "Lueur" [[2]] [1] "lueur" |
- Dans le premier exemple, le mot Lueur est coupé en fonction de la chaine de caractère initiale
- Dans le second exemple on utilise « .* » pour cibler l’éventuelle présence de caractères en nombre de 0 à n situé(s) entre une majuscule et « eu », etc…
NB: cet exemple n’a pour but que d’expliciter l’emploi de « | », on obtient le même résultat sans ciblage alternatif comme suit.
1 2 3 4 5 6 |
str_extract_all(c("Lueur", "lueur"),"[[:alpha:]]*eu[a-z]*") [[1]] [1] "Lueur" [[2]] [1] "lueur" |
A ce stade rappelons un des buts de cette présentation sur les expressions régulières, produire des données structurées. Voici donc un exemple simple. Nous avons une suite de chaîne de caractères comprenant des noms avec leurs initiales, le tout dans un ordre variable avec ou sans espace. Il s’agit de données produites pour l’exemple. Comment automatiser une structuration qui pourrait être ultérieurement exploitable? L’échantillon de données est petit, mais pour un ensemble plus conséquent que celui, l’effort de programmation en vaudrait la peine.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
STRINGS <- c("Nacher1,2 R. Koziv5 A. A. Röss1 C. Reichl3 C. Wheider1,4 W. Ihnt1 T. Ein1 K.", "Nacher R. Koziv A.A. Röss C. Reichl C. Wheider W. Ihnt T. Ein K.", "NacherR.KozivA.A.RössC.ReichlC.WheiderW.IhntT.EinK.", "R. Nacher A. A. Koziv C. Röss C. Reichl W. Wheider T. Ihnt K. Ein", "R.A.NacherA.A.KozivC.RössC.ReichlW.WheiderT.IhntK.Ein", "R. A. Nacher A. A. Koziv C. Röss C. Reichl W. Wheider T. Ihnt K. Ein", "Nacher-Könitz R. Koziv A. A. Röss C. Reichl C. Wheider W. Ihnt T. Ein K.", "Nacher-Könitz R. Koziv A.-A. Röss C. Reichl C. Wheider W. Ihnt T. Ein K.", "R. Nacher-Könitz A.-A. Koziv C. Röss C. Reichl W. Wheider T. Ihnt K. Ein") Fam_nam <- str_extract_all(INT,"[[:upper:]]{1}[[a-zšžþàáâãäåçèéêëìíîïðñòóôõöùúûüý]]{1,}(-[[:upper:]]{1}[[a-zšžþàáâãäåçèéêëìíîïðñòóôõöùúûüý]]{1,})?") Initials <- str_extract_all(INT,"[[:upper:]]{1}\\.(-?[[:upper:]]{1}\\.)?") Names <- do.call(rbind, Fam_nam) Given_N <- do.call(rbind, Initials) List <- lapply(1:length(Names[, 1]), function(i){ sapply(1:length(Names[1, ]), function(j)paste(Names[i, j], Given_N[i, j], sep=" ")) } ) do.call(rbind, List) [,1] [,2] [,3] [,4] [,5] [,6] [,7] [1,] "Nacher R." "Koziv A.A." "Röss C." "Reichl C." "Wheider W." "Ihnt T." "Ein K." [2,] "Nacher R." "Koziv A.A." "Röss C." "Reichl C." "Wheider W." "Ihnt T." "Ein K." [3,] "Nacher R." "Koziv A.A." "Röss C." "Reichl C." "Wheider W." "Ihnt T." "Ein K." [4,] "Nacher R." "Koziv A.A." "Röss C." "Reichl C." "Wheider W." "Ihnt T." "Ein K." [5,] "Nacher R.A." "Koziv A.A." "Röss C." "Reichl C." "Wheider W." "Ihnt T." "Ein K." [6,] "Nacher R.A." "Koziv A.A." "Röss C." "Reichl C." "Wheider W." "Ihnt T." "Ein K." [7,] "Nacher-Könitz R." "Koziv A.A." "Röss C." "Reichl C." "Wheider W." "Ihnt T." "Ein K." [8,] "Nacher-Könitz R." "Koziv A.-A." "Röss C." "Reichl C." "Wheider W." "Ihnt T." "Ein K." [9,] "Nacher-Könitz R." "Koziv A.-A." "Röss C." "Reichl C." "Wheider W." "Ihnt T." "Ein K." |
A bientôt,
g.