dimanche 2 avril 2017

API SensCritique, web scraping avec rvest


Contexte

Ouvert au public en 2011, SensCritique s'est vite élevé au rang de référence française du rating de contenu culturel. La pratique consiste à évaluer du contenu en partageant son avis afin de favoriser le bouche à oreille culturel. Dans le paysage du web francophone, Allociné trône en bonne place pour le 7ème art. Babelio, lui, fédère les amateurs de lecture et BetaSeries répond à la problématique majeure des binge watcher : garder le fil des séries en cours et à venir. SensCritique se veut transverse en proposant de réunir des univers représentés de manière éparse : films, séries, jeux-vidéos, livres, bds et enfin musique.

Très tôt, SensCritique a eu la volonté de proposer de nouvelles expériences utilisateurs avec dans le pipe tout un lot d'innovation. Le lab, laboratoire d'expérimentation d'applications, fut l'initiative qui affichait cette volonté d'aller plus loin avec des démonstrateurs. Sur la base des données utilisateurs et de contenu, on parlait d'outil de data visualisation (ex : afficher les corrélations entre l'âge ou le sexe du noteur et la note attribuée). Mais l'arlésienne qui nous intéresse, promise dès 2012, c'est la légendaire API publique. En 2017, la promesse tient toujours et une petite communauté, dont je fais partie, s'impatiente.

L'API publique, messie qui n'arrivera jamais

La communication de SensCritique n'est pas très claire à propos du sujet. Sur Twitter, l'espoir est régulièrement entretenu alors qu'on peut lire ailleurs que «l’équipe dit y penser depuis longtemps mais ne pas l’avoir prévu dans l’immédiat, les membres n’en étant pas demandeurs.». Cela m'a donc fait m'interroger sur les raisons de retarder sa mise à disposition. Trois axes de réflexion : les performances de l'infrastructure, l'importance de la donnée et enfin la priorité quant au business model.

Les performances

SensCritique se repose sur un très large catalogue alimenté par d'autres bases ainsi que par l'enrichissement que peut effectuer les utilisateurs eux-mêmes en mode wiki. Peut être que j'ai des problèmes de navigateur pour afficher un si riche contenu, mais de mon ressenti, le requêtage sur SensCritique est lent. Mettre à disposition une API publique implique une infrastructure solide qui puisse tenir la charge sans faire effondrer les performances globales pour l'utilisateur qui utilise le service classiquement. Cela implique aussi une politique d'utilisation du service qui permette d'éviter toutes dérives. L'API publique un sujet qui apporte plus de problématiques que de satisfaction.

La donnée

SensCritique est une mine d'or en terme de données. Mettre à disposition l'information de manière libre et gratuite, c'est perdre le monopole sur cette dernière. Pire encore, c'est donner l'opportunité à un tiers de l'exploiter à des fins commerciales (ou non) pour un service que SensCritique pourrait proposer le premier.

Le business model

SensCritique a du pour continuer à exister s'imaginer un business model. L'API publique (gratuite, j'entends) n'assurant pas de retour sur investissement, il est normal que la priorité ne se place pas en premier lieu sur ce terrain là. 

Le premier investissement a concerné la fidélisé utilisateur : enrichissement du contenu (la musique, série épisodes par épisodes, etc), ajout de nouveaux services (calendriers, posts, etc) et enfin alignement sur les nouveaux usages : appli mobile Andoid (ios toujours en cours de développement). Pour la petite histoire, la problématique de la mobilité avait été mal adressée au départ avec une copie du site version mobile.

Le second investissement (à l'instar de Facebook pour les professionnels) consiste à exploiter les données des utilisateurs pour proposer à ces derniers et aux professionnels (annonceurs) un service adapté. On notera que l'intégration de la publicité ciblée dans le site est plutôt bien pensé. La première, invasive, se traduit par un bandeau qui permet la diffusion de vidéos qui invite l'utilisateur à rajouter le contenu dans sa liste d'envie. Enfin, la seconde se traduit par un système de notifications et de participation à des concours. La publicité est ludique, au service de l'internaute.


Back-up de mes données

Mon besoin n'est pas directement lié avec la mise à disposition d'une API. Cependant une API favoriserait beaucoup sa résolution. En effet, en tant qu'utilisateur de SensCritique, je confie au service de la donnée en attribuant des notes aux œuvres référencées. Mon souhait est de pouvoir externaliser cette donnée (dans excel) pour la savoir en sécurité chez moi puis à terme la manipuler. Ce que le site ne propose pas actuellement. Pour répondre à mon besoin, j'ai choisi d'utiliser la librairie rvest (à prononcer harvest, la moisson en français) pour mettre en pratique mes connaissances acquises sur R (dans le cadre du machine learning) et pour la simplicité (et rapidité) d'adressage qu'elle m'apporte du fait qu'il suffit juste d'un client R sur mon poste.

Un bref état de l'art permet d'observer que la communauté de membres (NitriKx/senscritique-api, disceney/SensCritiqueAPI, thcolin/senscritique-api) ne reste pas inactive. L'analyse du code montre néanmoins que les solutions sont soient partielles soient obsolètes (se reposent sur une structuration html que le site a fait évoluer depuis). De plus, monter une solution autour de wamp pour faire de l'extraction de données one-shot me paraît être une perte d'énergie. Des services en ligne existent comme dexi.io offrant la possibilité de mettre en place un pipeline d'extraction s'achevant sur de l'écriture dans une spreadsheet google. Bonne idée pour une solution qui se veut industrialisée. Enfin, pour un usage court-termiste, Chrome propose aussi son plugin Web Scraper. 

Web scraping de SensCritique avec rvest, ses limites

Principe
La fonction principale (de rvest) sur lequel se repose mon script est html_nodes. A partir d'un sélecteur xpath, on extrait le noeud (titre, date, type, artiste, note, infos complémentaires) qui nous intéresse pour l'ensemble des items (œuvres listées dans ma bibliothèque) affichés sur les pages de ma collection d’œuvres.

Limite et contournements
SensCritique étant une base de données en perpétuelle complétion, certains items peuvent être partiellement complétés : une date peut venir à manquer. Points noirs discutés sur les forums, html_nodes n'est pas en mesure de détecter un noeud manquant : il va simplement lister un à un les nœuds qu'il rencontre. La première alternative, moyennement performante, consiste à extraire à la main (sans rvest) les items pour les parser chacun à leur tour. La seconde, que j'ai employé, tire profit du mode wiki de SensCritique (sauf pour la musique) : c'est win-win, je complète ce qui manque dans la base du site en assurant d'une meilleure qualité de ce que j'extraie.

Ethique
Dernier point de vigilance sur la moralité de l'entreprise. Avec un rapide coup d’œil dans les CGU, SensCritique n'évoque rien à propos de la consommation de ses données et l'éventuelle sollicitation de ses serveurs par un robot. Dans le doute, pour ne pas faire apparaître la démarche comme du hacking, on sollicite les serveurs avec un délai raisonnable pour éviter un éventuel blocage ip. Enfin, on utile l'outil pour son usage strictement personnel. Voila, bon scraping !

Afficher / Masquer
library(rvest) library(stringr) # Paramétrage utilisateur <- "Lukeskyforges" page_init <- 1 page_fin <- 2 # Préparation URL de scrapping url_sens <- "https://www.senscritique.com/" url_end <- "/collection/all/all/all/all/all/all/all/all/list/page-" url_pre <- paste(url_sens, utilisateur, url_end,sep="") for (i in page_init:page_fin) { # Log start cat("Page ", i," en cours de traitement\n") # Wait wait <- sample(3:10, 1) Sys.sleep(wait) # Téléchargement de la page i url <- paste(url_pre,i,sep="") fichier <- paste("scrapedpage-",i,".html",sep="") download.file(url, destfile = fichier, quiet=TRUE) content <- read_html(fichier) # Extraction titre title <- html_nodes(content, xpath="//h2[contains(@class, 'elco-title')]/a/text()") %>% html_text(title) # Extraction date date <- html_nodes(content, xpath="//span[contains(@class, 'elco-date')]/text()") %>% html_text(date) %>% str_replace_all("\\(" , "") %>% str_replace_all("\\)" , "") # Extraction type pattern="/(.*?)/" type <- html_nodes(content, xpath="//h2[contains(@class, 'elco-title')]/a/@href") %>% html_text(type) type <- data.frame(matrix(unlist(regmatches(type, regexec(pattern,type))), nrow=2))[2,] type <- unname(unlist(type)) # Extraction artiste artiste <- html_nodes(content, xpath="//a[contains(@class, 'elco-baseline-a')][1]/text()") %>% html_text(artiste) # Extraction note note <- html_nodes(content, xpath="//span[contains(@class, 'elrua-useraction-inner only-child')]/text()") %>% html_text(note) note <- note[note != "\t\t\t\t"] note <- str_replace_all(note, "[\r\n\t]" , "") note <- replace(note, note=="", "Envie") note <- matrix(unlist(note), ncol=3, byrow= TRUE) note <- note[,-c(1,2)] note # Extraction commentaire commentaire <- html_nodes(content, xpath="//p[contains(@class, 'elco-baseline elco-options')]") %>% html_text(commentaire) %>% str_replace_all("[\r\n\t]" , "") # Consolidation du tableau df_page <- data.frame(title, note, date, artiste, type, commentaire, i) colnames(df_page) <- c("Titre","Note", "Date de sortie", "Auteur/Artiste/Réalisateur", "Categorie", "Détails", "Page") # Export dans excel (ajout entête pour première page) if (i == 1) { write.table(df_page, file = "bib.csv", sep = ";", row.names = F, qmethod = "double") } else { write.table(df_page, file = "bib.csv", append = TRUE, sep = ";", row.names = F, col.names = F, qmethod = "double") } # Log end cat("Page ", i," extraite\n") }