{"word_count": 1483, "character_count": 13034}
Aller, deux billets pour parler de scraping, ou harvesting serait peut être plus juste ? Disons qu'on va faire un code automatique pour récupérer des informations en ligne. Plus particulièrement, on va s’intéresser au cas ou les données sont obtenues par une requête manuelle, et le site n'a pas d'API pour générer un joli fichier csv. Pierre-Marie, de la formation Data Science pour l’Actuariat a propose un exemple dont je vais m'inspirer (avec une autre methode pour scraper car j'ai eu des soucis pour installer le logiciel).En 1973 a été lancée en France Prométhée, une base de données sur les incendies de forêts de la région méditerranéenne (couvrant 15 départements du Sud-Est). Oui, Προμηθεύς est ce titan qui a voulu jouer avec le feu... On peut consulter la base sur http://promethee.com/default/incendies,En remplissant les champs, on peut faire une extraction suivant tel ou tel critère. Par exemple, en 2016, en Corse, il y a eu 6 incendies de nature "accidentelle",On le voit, le lien de la page ne change pas... il faut donc trouver une methode pour remplir manuellement les champs, et récupérer les informations. En particulier, on voit qu'on peut avoir accès aux informations relatives aux différents sinistres en cliquant sur la loupeIdéalement, on voudrait un code R, automatique, pour faire ça ! Une première solutions, proposée par Pierre-Marie, est de passer par RSelenium. Le package R (non maintenu sur le CRAN depuis quelques semaines) est base sur Selenium, "Selenium automates browsers" comme dit la publicité. Le code pour scraper une page comme parrequire("devtools")devtools::install_github("ropensci/RSelenium")require(RSelenium)remDr = remoteDriver(remoteServerAddr = "localhost"                       , port = 4445L                      , browserName = "firefox")Le soucis est que ce code ne tourne pas sur mon mac. Il y a de nombreuses pages qui parlent des spécificités de mac (en particulier le soucis du port par défaut 4445, qu'il faut changer). J'ai tout essayé, sans succès.Une autre piste est de passer par un autre packagerequire("devtools")devtools::install_github("ropensci/wdman")wdman, pour "Webdriver Manager", propose aussi de le faire, en s'appuyant sur diverses technologies, selenium, chromedriver, phantomJS binary ou encore geckodriver. Moyennant l'installation de chrome, la seconde option est intéressantelibrary(wdman)cDrv = chrome()eCaps = list(chromeOptions = list(  args = c('--headless', '--disable-gpu', '--window-size=1280,800')))remDr = remoteDriver(browserName = "chrome", port = 4567,                      extraCapabilities = eCaps)remDr$open()qui marche ! On peut lui demander d'aller sur la base promethee, et de faire une copie d’écranremDr$navigate("http://www.promethee.com/default/incendies")remDr$screenshot(display = TRUE)On va ensuite remplir les champs, pour notre requête, sur la page. Par exemple, pour la date de début, on peut remonter au début des années 80webElem = remDr$findElement(using = 'name', 'dtAlerteDeb')webElem$clearElement()webElem$sendKeysToElement(list('01/01/1981'))et terminer fin 2017webElem = remDr$findElement(using = 'name', 'dtAlerteFin')webElem$clearElement()webElem$sendKeysToElement(list('31/12/2017'))On se focalise seulement sur la CorsewebElem = remDr$findElement(using = 'name', 'codeg')webElem$sendKeysToElement(list('CORSE'))et on veut 50 observations par pagewebElem = remDr$findElement(using = 'name', 'nbrLigne')webElem$sendKeysToElement(list('50'))Ah oui, et on va temporiser entre chaque requêtewebElem = remDr$findElement(using = 'name', 'btnSubmit')webElem$submitElement()Sys.sleep(15)remDr$screenshot(TRUE)(c'est une règle assez classique quand on scrap: on évite d'aller trop vite, pour pas se faire repérer...). On va ensuite commencer a regarder ce que l'on demandetext_base = read_html(remDr$getPageSource()[[1]])list_tr = html_nodes(text_base, "tr")a = html_nodes(list_tr[length(list_tr)], "a")page_max = as.numeric(html_text(a[10]))On a environ 620 pages ! avec au moins 10 secondes pour changer de pages, et en rajoutant 15 secondes (on en parlait avant), ca fait quelques heures pour scraper.Que veut-on récupérer en grattant la page ? On peut récupérer les coordonnées de l'incendie (latitude/longitude), les caractéristiques de l'incendie (dates, lieu, surface), et retourner le tout dans une tablescrapping = function(feux, page) {  list_a = html_nodes(read_html(feux), "a")  if (length(list_a) &#62;= 2) {    att = html_attrs(list_a[2])    longitude = as.numeric(str_extract(att, "[0-9]\\.([0-9])*"))    latitude = as.numeric(str_extract(att, "([0-9]){2}\\..([0-9])*"))  } else {    longitude = 0    latitude = 0  }  list_span = html_nodes(read_html(feux), "span")  date = html_text(list_span[2])  commune = html_text(list_span[4])  surface = as.numeric(html_text(list_span[5]))    list_div = html_nodes(read_html(feux), "div")  if (length(list_a) &#62;= 2) {    infos1 = html_text(list_div[3])    infos2 = html_text(list_div[5])  } else {    infos1 = html_text(list_div[2])    infos2 = html_text(list_div[4])  }  alerte = gsub("First alert : |First intervention", "", str_extract(infos1, "First alert : (.)*First intervention"))  deb_inter = gsub("First intervention : |End of intervention", "", str_extract(infos1, "First intervention : (.)*End of intervention"))  fin_inter = gsub("End of intervention : |Origins of the alert", "", str_extract(infos1, "End of intervention : (.)*Origins of the alert"))  code_INSEE = gsub("INSEE code : |Locality", "", str_extract(infos2, "INSEE code : (.)*Locality"))  lieudit = str_trim(gsub("Locality : ", "", str_extract(infos2, "Locality : (.)*")))  liste_feux = data.frame(commune, date, code_INSEE, longitude, latitude, lieudit, surface, alerte, deb_inter, fin_inter, page)  return(liste_feux)}Reste a faire tourner sur les 620 pages. Pour chaque ligne, on récupère les informations !df = data.frame()for (page in 1:page_max) {  text_base = read_html(remDr$getPageSource()[[1]])  list_tr = html_nodes(text_base, "tr")  if (length(html_nodes(read_html(as.character(list_tr[14])), "a")) == 8) {    deb = 15  } else {    deb = 14  }  for (i in deb:(deb+49)) {    feux = as.character(list_tr[i])    df = rbind(df, scrapping(feux, page))  }  if (page == 1) {    webElem = remDr$findElement(using = 'xpath', '//*[contains(concat( " ", @class, " " ), concat( " ", "page_img", " " ))]')  } else {    webElem = remDr$findElement(using = 'xpath', '//*[contains(concat( " ", @class, " " ), concat( " ", "page_img", " " ))][3]')  }  webElem$clickElement()  heure = (floor((page_max-page)*15/60) - floor((page_max-page)*15/60) %% 60) / 60  minute = floor((page_max-page)*15/60) %% 60  pb = progress_bar$new(total = 100)  for (i in 1:100) {    if (i==1) {cat("page", page, "sur", page_max," -  (reste", heure, "h", minute, "min)",  "\n")}    pb$tick()    Sys.sleep(15 / 100)  }  remDr$screenshot(TRUE)}feux = dfÇa y est, on a une base, avec presque 31 000 lignes ! on va ensuite ranger un peu, et clarifiercolnames(feux)[colnames(feux)=="longitude"] = "x"colnames(feux)[colnames(feux)=="latitude"] = "y"feux[, date := as.Date(date, "%d/%m/%Y")]feux[, date_alerte := as.Date(substr(alerte, 1, 10), "%d/%m/%Y")]feux$heure_alerte[nchar(feux$alerte) &#62; 10] = substr(feux$alerte[nchar(feux$alerte) &#62; 10], 12, 16)feux$heure_alerte[!(nchar(feux$alerte) &#62; 10)] = ""feux[, date_deb_inter := substr(deb_inter, 1, 10)]feux[, date_deb_inter := as.Date(date_deb_inter, "%d/%m/%Y")]feux$heure_deb_inter[nchar(feux$deb_inter) &#62; 10] = substr(feux$deb_inter[nchar(feux$deb_inter) &#62; 10], 12, 16)feux$heure_deb_inter[!(nchar(feux$deb_inter) &#62; 10)] = ""feux$heure_deb_inter[feux$heure_deb_inter == "00:00"] = ""feux[, date_fin_inter := substr(fin_inter, 1, 10)]feux[, date_fin_inter := as.Date(date_fin_inter, "%d/%m/%Y")]feux$heure_fin_inter[nchar(feux$fin_inter) &#62; 10] = substr(feux$fin_inter[nchar(feux$fin_inter) &#62; 10], 12, 16)feux$heure_fin_inter[!(nchar(feux$fin_inter) &#62; 10)] = ""feux$heure_fin_inter[feux$heure_fin_inter == "00:00"] = ""feux$annee = year(feux$date_alerte)feux$month = month(feux$date_alerte)Pour voir un peu nos données, rien de tel que des statistiques descriptives. On peut commencer avec du barplot du nombre de feux de forêts par annéeagg_annee = aggregate(alerte~annee, FUN=length, data=feux)colnames(agg_annee)[colnames(agg_annee)=="alerte"] = "Nombre"nb = ggplot(agg_annee) +  geom_bar(aes(x=annee, y=Nombre, fill="Feux de forêts"), stat="identity", width = 0.8) +  scale_colour_manual(name=NULL, values=c("orange")) +  ggtitle("Nombre de feux par an") +  theme(plot.title = element_text(hjust = 0.5, size=14),        legend.position = "none",        axis.text.x = element_text(angle=50, vjust=0.5, size=10),        axis.text.y = element_text(size=12),        axis.title.x = element_blank(),        axis.title.y = element_blank()) +  scale_fill_discrete("") +  scale_x_discrete(limit=seq(1981, 2017, by = 2))ou un barplot de la surface de feux de forêts par aneeagg_surface = aggregate(surface~annee, FUN=sum, data=feux)surf = ggplot(agg_surface) +  geom_bar(aes(x=annee, y=surface, fill="Feux de forêts"), stat="identity", width = 0.8) +  scale_colour_manual(name=NULL, values=c("orange")) +  ggtitle("Surface totale incendiée par an (en ha)") +  theme(plot.title = element_text(hjust = 0.5, size=14),        legend.position = "none",        axis.text.x = element_text(angle=50, vjust=0.5, size=10),        axis.text.y = element_text(size=12),        axis.title.x = element_blank(),        axis.title.y = element_blank()) +  scale_fill_discrete("") +  scale_x_discrete(limit=seq(1981, 2017, by = 2)) +  scale_y_discrete(limit=seq(0, 25000, by = 5000))Et on fait le graphiquerequire(grid)grid.newpage()pushViewport(viewport(layout = grid.layout(1, 2)))define_region = function(row, col){  viewport(layout.pos.row = row, layout.pos.col = col)} print(nb, vp = define_region(1, 1))print(surf, vp = define_region(1, 2))Passons aux choses serieuses, avec des cartes (la partie la plus serieuse, on en conviendra, c'etait la phase initiale de scraping). On commence par des informations relatives a la Corseurl="https://raw.githubusercontent.com/data-PM/Promethee/master/communes_corse-shp.zip"temp="corse.zip"download.file(url, "corse.zip")unzip("corse.zip")communes_corse = sp::spTransform(shapefile("/Users/arthur/communes_corse-shp/communes_corse.shp"), CRS("+proj=longlat +datum=WGS84"))On se limite aux feux de plus de 1000m2feux_min1000m2 = feux[feux$surface &#62; 0.1, ]Maintenant, on peut faire la cartefeux_INSEE = aggregate(x = feux_min1000m2$code_INSEE, by = list(unique.values = feux_min1000m2$code_INSEE), FUN = length)colnames(feux_INSEE)[colnames(feux_INSEE)=="unique.values"] = "insee"communes_corse = merge(x = communes_corse, y = feux_INSEE, by = "insee", all.x = TRUE)communes_corse$x[is.na(communes_corse$x)] = 0max = max(communes_corse$x)communes_corse$x2 = communes_corse$x/max * 100corse2 = gSimplify(communes_corse, tol = 0.001, topologyPreserve = TRUE)corse2 = SpatialPolygonsDataFrame(corse2, communes_corse@data, match.ID = FALSE)opar = par(mar = c(0,0,1.2,0))plot(corse2, col = "#DAE3E6", border = "#8A0641", lwd = 0.7, bg = "#B5D0D0")Le fond de carte (avec les communes) ressemble a caOn peut maintenant passer a la carte choroplethechoroLayer(spdf = corse2, df = corse2@data, var = "x", border = NA,            col = carto.pal("wine.pal", 6), legend.pos = "topleft",            add = TRUE, method = "fisher-jenks", nclass = 6, legend.title.txt = NULL)top_villes = corse2[corse2$nom %in% c("Ajaccio", "Bastia", "Corte", "Porto-Vecchio", "Propriano", "Bonifacio", "Calvi"), ]graphics::points(coordinates(top_villes), pch = 20, cex = 0.5)labelLayer(top_villes, top_villes@data, txt = "nom", cex = 1, pos = 2, font = 4, offset = 0.5)layoutLayer(title = "Nombre de feux de forêts depuis 1981 (minimum 1000 m2)",            source = "Source : 1973-2018 Prométhée ©",            author = NULL, scale = 10, frame = FALSE, col = "#cdd2d4", coltitle = "#8A5543")Comme souvent avec R, plusieurs formats sont envisageables. On peut passer a du ggplot2, mais on commence par aller chercher un shapefile global de la Corsecorse = shp_zip("https://raw.githubusercontent.com/data-PM/Promethee/master/corse-shp.zip", "corse")corse = spTransform(corse, CRS("+proj=longlat +datum=WGS84"))corse = fortify(corse)p=data %&#62;%  ggplot() +  geom_polygon(data=corse, aes(x=long, y = lat, group = group), fill="grey", alpha=0.3) +  geom_point(data=feux_min100ha, aes(x=x, y=y, size=surface, color=surface, text=mytext, alpha=surface) ) +  labs(title = "\nPrincipaux feux de forêts depuis 1981 \n (plus de 100 Ha)") +  xlim(8.5, 9.6) + ylim(41.3,43.1) +  scale_size_continuous(range=c(1,5), ) +  scale_color_viridis(option="inferno", trans="log", name="Surface", limits=c(99, 5700), breaks=c(100, 200, 500, 1000, 2500, 5000)) +  scale_alpha_continuous(trans="log") +  theme_void() +  theme(legend.title = element_text(size=10, face="bold"))t=800div(ggplotly(tooltip="text", width=t/1.7, height=t), align = "center")Joli, non ?