Le package R tm propose un cadre général pour l'analyse exploratoire de textes (text mining) dans R. Son utilisation est relativement complexe et la documentation est essentiellement rédigée en anglais. L'objectif de ce premier billet est de montrer comment construire un corpus dans tm, première étape pour analyser un ensemble de textes. La difficulté principale réside dans la conversion entre le format natif des textes et un format accepté par tm.
On cherche à construire un corpus à partir d'un ensemble de textes disponibles sur le web sous forme de liens réunis sur une ou plusieurs pages. Pour cela, on s'appuie sur les packages RCurl et XML.
Prenons comme exemple la récupération de tous les billets du présent site. La
liste des billets est disponible ici. On doit donc récupérer le code source de
la page web puis extraire les attributs href
des balises HTML a
. Pour ce
faire, on utilise le code proposé dans les exemples du package XML, en
définissant une fonction d'extraction comme suit :
library(XML)
getLinks <- function() {
links <- character()
list(a = function(node, ...) {
links <<- c(links, xmlGetAttr(node, "href"))
node
},
links = function() links)
}
Cette fonction renvoie une liste utilisable comme handler dans l'analyseur
syntaxique de fichiers html (htmlTreeParse
). À chaque fois que l'analyseur
rencontre une balise a
, la fonction définie comme membre de nom a
de la
liste est exécutée : elle récupère le contenu de l'attribut href
et l'ajoute
dans la liste links
.
On applique la fonction de la façon suivante :
parseResult <- htmlTreeParse("http://test.apiacoa.org/blog/posts.fr.html",handlers=getLinks())
myLinks <- parseResults$links()
Au moment où j'écris ces lignes, le résultat est le suivant :
> myLinks \[1\] "http://apiacoa.org/" \[2\] "http://test.apiacoa.org/index.fr.html" \[3\] "http://test.apiacoa.org/blog/index.fr.html" \[4\] "http://test.apiacoa.org/research/index.fr.html" \[5\] "http://test.apiacoa.org/teaching/index.fr.html" \[6\] "posts.en.html" \[7\] "#" \[8\] "posts.en.html" \[9\] "news.fr.html" \[10\] "/blog/2012/11/a-propos-de-modl.fr.html" \[11\] "/blog/2012/10/publications-in-2012.en.html" \[12\] "/blog/2012/10/amazing-support-from-the-jekyll-community.en.html" \[13\] "/blog/2012/09/phd-defense.en.html" \[14\] "/blog/2012/09/multilingual-jekyll.en.html" \[15\] "/blog/2012/09/migration-vers-jekyll.fr.html" \[16\] "/blog/2012/08/moving-to-jekyll.en.html" \[17\] "http://test.apiacoa.org/contact.fr.html" \[18\] "http://test.apiacoa.org/thanks.fr.html"
Comme on ne s'intéresse qu'aux billets, on doit filtrer la liste obtenue. Ceci
peut être fait simplement en s'appuyant sur la fonction grep
qui cherche des
expressions rationnelles dans des chaînes de caractères. Ici, il suffit de ne
conserver que les liens qui commencent par /blog/
, soit simplement :
myLinks <- myLinks[grep("^/blog/",myLinks)]
ce qui donne
> myLinks \[1\] "/blog/2012/11/a-propos-de-modl.fr.html" \[2\] "/blog/2012/10/publications-in-2012.en.html" \[3\] "/blog/2012/10/amazing-support-from-the-jekyll-community.en.html" \[4\] "/blog/2012/09/phd-defense.en.html" \[5\] "/blog/2012/09/multilingual-jekyll.en.html" \[6\] "/blog/2012/09/migration-vers-jekyll.fr.html" \[7\] "/blog/2012/08/moving-to-jekyll.en.html"
Il ne reste plus qu'à télécharger les pages correspondantes et à les
sauvegarder dans un dossier local. Je choisis ici de mettre les fichiers dans
le dossier local tm-html
en ne conservant qu'un niveau de représentation
(alors que le blog est organisé hiérarchiquement). Il faut donc transformer un
nom de la forme /blog/2012/11/truc.html
en quelque chose de la forme
/tm-html/2012_11_truc.html
(par exemple). Pour ce faire, on utilise les
fonctions sub
et gsub
qui réalisent des substitutions en s'appuyant sur
les expressions rationnelles. Le code de téléchargement est alors :
library(RCurl)
dir.create("tm-html")
for(i in seq_along(myLinks)) {
# téléchargement du fichier
theFile <- getURL(paste("http://test.apiacoa.org",myLinks[i],sep=""),.encoding="utf-8")
write(theFile,file=file.path("tm-html",gsub("/","_",sub("/blog/","",myLinks[i]))))
}
On remarque l'utilisation de la fonction paste
pour construire l'URL
utilisée par getURL
. En effet, les liens sont relatifs dans la page web
d'origine et ne contiennent donc pas le nom de l'hôte qui héberge le site. Il
faut donc le rajouter avant de demander la récupération des pages.
Je considère maintenant qu'on dispose d'un ensemble de textes au
format HTML, rangés dans un dossier (par exemple à partir d'une archive zip
(comme celle-ci). Chaque fichier porte un nom se terminant par .html
. Les
textes peuvent avoir été obtenus par la méthode ci-dessus ou d'une autre
façon (avec l'outil wget par exemple).
Si les textes sont dans le dossier tm-html
par exemple, on peut définir une
source de textes correspondante de la façon suivante (en supposant que le
package chargé) :
mySource <- DirSource("tm-html",pattern="html")
Le tableau mySource$Names
contient alors la liste des fichiers sélectionnés
(sans le dossier), par exemple ici :
> mySource$Names \[1\] "201208moving-to-jekyll.en.html" \[2\] "201209migration-vers-jekyll.fr.html" \[3\] "201209multilingual-jekyll.en.html" \[4\] "201209phd-defense.en.html" \[5\] "201210amazing-support-from-the-jekyll-community.en.html" \[6\] "201210publications-in-2012.en.html" \[7\] "201211a-propos-de-modl.fr.html"
Malheureusement, tm ne supporte pas le format HTML proprement dit par
défaut. La fonction readXML
s'appuie demande en effet que les documents
soient au format XML strict et ne peut pas lire facilement des documents HTML,
en particulier quand ceux-ci contiennent des erreurs (ce qui est
classique). Pour contourner ce problème, on définit la fonction de lecture
suivante :
library(XML)
mySource <- DirSource("tm-html",pattern="html")
myHTMLReader <- function(elem,language,id) {
htree <- htmlParse(elem$content, asText = TRUE)
text <- xpathSApply(htree,
"//text()[not(ancestor::script)][not(ancestor::style)][not(ancestor::noscript)][not(ancestor::form)]",
xmlValue)
PlainTextDocument(text, id = id, language = language)
}
La conversion au format texte est réalisée par l'appel xpathSApply
, comme
suggéré dans ce billet (cf le code correspondant).
Il ne reste plus qu'à définir le corpus comme suit :
myCorpus <- Corpus(mySource,readerControl = list(reader=myHTMLReader))
On peut ensuite manipuler le corpus comme suggéré dans ce billet et plus généralement avec toutes les techniques disponibles dans tm.