Scraping con rvest in R: Borsa Italiana

Beniamino Sartini

2023-02-01

Scraping del Listino Azionario Italiano

Lo scopo di questo breve articolo è di formire un’esempio di costruzione di uno scraper per un sito web. In questo caso particolare è stato scelto il sito della Borsa Italiana da cui vogliamo estrarre tutte le azioni quotate nel marcato italiano.

Step 1: Identificare la pagina di partenza

Per prima cosa occorre identificare la pagina da cui iniziare a scaricare i dati, in questo caso è stata individuata una pagina che contiene il listino azionario diviso per lettera.

  • La pagina dedicata al listino azionario https://www.borsaitaliana.it/borsa/azioni/listino-a-z.html?initial=A.

Come è possibile notare dal sito web (e dall’url) il sito presenta delle variabili di query:

  • initial: la lettera dell’alfabeto di cui vogliamo importare le azioni.
  • page=: per il numero della pagina, nel caso in cui ci si trovi sulla prima pagina può essere omesso.

Di conseguenza per ciascuna lettera è necessario importare piu di una pagina. Come ulteriore difficoltà non è noto a priori quale sia il numero massimo di pagine per ogni lettera.

Inizio della pagina con Listino A-Z per Iniziale A

Inizio della pagina con Listino A-Z per Iniziale A

Come possiamo vedere a ogni lettera possono corrispondere più pagine, fortunatamente l’url della pagina contiene un pattern utile al cambio della pagina. Infatti andando a pagina 2, l’url si modifica nel seguente modo:

  • Pagina 2 Lettera A: https://www.borsaitaliana.it/borsa/azioni/listino-a-z.html?initial=A&page=2.
Fine della pagina con Listino A-Z per Iniziale A

Fine della pagina con Listino A-Z per Iniziale A

Step 2: Estrarre le informazioni da una singola pagina

Per prima cosa è necessario creare una funzione per importare i dati per una lettera e una pagina ben specifiche. Per farlo è necessario comporre l’url desiderato, successivamente è possibile effettuare la lettura dell’html usando la funzione read_html dal pacchetto rvest.

library(rvest)
library(httr)

# Url di Base
url_base <- "https://www.borsaitaliana.it"

# Path as vector
url_path <- c("borsa", "azioni", "listino-a-z.html")

# Query as named list
url_query <- list(initial = "A", lang = "it")

# Url Finale 
url_completo <- httr::modify_url(url_base, path = url_path, query = url_query)
    
# Lettura dell' HTML
html <- rvest::read_html(url_completo)

html
## {html_document}
## <html class="no-js" lang="it" xmlns="http://www.w3.org/1999/html">
## [1] <head>\n<meta http-equiv="Content-Type" content="text/html; charset=UTF-8 ...
## [2] <body> \n<noscript><iframe src="https://www.googletagmanager.com/ns.html? ...

A meno di errori dovremmo ottenere un oggetto contenente l’html della pagina. Utilizzando il web inspector è possibile individuare la posizione degli elementi che vogliamo importare. In questo caso vogliamo salvare:

  • L’url dell’azione ha xpath = "//table/tr/td[1]/a", sotto lo stesso xpath è possibile accedere al nome dell’azione.
  • Il nome del mercato ha xpath = "//table/tr/td[1]/a/span/span".
  • L’ISIN può essere estratto dall’url.

A questo punto abbiamo tutte le informazioni per definire una funzione che prenda in input un’oggetto html e resituisca in output una tabella con 4 colonne: Isin, Market, Name e Url.

library(stringr)
library(purrr)

singola_pagina <- function(html){
  
  # Estrazione Url
  stock_url <- rvest::html_nodes(html, xpath = "//table/tr/td[1]/a")
  
  # Estrazione Nomi dagli Url 
  stock_name <- rvest::html_text(stock_url)
  stock_name <- stringr::str_trim(stock_name)
  
  # Composizione Url finale 
  stock_url <- rvest::html_attr(stock_url, "href")
  stock_url <- paste0(url_base, stock_url)
  
  # Estrazione ISIN dagli Url 
  stock_isin <- unlist(stringr::str_extract_all(stock_url, "scheda/.*"))
  stock_isin <- stringr::str_remove_all(stock_isin, "scheda/|\\.html\\?lang=it")
    
  # Estrazione Mercato 
  stock_market <- rvest::html_nodes(html, xpath = "//table/tr/td[1]/a/span/span")
  stock_market <- rvest::html_text(stock_market)
  
  # Rimozione del nome del Mercato dal nome delle azioni.
  stock_name <- purrr::map_chr(stock_name, ~stringr::str_remove_all(.x, paste(stock_market, collapse = "|")))
  stock_name <- stringr::str_trim(stock_name)
  
  # Dataset finale 
  out_data <- dplyr::tibble(Isin = stock_isin,
                            Market = stock_market,
                            Name = stock_name, 
                            Url = stock_url)
  out_data
  
}

singola_pagina(html)
Isin Market Name Url
IT0005439861 Euronext Growth Milan A.B.P. Nocivelli https://www.borsaitaliana.it/borsa/azioni/euronext-growth-milan/scheda/IT0005439861.html?lang=it
IT0001233417 Euronext Milan A2a https://www.borsaitaliana.it/borsa/azioni/scheda/IT0001233417.html?lang=it
IT0005466294 Euronext Growth Milan - Segmento Professionale Abc Company - Segmento Professionale https://www.borsaitaliana.it/borsa/azioni/euronext-growth-milan/scheda/IT0005466294.html?lang=it
IT0005445280 Euronext STAR Milan Abitare In https://www.borsaitaliana.it/borsa/azioni/scheda/IT0005445280.html?lang=it
IT0001207098 Euronext Milan Acea https://www.borsaitaliana.it/borsa/azioni/scheda/IT0001207098.html?lang=it
IT0001382024 Euronext Milan Acinque https://www.borsaitaliana.it/borsa/azioni/scheda/IT0001382024.html?lang=it
IT0005443061 Euronext Growth Milan - Segmento Professionale Acquazzurra - Segmento Professionale https://www.borsaitaliana.it/borsa/azioni/euronext-growth-milan/scheda/IT0005443061.html?lang=it
DE000A1EWWW0 Global Equity Market Adidas https://www.borsaitaliana.it/borsa/azioni/global-equity-market/scheda/DE000A1EWWW0.html?lang=it
US0079031078 Global Equity Market Advanced Micro Devices https://www.borsaitaliana.it/borsa/azioni/global-equity-market/scheda/US0079031078.html?lang=it
IT0005350449 Euronext Milan Aedes https://www.borsaitaliana.it/borsa/azioni/scheda/IT0005350449.html?lang=it
IT0001384590 Euronext STAR Milan Aeffe https://www.borsaitaliana.it/borsa/azioni/scheda/IT0001384590.html?lang=it
NL0000303709 Global Equity Market Aegon https://www.borsaitaliana.it/borsa/azioni/global-equity-market/scheda/NL0000303709.html?lang=it
IT0001006128 Euronext STAR Milan Aeroporto Guglielmo Marconi Di Bologna https://www.borsaitaliana.it/borsa/azioni/scheda/IT0001006128.html?lang=it
IT0005421919 Euronext Growth Milan Agatos https://www.borsaitaliana.it/borsa/azioni/euronext-growth-milan/scheda/IT0005421919.html?lang=it
IT0005256059 Euronext Growth Milan Agatos 4,75% Cv 2017-2026 https://www.borsaitaliana.it/borsa/azioni/obbligazioni-convertibili/scheda/IT0005256059.html?lang=it
BE0974264930 Global Equity Market Ageas https://www.borsaitaliana.it/borsa/azioni/global-equity-market/scheda/BE0974264930.html?lang=it
NL0011794037 Global Equity Market Ahold Del https://www.borsaitaliana.it/borsa/azioni/global-equity-market/scheda/NL0011794037.html?lang=it
FR0000031122 Global Equity Market Air France-Klm https://www.borsaitaliana.it/borsa/azioni/global-equity-market/scheda/FR0000031122.html?lang=it
NL0000235190 Global Equity Market Airbus https://www.borsaitaliana.it/borsa/azioni/global-equity-market/scheda/NL0000235190.html?lang=it
IT0005446700 Euronext Growth Milan Ala https://www.borsaitaliana.it/borsa/azioni/euronext-growth-milan/scheda/IT0005446700.html?lang=it

Step 3: Gestire più pagine

Per gestire l’importazione di più pagine web esistono diversi approcci. In questo caso, avendo notato che cliccando sulla seconda pagina l’url viene modificato e viene introdotto un nuovo parametro di query page=2, possiamo sfruttare questo aspetto per importare più pagine. Tuttavia l’ultimo problema da risolvere resta il sapere quale sia l’ultima pagina per evitare di incorrere in un loop infinito. Per risolvere questo problema è possibile definire una funzione ausiliaria find_next_page: prende in input una pagina html e restituisce come output l’xpath del pulsante con la freccia per andare alla pagina successiva. Il caso in cui la funzione non riesce a trovare nulla si puo interpretare come condizione di stop indicando che la pagina dove ci troviamo è l’ultima.

find_next_page <- function(html){
  
  # Identificazione di tutti i nodi
  next_page_button <- rvest::html_nodes(html, css = "li a")
  
  # Filtro individuando l'indice con nodo con attributo title="Successiva"
  index_button <- which(rvest::html_attr(next_page_button, "title") == "Successiva")
  
  # Restituisce l'xpath del nodo selezionato o NULL
  if(purrr::is_empty(index_button)){
    NULL
  } else {
    next_page_button <- next_page_button[index_button]
    xml2::xml_path(next_page_button)
  }
}

find_next_page(html)
## [1] "/html/body/div[2]/div[6]/main/section/div[4]/article[1]/div[2]/div[2]/div/ul/li[4]/a"

Step 4: Importare tutte le pagine per una lettera

A questo punto possiamo scrivere una funzione che, data una lettera, importi tutti le azioni contenute in quella sezione. Difatti dovendo importare piu pagine è piu pratico utilizzare la funzione session e session_follow_link dal pacchetto rvest, in tal modo si evita di comporre un nuovo url per ogni pagina.

singola_lettera <- function(initial = "A"){
  
  # Url di Base
  url_base <- "https://www.borsaitaliana.it"
  
  # Path as vector
  url_path <- c("borsa", "azioni", "listino-a-z.html")
  
  # Query as named list
  url_query <- list(initial = initial, lang = "it")
  
  # Url Finale 
  url_completo <- httr::modify_url(url_base, path = url_path, query = url_query)
      
  # Apertura della sessione 
  sess <- rvest::session(url_completo)
  
  # Lettura iniziale dell' HTML
  html <- rvest::read_html(sess)
  
  # Inizializziamo una lista dove salvare le pagine 
  all_pages <- list()
  
  # Il primo elemento può essere importato subito 
  all_pages[[1]] <- singola_pagina(html)
  
  # Cerchiamo il pulsante "successivo" sarà la condizione per il  ciclo while 
  next_page <- find_next_page(html)
  condition <- !is.null(next_page)
  page <- 2
  
  # Se la condizione è vera il ciclo inizia 
  while(condition){
    
    # Cambio sessione 
    sess <- rvest::session_follow_link(sess, xpath = next_page)
    
    # Lettura del nuovo HTML
    html <- rvest::read_html(sess)
    
    # Importazione della pagina       
    all_pages[[page]] <- singola_pagina(html)
      
    # Cerchiamo il pulsante "successivo" sarà la condizione per il  ciclo while     
    next_page <- find_next_page(html)
    condition <- !is.null(next_page)
    page <- page + 1
  } 
  
  all_pages <- dplyr::bind_rows(all_pages)

  all_pages
  
}

singola_lettera(initial = "A") %>%
  head(n = 5)
Isin Market Name Url
IT0005439861 Euronext Growth Milan A.B.P. Nocivelli https://www.borsaitaliana.it/borsa/azioni/euronext-growth-milan/scheda/IT0005439861.html?lang=it
IT0001233417 Euronext Milan A2a https://www.borsaitaliana.it/borsa/azioni/scheda/IT0001233417.html?lang=it
IT0005466294 Euronext Growth Milan - Segmento Professionale Abc Company - Segmento Professionale https://www.borsaitaliana.it/borsa/azioni/euronext-growth-milan/scheda/IT0005466294.html?lang=it
IT0005445280 Euronext STAR Milan Abitare In https://www.borsaitaliana.it/borsa/azioni/scheda/IT0005445280.html?lang=it
IT0001207098 Euronext Milan Acea https://www.borsaitaliana.it/borsa/azioni/scheda/IT0001207098.html?lang=it

Step 5: Importare tutto il listino

Infine, avendo definito tutti i passaggi necessari, resta solo un parametro da considerare: l’iniziale del nome. In R è possibile accedere ad un oggetto built-in chiamato LETTERS che contiene tutte le lettere dell’alfabeto in maiuscolo.

get_listino_azionario <- function(){
  
  # definiamo una funzione "safe" per evitare che il ciclo si possa bloccare:  
  safe_singola_lettera <- purrr::safely(singola_lettera)
  
  all_letters <- purrr::map(LETTERS, ~safe_singola_lettera(initial = .x)$result)
  
  dplyr::bind_rows(all_letters)
}

listino_azionario <- get_listino_azionario() 

head(listino_azionario, n = 5)
Isin Market Name Url
IT0005439861 Euronext Growth Milan A.B.P. Nocivelli https://www.borsaitaliana.it/borsa/azioni/euronext-growth-milan/scheda/IT0005439861.html?lang=it
IT0001233417 Euronext Milan A2a https://www.borsaitaliana.it/borsa/azioni/scheda/IT0001233417.html?lang=it
IT0005466294 Euronext Growth Milan - Segmento Professionale Abc Company - Segmento Professionale https://www.borsaitaliana.it/borsa/azioni/euronext-growth-milan/scheda/IT0005466294.html?lang=it
IT0005445280 Euronext STAR Milan Abitare In https://www.borsaitaliana.it/borsa/azioni/scheda/IT0005445280.html?lang=it
IT0001207098 Euronext Milan Acea https://www.borsaitaliana.it/borsa/azioni/scheda/IT0001207098.html?lang=it

Scraping del Listino del MOT

Appendice

Nota 1: programmazione funzionale

Nel comporre lo scraper è molto importante decidere come impostare il lavoro. Nel caso di scraping da un sito web sprovvisto di API è raccomandabile definire le funzioni utilizzando il piu possibile la logica della programmazione funzionale, scomponendo, dove possibile, le funzioni in sotto-funzioni cosi da rendere le eventuali (e necessarie) modifiche nel tempo facili e intuitive. Inoltre scomponendo le funzioni si riesce ad eseguire test singoli e ad individuare i problemi molto piu rapidamente.

Di seguito abbiamo riportato un’altra versione della funzione listino_azionario implementata con cicli for e while, ma utilizzando la funzione singola_pagina e find_next_page.

get_listino_azionario_old <- function(){

  # Inizializzazione lista per le lettere 
  i <- 1
  all_stocks <- list()
  
  # Url di Base
  url_base <- "https://www.borsaitaliana.it"
  
  # Path as vector
  url_path <- c("borsa", "azioni", "listino-a-z.html")
  
  # For loop for all the Letters 
  for(i in 1:length(LETTERS)){
    
    # Query Dinamica 
    url_query <- list(initial = LETTERS[i], lang = "it")
 
    # Url Finale 
    url_completo <- httr::modify_url(url_base, path = url_path, query = url_query)
    
    # Inizio la Sessione
    sess <- rvest::session(url_completo)
    
    # Lettura dell' HTML
    html <- rvest::read_html(sess)
    
    # Inizializziamo una lista dove salvare le pagine 
    all_pages <- list()
  
    # Il primo elemento può essere importato subito 
    all_pages[[1]] <- singola_pagina(html)
    
    # Cerchiamo il pulsante "successivo" sarà la condizione per il  ciclo while 
    next_page <- find_next_page(html)
    condition <- !is.null(next_page)
    page <- 2
    
    # Se la condizione è vera il ciclo inizia 
    while(condition){
      
      # Cambio sessione 
      sess <- rvest::session_follow_link(sess, xpath = next_page)
      
      # Lettura del nuovo HTML
      html <- rvest::read_html(sess)
      
      # Importazione della pagina       
      all_pages[[page]] <- singola_pagina(html)
        
      # Cerchiamo il pulsante "successivo" sarà la condizione per il  ciclo while     
      next_page <- find_next_page(html)
      condition <- !is.null(next_page)
      page <- page + 1
    } 

    all_stocks[[i]] <- dplyr::bind_rows(all_pages)
    
  }
  
  dplyr::bind_rows(all_stocks)
  
}

Nota 2: Gestire più pagine

find_prev_page <- function(html){
  
  # Identificazione di tutti i nodi
  prev_page_button <- rvest::html_nodes(html, css = "li a")
  
  # Filtro individuando l'indice con nodo con attributo title="Precedente"
  index_button <- which(rvest::html_attr(prev_page_button, "title") == "Precedente")
  
  # Restituisce l'xpath del nodo selezionato o NULL
  if(purrr::is_empty(index_button)){
    NULL
  } else {
    prev_page_button <- prev_page_button[index_button]
    xml2::xml_path(prev_page_button)
  }
}
find_max_page <- function(html){
  
  # Identificazione di tutti i nodi
  prev_page_button <- rvest::html_nodes(html, css = "li.m-pagination__item a")
  
  # Filtro individuando tutti i numeri delle pagine
  index_button <- as.integer(rvest::html_text(prev_page_button))
  
  # Restituisce il numero massimo di pagine o NULL
  if(purrr::is_empty(index_button)){
    NULL
  } else {
    max(index_button, na.rm = TRUE)
  }
}