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") }

9 commentaires:

  1. Pas besoin de parser google a toutes les infos du film sur senscritique avec son api restfull gratuite :

    // DOCU : https://developers.google.com/custom-search/json-api/v1/using_rest
    // POUR CREER MOTEUR pour senscritique : https://cse.google.com/cse/all

    Exemple voir le panel de droite où google référence le film sur senscritique :
    https://www.google.fr/search?source=hp&ei=dap9WsuCMYXbwQK1hIWACg&q=casablanca+by+night+film&oq=casablanca+by+night+&gs_l=psy-ab.3.0.0l5j0i22i30k1l5.1002.16736.0.20056.28.16.4.8.9.0.175.1278.12j3.15.0....0...1c.1.64.psy-ab..1.27.1312...0i13k1j0i13i30k1.0.ADcWrJaO4FM

    RépondreSupprimer
  2. Merci pour ces éléments.
    Cependant j'ai du mal à cerner comment l'api google et un moteur custom peuvent répondre à mon besoin d'extract de ma collection SensCritique.
    En effet, Google ne référence pas ma collection (contenu dynamique non référencé).
    Ok pour tirer profit de l'api Google (c'est tellement moins crado que du scraping) mais comment lister les éléments de ma collection pour ensuite la passer à Google ?

    RépondreSupprimer
  3. j'avais lu en biais ! Effectivement dans votre cas l'api rest de google ne vous servira pas. Je pensais que vous vouliez rechercher des films et en extraire les infos.

    RépondreSupprimer
  4. Bonjour,

    Super article et merci beaucoup pour le script.
    Je suis développeur mais n'y connais rien en R.

    Je suis sur Windows. J'ai installe R ainsi que RStudio.
    J'ai installe les librairies rvest, stringr et xml2 mais quand j'execute le script j'obtiens le message

    Page 1 en cours de traitement
    Error in isTRUE(trim) : object 'type' not found

    Est-ce que le script marche toujours de votre cote? Est-ce moi qui ai oublie d'installer une libraire?
    Je ne vois pas bien comment corriger cette erreur lorsque je copie colle le message dans Google.

    Je tiens enormement a mes data sur Sens Critique et j'aimerai comme vous, les sauvegarder dans un fichier Excel
    Merci beaucoup.

    RépondreSupprimer
  5. Bonjour,
    Merci pour l'intérêt porté à l'article et au script !
    Je l'ai réutilisé récemment en utilisant le script publié ici et effectivement il y a quelques anomalies (que j'ai corrigé pour le faire fonctionner de nouveau) !
    La première vous l'avez bien comprise, c'est qu'il ne suffit pas de charger les librairies mais il faut aussi les installer !
    Ensuite, il y en a une seconde, moins grave, j'ai simplement oublié d'initialiser certaines variables. Une fois que le script aura tourné une fois dans votre environnement, ça ne posera plus problème.
    La dernière est plus subtile, c'est que pour la publication du script dans le blog, j'ai fait des retours à la ligne pour faciliter la lisibilité du code. Apparemment, R ne les supporte pas. Il faut simplement remettre sur une ligne les instructions qui sont coupés en deux (instruction terminée par une virgule ou %>% en fin de ligne par exemple).
    Dites moi si ça fonctionne ! Au pire, j'ai le code qui fonctionne dans un script quelque part sur un autre PC et je peux vous le partager.

    RépondreSupprimer
  6. Dernier debug :
    if(!require(rvest)){
    install.packages("rvest")
    library(rvest)
    }
    library(stringr)

    # Paramétrage
    utilisateur <- "Lukeskyforges"
    page_init <- 1
    page_fin <- 2

    # Init
    date <- 0
    commentaire <- 0
    artiste <- 0
    title <- 0
    type <- 0
    note <- 0


    # 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\t"]
    note <- str_replace_all(note, "^\r\n\t\t\t\t\t\t$" , "Envie")
    note <- str_replace_all(note, "[\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t]" , "")
    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")

    }

    RépondreSupprimer
  7. Ce commentaire a été supprimé par l'auteur.

    RépondreSupprimer
  8. Hello Unknown!
    Pour ta problématique, ça a l'air plutôt simple.
    Voici l'URL de recherche avec trois critères : nom de l'œuvre et plage min max de l'année de l'oeuvre
    https://www.senscritique.com/search?q=6eme%20sens&categories[0][0]=Films&year[min]=1999&year[max]=1999
    Avec deux inputs : nom du film et année de sortie, je pense que c'est suffisant à 90% pour matcher en premier résultat la fiche du film souhaité ! 90% car il pourrait avoir divergence sur l'année de sortie du film (des films sortis en 2022 sont parfois taggués en 2021...). Si on élargit la plage d'année, ça a tendance à retourner le bon résultat parfois en second rang si une autre œuvre matche les mêmes mots clés...

    RépondreSupprimer
  9. Pour info, SensCritique redéveloppe son site ! Et... il se repose enfin sur une API !! Si ça intéresse, j'ai codé l'équivalent de mon script de scrapping. En python, cette fois ci.

    RépondreSupprimer