Web scraping

Readings and class materials for Tuesday, October 31, 2023

Nowadays, we manage a very huge part of our life online. This has an important side-effect: we can collect enormous data from the web for our researches. You can access data about shops, blogs, social media etc. The target of this chapter is to give a brief introduction how you can collect this data effectively. We will need a new package for this purpose: rvest

We will scrape the data from hasznaltauto, which is the online second hand car market of Hungary. Lets navigate to the page in our browser and lets click on search.

www.hasznaltauto.hu/

Now we have to copy and paste the new url from the browser to Rstudio. This will be the first link we want to visit while scraping. Lets assign this url as url in R.

url <- "https://www.hasznaltauto.hu/talalatilista/PCOG2VGRR3RDAD [...]" # long url

Click on the search button.

The next step is load the website into your R session. This can be done by the read_html function from the rvest package.

page <- read_html(url)

page
{html_document}
<html lang="hu-HU">
[1] <head>\n<meta http-equiv="Content-Type" content="text/html; charset=UTF-8 ...
[2] <body>\n    <script type="text/javascript">window.dataLayer = window.data ...

Data from tables

A random example of car ad.

You can see that the data is tabulated on the page. This is the best option for us, as you don’t have to search for the item ID on the page one by one (as before with the ad title). You can simply apply the html_table function to a loaded page, which collects all the tables on the it.

car_url <- "https://www.hasznaltauto.hu/szemelyauto/suzuki/vitara/suzuki_vitara_1.4_hybrid_gl_plusz_akar_2.1_millio_ar-elony_plusz_0_thm-18561661#sid=662a20d7-dd8c-4ede-965c-8736e82d6024"

info_tables <- read_html(car_url) %>% 
  html_table(fill = TRUE)

info_tables
[[1]]
# A tibble: 44 × 2
   `Ár, költségek`                                               `Ár, költségek`
   <chr>                                                         <chr>          
 1 "Vételár:"                                                    "Ár nélkül"    
 2 "Általános adatok"                                            "Általános ada…
 3 "Átvehető:"                                                   "2023."        
 4 "Évjárat:"                                                    "2023"         
 5 "Állapot:"                                                    "Normál"       
 6 "Kivitel:"                                                    "Városi terepj…
 7 "Nemzetközi járműelőélet"                                     "Nemzetközi já…
 8 "Informálódj az alvázszámról a nemzetközi autó-előélet szolg… "Informálódj a…
 9 "Hazai járműelőélet"                                          "Hazai járműel…
10 "Informálódj az alvázszámról a magyarországi autó-előélet sz… "Informálódj a…
# ℹ 34 more rows

What is the type of info_tables? Since it collects all the tables it can find from the page, it is a list of data frames.

I will reveal that in some cases we will see that there are multiple tables on a page (certain types of info are taken separately) and we want to avoid an irrelevant single column table causing an error (step 1).

Our goal is to be able to gather all the data about the car into a single two-column table. To join all two column tables (step 3), they must also have the same name (step 2).

info_tables %>% 
  keep(~ ncol(.) == 2) %>%  # keep tables that have 2 columns
  map(set_names, "x", "y") %>%  # set the names to x and y for each table
  bind_rows() # join the tables to one sinle table
# A tibble: 44 × 2
   x                                                                       y    
   <chr>                                                                   <chr>
 1 "Vételár:"                                                              "Ár …
 2 "Általános adatok"                                                      "Ált…
 3 "Átvehető:"                                                             "202…
 4 "Évjárat:"                                                              "202…
 5 "Állapot:"                                                              "Nor…
 6 "Kivitel:"                                                              "Vár…
 7 "Nemzetközi járműelőélet"                                               "Nem…
 8 "Informálódj az alvázszámról a nemzetközi autó-előélet szolgáltatás ha… "Inf…
 9 "Hazai járműelőélet"                                                    "Haz…
10 "Informálódj az alvázszámról a magyarországi autó-előélet szolgáltatás… "Inf…
# ℹ 34 more rows

We see that this works, the output now is one single table. Lets use this method on all the links. To do this, we need to write a function.

get_data <- function(url_to_car) {
  url_to_car %>% 
    read_html() %>% 
    html_table(fill = TRUE) %>% 
    keep(~ ncol(.) == 2) %>% 
    map(~ set_names(., "x", "y")) %>% 
    bind_rows()
}
get_data(car_url)
# A tibble: 44 × 2
   x                                                                       y    
   <chr>                                                                   <chr>
 1 "Vételár:"                                                              "Ár …
 2 "Általános adatok"                                                      "Ált…
 3 "Átvehető:"                                                             "202…
 4 "Évjárat:"                                                              "202…
 5 "Állapot:"                                                              "Nor…
 6 "Kivitel:"                                                              "Vár…
 7 "Nemzetközi járműelőélet"                                               "Nem…
 8 "Informálódj az alvázszámról a nemzetközi autó-előélet szolgáltatás ha… "Inf…
 9 "Hazai járműelőélet"                                                    "Haz…
10 "Informálódj az alvázszámról a magyarországi autó-előélet szolgáltatás… "Inf…
# ℹ 34 more rows

The resulting table can all be collected in a single column of our table. Each cell will be a table that we all collected from a given link.

This can also take a serious amount of time if we want to retrieve data from hundreds of cars at once. You should always use only a few. Use the sample_n function to randomly select a few lines to test if everything works as expected.

cars_add_df %>% 
  sample_n(size = 3) %>% # remove this line at the end if everything is fine
  mutate(
    data = map(url_to_cars, get_data)
  )
# A tibble: 3 × 3
  url_to_cars                                                  ad_title data    
  <chr>                                                        <chr>    <list>  
1 https://www.hasznaltauto.hu/szemelyauto/peugeot/5008/peugeo… PEUGEOT… <tibble>
2 https://www.hasznaltauto.hu/szemelyauto/kia/niro/kia_niro_1… KIA NIR… <tibble>
3 https://www.hasznaltauto.hu/szemelyauto/toyota/auris/toyota… TOYOTA … <tibble>

Currently, our small table is also nested in a cell. Expand it!

cars_add_df %>% 
  sample_n(size = 3) %>% # remove this line at the end if everything is fine
  mutate(
    data = map(url_to_cars, get_data)
  ) %>% 
  unnest()
# A tibble: 107 × 4
   url_to_cars                                              ad_title x     y    
   <chr>                                                    <chr>    <chr> <chr>
 1 https://www.hasznaltauto.hu/szemelyauto/peugeot/3008/pe… PEUGEOT… "Ala… "9 8…
 2 https://www.hasznaltauto.hu/szemelyauto/peugeot/3008/pe… PEUGEOT… "Ext… "11 …
 3 https://www.hasznaltauto.hu/szemelyauto/peugeot/3008/pe… PEUGEOT… "Akc… "9 8…
 4 https://www.hasznaltauto.hu/szemelyauto/peugeot/3008/pe… PEUGEOT… "Akc… "Érd…
 5 https://www.hasznaltauto.hu/szemelyauto/peugeot/3008/pe… PEUGEOT… "Vét… "€ 2…
 6 https://www.hasznaltauto.hu/szemelyauto/peugeot/3008/pe… PEUGEOT… "Fin… "Fin…
 7 https://www.hasznaltauto.hu/szemelyauto/peugeot/3008/pe… PEUGEOT… "Ált… "Ált…
 8 https://www.hasznaltauto.hu/szemelyauto/peugeot/3008/pe… PEUGEOT… "Átv… "202…
 9 https://www.hasznaltauto.hu/szemelyauto/peugeot/3008/pe… PEUGEOT… "Fut… "Új …
10 https://www.hasznaltauto.hu/szemelyauto/peugeot/3008/pe… PEUGEOT… "Évj… "202…
# ℹ 97 more rows

Now that we have a lot of rows instead of 3, the url in the first row is repeated as many times as the number of rows in the table next to it so far.

Since the x column now has the variable name and the y column has the value of the variable, we need another super useful function we’ve seen before: pivot_wider!

cars_add_df %>% 
  sample_n(size = 3) %>% # remove this line at the end if everything is fine
  mutate(
    data = map(url_to_cars, get_data)
  ) %>% 
  unnest() %>% 
  pivot_wider(names_from = "x", values_from = "y")
# A tibble: 3 × 55
  url_to_cars      ad_title `Alaptípus ára:` `Extrákkal növelt ár:` `Akciós ár:`
  <chr>            <chr>    <chr>            <chr>                  <chr>       
1 https://www.has… PEUGEOT… 9 840 000 Ft     11 700 000 Ft          9 840 000 Ft
2 https://www.has… MERCEDE… <NA>             <NA>                   <NA>        
3 https://www.has… MERCEDE… <NA>             <NA>                   <NA>        
# ℹ 50 more variables: `Akció feltételei:` <chr>, `Vételár EUR:` <chr>,
#   `Finanszírozás kalkulátor                    \n                \n            \n        \n    \n            \n            HIRDETÉS` <chr>,
#   `Általános adatok` <chr>, `Átvehető:` <chr>, `Futásidő:` <chr>,
#   `Évjárat:` <chr>, `Állapot:` <chr>, `Kivitel:` <chr>,
#   `Nemzetközi járműelőélet` <chr>,
#   `Informálódj az alvázszámról a nemzetközi autó-előélet szolgáltatás használatához!    \n    \n    \n        \n            Előélet lekérdezése` <chr>,
#   `Hazai járműelőélet` <chr>, …

The last thing that causes this headache for this task is the special characters used in the Hungarian language. The janitor, on the other hand, handles this smoothly as well.

We can now download all the cars if we wish!

cars_data_df <- cars_add_df %>% 
  mutate(
    data = map(url_to_cars, get_data)
  ) %>% 
  unnest() %>% 
  pivot_wider(names_from = "x", values_from = "y") %>% 
  janitor::clean_names()

Footnotes

  1. You can also use it on lists and vectors.↩︎