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.

Récupération de textes au format html

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.

Récupération des liens sortants d'une page web

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"

Filtrage des résultats

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"

Téléchargement des pages

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.

Corpus de textes au format html

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).

Source

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"

Lecture

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).

Définition du corpus

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.

Date de publication

28 novembre 2012

Mots clés

enseignement

R

tm

text mining