Web scraping with R

Lise Vaudor / Sébastien Rey-Coyrehourcq / Fabien Pfaender

Today !

Webscraping

Ethics, some elements of reflexion.

In two words

In more than two words …

Packages

A theoretical example of Web scraping

HTML Document

Understanding html documents’ structure

Tools SelectorGadget or html inspector

Package rvest: hexlogo_rvest

To collect the contents of a web page, one has to:

html tags

<html>
<style>
h1 {background-color: powderblue;}
.image {margin-left:50%;}
.comment{border-style:solid; background-color:LemonChiffon;}
.comment-author{font-style: italic;}
</style>
<h1> MA VIE A LA FERME </h1>
<div class="ingredients">
  <b> INGREDIENTS</b>
  <ul>
    <li> >1 cochon(s) </li>
    <li> >1 légume(s) </li>
  </ul>
</div>
<div class="right"><div class="image"><img src='images/cochon.png'></div></div>
<p> Je fais de la bouillie pour mes petits cochons.</p>
<p> Pour un cochon, pour deux cochons, pour trois cochons, pour quatre, puis pour cinq, pour six, pour sept, pour huit, pour neuf, boeuf! </p>
<b>comments</b>
<div class="comment">Et pour une poule sur un mur qui picoterait du pain dur?
</div>
<div class="comment-author">Emma, 22 ans, Limoges</div>
<div class="comment">Je vois que vous êtes, telle la petite poule rousse, bien aimable. Avez-vous pu compter sur l'aide du chat et du canard pour semer vos 5 grains de blé?</div>
<div class="comment-author">Michel, 56 ans, Rennes</div>
</html>

Read an html page

<html>
<style>
h1 {background-color: powderblue;}
.image {margin-left:50%;}
.comment{border-style:solid; background-color:LemonChiffon;}
.comment-author{font-style: italic;}
</style>
<h1> MA VIE A LA FERME </h1>
<div class="ingredients">
  <b> INGREDIENTS</b>
  <ul>
    <li> >1 cochon(s) </li>
    <li> >1 légume(s) </li>
  </ul>
</div>
<div class="right"><div class="image"><img src='images/cochon.png'></div></div>
<p> Je fais de la bouillie pour mes petits cochons.</p>
<p> Pour un cochon, pour deux cochons, pour trois cochons, pour quatre, puis pour cinq, pour six, pour sept, pour huit, pour neuf, boeuf! </p>
<b>comments</b>
<div class="comment">Et pour une poule sur un mur qui picoterait du pain dur?
</div>
<div class="comment-author">Emma, 22 ans, Limoges</div>
<div class="comment">Je vois que vous êtes, telle la petite poule rousse, bien aimable. Avez-vous pu compter sur l'aide du chat et du canard pour semer vos 5 grains de blé?</div>
<div class="comment-author">Michel, 56 ans, Rennes</div>
</html>

Read html page in R:

library(rvest)
html=read_html("data/blog_de_ginette.htm", encoding="UTF-8")
html
## {xml_document}
## <html>
## [1] <head>\n<meta http-equiv="Content-Type" content="text/html; charset= ...
## [2] <body>\n<h1> MA VIE A LA FERME </h1>\n<div class="ingredients">\n  < ...

Extract some elements in the html page

<html>
<style>
h1 {background-color: powderblue;}
.image {margin-left:50%;}
.comment{border-style:solid; background-color:LemonChiffon;}
.comment-author{font-style: italic;}
</style>
<h1> MA VIE A LA FERME </h1>
<div class="ingredients">
  <b> INGREDIENTS</b>
  <ul>
    <li> >1 cochon(s) </li>
    <li> >1 légume(s) </li>
  </ul>
</div>
<div class="right"><div class="image"><img src='images/cochon.png'></div></div>
<p> Je fais de la bouillie pour mes petits cochons.</p>
<p> Pour un cochon, pour deux cochons, pour trois cochons, pour quatre, puis pour cinq, pour six, pour sept, pour huit, pour neuf, boeuf! </p>
<b>comments</b>
<div class="comment">Et pour une poule sur un mur qui picoterait du pain dur?
</div>
<div class="comment-author">Emma, 22 ans, Limoges</div>
<div class="comment">Je vois que vous êtes, telle la petite poule rousse, bien aimable. Avez-vous pu compter sur l'aide du chat et du canard pour semer vos 5 grains de blé?</div>
<div class="comment-author">Michel, 56 ans, Rennes</div>
</html>

Extract some elements (“nodes” or “nodesets”):

html_nodes(html,"b")
## {xml_nodeset (2)}
## [1] <b> INGREDIENTS</b>
## [2] <b>Commentaires</b>
html_nodes(html,".comment-author") 
## {xml_nodeset (2)}
## [1] <div class="comment-author">Emma, 22 ans, Limoges</div>
## [2] <div class="comment-author">Michel, 56 ans, Rennes</div>
html_nodes(html,".ingredients") %>% 
  html_children()
## {xml_nodeset (2)}
## [1] <b> INGREDIENTS</b>
## [2] <ul>\n<li> &gt;1 cochon(s) </li>\n    <li> &gt;1 légume(s) </li>\n   ...

Extract the type of some elements

<html>
<style>
h1 {background-color: powderblue;}
.image {margin-left:50%;}
.comment{border-style:solid; background-color:LemonChiffon;}
.comment-author{font-style: italic;}
</style>
<h1> MA VIE A LA FERME </h1>
<div class="ingredients">
  <b> INGREDIENTS</b>
  <ul>
    <li> >1 cochon(s) </li>
    <li> >1 légume(s) </li>
  </ul>
</div>
<div class="right"><div class="image"><img src='images/cochon.png'></div></div>
<p> Je fais de la bouillie pour mes petits cochons.</p>
<p> Pour un cochon, pour deux cochons, pour trois cochons, pour quatre, puis pour cinq, pour six, pour sept, pour huit, pour neuf, boeuf! </p>
<b>comments</b>
<div class="comment">Et pour une poule sur un mur qui picoterait du pain dur?
</div>
<div class="comment-author">Emma, 22 ans, Limoges</div>
<div class="comment">Je vois que vous êtes, telle la petite poule rousse, bien aimable. Avez-vous pu compter sur l'aide du chat et du canard pour semer vos 5 grains de blé?</div>
<div class="comment-author">Michel, 56 ans, Rennes</div>
</html>

Extract the type of nodes or nodesets:

html_nodes(html,".image") %>% 
  html_name()
## [1] "div"

Extract the content of some elements

<html>
<style>
h1 {background-color: powderblue;}
.image {margin-left:50%;}
.comment{border-style:solid; background-color:LemonChiffon;}
.comment-author{font-style: italic;}
</style>
<h1> MA VIE A LA FERME </h1>
<div class="ingredients">
  <b> INGREDIENTS</b>
  <ul>
    <li> >1 cochon(s) </li>
    <li> >1 légume(s) </li>
  </ul>
</div>
<div class="right"><div class="image"><img src='images/cochon.png'></div></div>
<p> Je fais de la bouillie pour mes petits cochons.</p>
<p> Pour un cochon, pour deux cochons, pour trois cochons, pour quatre, puis pour cinq, pour six, pour sept, pour huit, pour neuf, boeuf! </p>
<b>comments</b>
<div class="comment">Et pour une poule sur un mur qui picoterait du pain dur?
</div>
<div class="comment-author">Emma, 22 ans, Limoges</div>
<div class="comment">Je vois que vous êtes, telle la petite poule rousse, bien aimable. Avez-vous pu compter sur l'aide du chat et du canard pour semer vos 5 grains de blé?</div>
<div class="comment-author">Michel, 56 ans, Rennes</div>
</html>

Extract the content of nodes or nodesets:

html_nodes(html,"b") %>% 
  html_text() 
## [1] " INGREDIENTS" "Commentaires"

Extract the attributes of some elements

<html>
<style>
h1 {background-color: powderblue;}
.image {margin-left:50%;}
.comment{border-style:solid; background-color:LemonChiffon;}
.comment-author{font-style: italic;}
</style>
<h1> MA VIE A LA FERME </h1>
<div class="ingredients">
  <b> INGREDIENTS</b>
  <ul>
    <li> >1 cochon(s) </li>
    <li> >1 légume(s) </li>
  </ul>
</div>
<div class="right"><div class="image"><img src='images/cochon.png'></div></div>
<p> Je fais de la bouillie pour mes petits cochons.</p>
<p> Pour un cochon, pour deux cochons, pour trois cochons, pour quatre, puis pour cinq, pour six, pour sept, pour huit, pour neuf, boeuf! </p>
<b>comments</b>
<div class="comment">Et pour une poule sur un mur qui picoterait du pain dur?
</div>
<div class="comment-author">Emma, 22 ans, Limoges</div>
<div class="comment">Je vois que vous êtes, telle la petite poule rousse, bien aimable. Avez-vous pu compter sur l'aide du chat et du canard pour semer vos 5 grains de blé?</div>
<div class="comment-author">Michel, 56 ans, Rennes</div>
</html>

Extract the attributes of nodes or nodesets:

html_nodes(html,"div") %>% 
  html_attrs()
## [[1]]
##         class 
## "ingredients" 
## 
## [[2]]
##   class 
## "right" 
## 
## [[3]]
##   class 
## "image" 
## 
## [[4]]
##     class 
## "comment" 
## 
## [[5]]
##            class 
## "comment-author" 
## 
## [[6]]
##     class 
## "comment" 
## 
## [[7]]
##            class 
## "comment-author"

Rectangular format, and function-ization

Extract data and make it into a table:

page="data/blog_de_ginette.htm"
html=read_html(page, encoding="UTF-8")
texte=html %>% html_nodes(".comment") %>% html_text()
auteur=html %>% html_nodes(".comment-author") %>% html_text()
tib_comments=tibble(texte,auteur)
tib_comments
## # A tibble: 2 x 2
##   texte                                                         auteur    
##   <chr>                                                         <chr>     
## 1 Et pour une poule sur un mur qui picoterait du pain dur? c'e… Emma, 22 …
## 2 Je vois que vous êtes, telle la petite poule rousse, bien ai… Michel, 5…

It is actually a good idea to make all this into a fonction that would have the page’s url as intput and the tibble as output:

extract_comments=function(page){
  html=read_html(page, encoding="UTF-8")
  texte=html %>% html_nodes(".comment") %>% html_text()
  auteur=html %>% html_nodes(".comment-author") %>% html_text()
  tib_comments=tibble(doc=rep(page,length(texte)),
                          texte,
                          auteur)
  return(tib_comments)
}

extract_comments("data/blog_de_ginette.htm")
## # A tibble: 2 x 3
##   doc                      texte                                  auteur  
##   <chr>                    <chr>                                  <chr>   
## 1 data/blog_de_ginette.htm Et pour une poule sur un mur qui pico… Emma, 2…
## 2 data/blog_de_ginette.htm Je vois que vous êtes, telle la petit… Michel,…
extract_comments("data/blog_de_jean-marc.htm")
## # A tibble: 6 x 3
##   doc                        texte                            auteur      
##   <chr>                      <chr>                            <chr>       
## 1 data/blog_de_jean-marc.htm Les thons, avec un t comme croc… "Eddie, 76 …
## 2 data/blog_de_jean-marc.htm Pourquoi ces thons ne préféraie… Yves, 40 an…
## 3 data/blog_de_jean-marc.htm Tout ça me fait penser au blog … Roberta, 18…
## 4 data/blog_de_jean-marc.htm Je préfère la chanson qui parle… Eduardo, 29…
## 5 data/blog_de_jean-marc.htm On ne comprend pas trop cette p… Lise, 35 an…
## 6 data/blog_de_jean-marc.htm Et pendant ce temps-là, le roi … Nadia, 43 a…

Iteration

Now, let’s imagine that we actually have to deal with several pages with a common structure.

We would like to apply extract_comments() iteratively to all these pages.

The purrr package enables us to apply a function iteratively to all elements of a list or of a vector (… of course this is just a more straightforward way to loop through a for structure…).

Iteration with purrr

pages=c("data/blog_de_ginette.htm",
        "data/blog_de_jean-marc.htm",
        "data/blog_de_norbert.htm")

list_comments=map(pages, extract_comments)
list_comments
## [[1]]
## # A tibble: 2 x 3
##   doc                      texte                                  auteur  
##   <chr>                    <chr>                                  <chr>   
## 1 data/blog_de_ginette.htm Et pour une poule sur un mur qui pico… Emma, 2…
## 2 data/blog_de_ginette.htm Je vois que vous êtes, telle la petit… Michel,…
## 
## [[2]]
## # A tibble: 6 x 3
##   doc                        texte                            auteur      
##   <chr>                      <chr>                            <chr>       
## 1 data/blog_de_jean-marc.htm Les thons, avec un t comme croc… "Eddie, 76 …
## 2 data/blog_de_jean-marc.htm Pourquoi ces thons ne préféraie… Yves, 40 an…
## 3 data/blog_de_jean-marc.htm Tout ça me fait penser au blog … Roberta, 18…
## 4 data/blog_de_jean-marc.htm Je préfère la chanson qui parle… Eduardo, 29…
## 5 data/blog_de_jean-marc.htm On ne comprend pas trop cette p… Lise, 35 an…
## 6 data/blog_de_jean-marc.htm Et pendant ce temps-là, le roi … Nadia, 43 a…
## 
## [[3]]
## # A tibble: 4 x 3
##   doc                      texte                          auteur          
##   <chr>                    <chr>                          <chr>           
## 1 data/blog_de_norbert.htm "A quel moment faut-il claque… Jonas, 37 ans, …
## 2 data/blog_de_norbert.htm Norbert, mon petit chat, ça f… Julie, 34 ans, …
## 3 data/blog_de_norbert.htm L'ambiance doit être sympa qu… Mickaël, 23 ans…
## 4 data/blog_de_norbert.htm Vous devez avoir un grain pou… Viviane, 58 ans…
tibtot_comments <- list_comments %>%
  bind_rows()
tibtot_comments
## # A tibble: 12 x 3
##    doc                        texte                             auteur    
##    <chr>                      <chr>                             <chr>     
##  1 data/blog_de_ginette.htm   Et pour une poule sur un mur qui… Emma, 22 …
##  2 data/blog_de_ginette.htm   Je vois que vous êtes, telle la … Michel, 5…
##  3 data/blog_de_jean-marc.htm Les thons, avec un t comme croco… "Eddie, 7…
##  4 data/blog_de_jean-marc.htm Pourquoi ces thons ne préféraien… Yves, 40 …
##  5 data/blog_de_jean-marc.htm Tout ça me fait penser au blog d… Roberta, …
##  6 data/blog_de_jean-marc.htm Je préfère la chanson qui parle … Eduardo, …
##  7 data/blog_de_jean-marc.htm On ne comprend pas trop cette pa… Lise, 35 …
##  8 data/blog_de_jean-marc.htm Et pendant ce temps-là, le roi d… Nadia, 43…
##  9 data/blog_de_norbert.htm   "A quel moment faut-il claquer d… Jonas, 37…
## 10 data/blog_de_norbert.htm   Norbert, mon petit chat, ça fait… Julie, 34…
## 11 data/blog_de_norbert.htm   L'ambiance doit être sympa quand… Mickaël, …
## 12 data/blog_de_norbert.htm   Vous devez avoir un grain pour é… Viviane, …

Manipulate strings: package stringr

Package stringr Blog post (in French) here.

Strings: concatenate, replace pattern

str_c() to combine strings

str_c("abra","ca","dabra")
## [1] "abracadabra"
str_c("Les jeux","de mots laids","sont pour","les gens bêtes", sep=" ")
## [1] "Les jeux de mots laids sont pour les gens bêtes"

str_detect() detects a pattern

str_detect(c("Quarante","carottes","crues",
             "croient","que","croquer",
             "crée","des","crampes."),
           pattern="cr")
## [1] FALSE FALSE  TRUE  TRUE FALSE  TRUE  TRUE FALSE  TRUE

Strings: replace pattern, extract pattern

str_replace() replaces a pattern with another

str_replace(c("All we hear is",
              "Radio ga ga",
              "Radio goo goo",
              "Radio ga ga"),
            pattern="goo",
            replacement="ga")
## [1] "All we hear is" "Radio ga ga"    "Radio ga goo"   "Radio ga ga"

str_extract() extracts the pattern (if it’s there!)

str_extract(c("L'âne","Trotro","trotte","à une allure","traitreusement","tranquille"),
           pattern="tr")
## [1] NA   "tr" "tr" NA   "tr" "tr"

Regular expressions

Regular expressions are used to define patterns through rules of construction.A tutorial here

.

Regexp: character classes and groups

A character class corresponds to the notation [...].

For instance, to detect all voyels in the string:

str_view_all("youp la boum",
             "[aeiou]")

See the difference:

str_view_all("A132-f445-e34-C308-M9-E18",
             "[308]")
str_view_all("A132-f445-e34-C308-M9-E18",
             "308")

Any character can be noted ..

For instance, the pattern “any character followed by a letter” can be searched through

str_view_all("32a-B44-552-98eEf",
             ".[a-z]")

Regexp: special characters

To find dots, question marks, exclamation marks:

str_view_all(c("Allô, John-John? Ici Joe la frite. Surprise!"),
             "[\\.\\?\\!]")

Note that we don’t write "[.?!]", but "[\\.\\?\\!]".

. (as we saw before), as well as ? and ! are special characters. So, to point out actual dots or interrogation/exclamation marks, one has to use the escape character \. The regular expression hence becomes [\.\?\!]

But it does not end here… as it is not directly the regular expression that is passed to the function, but a string that is interpreted as a regular expression. Thus each escape character \ has to be escaped through a \. So that the pattern passed to the function is actually "[\\.\\?\\!]".

Regexp: excluded characters, character ranges

A character class can be defined as all characters excluding the ones listed. This is noted as [^...]:

For instance, all characters that are neither a vowel nor a blank space:

str_view_all("turlututu chapeau pointu",
             "[^aeiou ]")

Ranges of characters are noted [...-...]

For instance, all numbers between 1 and 5:

str_view_all(c("3 petits cochons", "101 dalmations", "7 nains"),
             "[1-5]")

… or all those between A-F or a-e:

str_view_all("A132-f445-e34-C308-M2244-Z449-E18",
             "[A-Fa-e]")

Regexp: predefined classes

Some character classes are predefined for instance digits, punctuation characters, lower-case alphabetical characters, etc.

Regexp: quantifiers

Quantifiers are used to specify how many consecutive times a particular class or group occurs.

zero or one: the pattern of interest is followed by ?.

str_view_all(c("file1990-fileB1990-fileAbis2005"),
             "file\\d?")

zero or more: O the pattern of interest is followed by *.

str_view_all(c("file1990-fileB1990-fileAbis2005"),
             "file\\d*")

one or more : the pattern of interest is followed by +.

str_view_all(c("file1990-fileB1990-fileAbis2005"),
             "file\\d+")

Xpath

XPATH is another way to query the DOM, it’s very powerfull, but also a little complex. Use some cheatsheet to remember principal operators.

Some examples :

html_nodes(html, xpath = "//div[@class='ingredients']//ul//li")
## {xml_nodeset (2)}
## [1] <li> &gt;1 cochon(s) </li>
## [2] <li> &gt;1 légume(s) </li>
html_nodes(html, xpath = "//text()[contains(.,'cochons') and not(contains(.,'poule'))]")
## {xml_nodeset (2)}
## [1]  Je fais de la bouillie pour mes petits cochons.
## [2]  Pour un cochon, pour deux cochons, pour trois cochons, pour quatre, ...
html_nodes(html, xpath = "//div[@class='comment']")
## {xml_nodeset (2)}
## [1] <div class="comment">Et pour une poule sur un mur qui picoterait du  ...
## [2] <div class="comment">Je vois que vous êtes, telle la petite poule ro ...

A and game (1)

How to defend from webscraping :

Legend : { : cost, difficulty to implement } , { : dificulty to bypass } , { or : user happiness }

A and game (2)

How to attack :

One rule : If you see it on your browser, so you can get it

Basics :

Advanced :

Uses cases

!Warning! You need Docker installed