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
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
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
<- "https://www.borsaitaliana.it"
url_base
# Path as vector
<- c("borsa", "azioni", "listino-a-z.html")
url_path
# Query as named list
<- list(initial = "A", lang = "it")
url_query
# Url Finale
<- httr::modify_url(url_base, path = url_path, query = url_query)
url_completo
# Lettura dell' HTML
<- rvest::read_html(url_completo)
html
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)
<- function(html){
singola_pagina
# Estrazione Url
<- rvest::html_nodes(html, xpath = "//table/tr/td[1]/a")
stock_url
# Estrazione Nomi dagli Url
<- rvest::html_text(stock_url)
stock_name <- stringr::str_trim(stock_name)
stock_name
# Composizione Url finale
<- rvest::html_attr(stock_url, "href")
stock_url <- paste0(url_base, stock_url)
stock_url
# Estrazione ISIN dagli Url
<- unlist(stringr::str_extract_all(stock_url, "scheda/.*"))
stock_isin <- stringr::str_remove_all(stock_isin, "scheda/|\\.html\\?lang=it")
stock_isin
# Estrazione Mercato
<- rvest::html_nodes(html, xpath = "//table/tr/td[1]/a/span/span")
stock_market <- rvest::html_text(stock_market)
stock_market
# Rimozione del nome del Mercato dal nome delle azioni.
<- purrr::map_chr(stock_name, ~stringr::str_remove_all(.x, paste(stock_market, collapse = "|")))
stock_name <- stringr::str_trim(stock_name)
stock_name
# Dataset finale
<- dplyr::tibble(Isin = stock_isin,
out_data Market = stock_market,
Name = stock_name,
Url = stock_url)
out_data
}
singola_pagina(html)
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.
<- function(html){
find_next_page
# Identificazione di tutti i nodi
<- rvest::html_nodes(html, css = "li a")
next_page_button
# Filtro individuando l'indice con nodo con attributo title="Successiva"
<- which(rvest::html_attr(next_page_button, "title") == "Successiva")
index_button
# Restituisce l'xpath del nodo selezionato o NULL
if(purrr::is_empty(index_button)){
NULL
else {
} <- next_page_button[index_button]
next_page_button ::xml_path(next_page_button)
xml2
}
}
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.
<- function(initial = "A"){
singola_lettera
# Url di Base
<- "https://www.borsaitaliana.it"
url_base
# Path as vector
<- c("borsa", "azioni", "listino-a-z.html")
url_path
# Query as named list
<- list(initial = initial, lang = "it")
url_query
# Url Finale
<- httr::modify_url(url_base, path = url_path, query = url_query)
url_completo
# Apertura della sessione
<- rvest::session(url_completo)
sess
# Lettura iniziale dell' HTML
<- rvest::read_html(sess)
html
# Inizializziamo una lista dove salvare le pagine
<- list()
all_pages
# Il primo elemento può essere importato subito
1]] <- singola_pagina(html)
all_pages[[
# Cerchiamo il pulsante "successivo" sarà la condizione per il ciclo while
<- find_next_page(html)
next_page <- !is.null(next_page)
condition <- 2
page
# Se la condizione è vera il ciclo inizia
while(condition){
# Cambio sessione
<- rvest::session_follow_link(sess, xpath = next_page)
sess
# Lettura del nuovo HTML
<- rvest::read_html(sess)
html
# Importazione della pagina
<- singola_pagina(html)
all_pages[[page]]
# Cerchiamo il pulsante "successivo" sarà la condizione per il ciclo while
<- find_next_page(html)
next_page <- !is.null(next_page)
condition <- page + 1
page
}
<- dplyr::bind_rows(all_pages)
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.
<- function(){
get_listino_azionario
# definiamo una funzione "safe" per evitare che il ciclo si possa bloccare:
<- purrr::safely(singola_lettera)
safe_singola_lettera
<- purrr::map(LETTERS, ~safe_singola_lettera(initial = .x)$result)
all_letters
::bind_rows(all_letters)
dplyr
}
<- get_listino_azionario()
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
.
<- function(){
get_listino_azionario_old
# Inizializzazione lista per le lettere
<- 1
i <- list()
all_stocks
# Url di Base
<- "https://www.borsaitaliana.it"
url_base
# Path as vector
<- c("borsa", "azioni", "listino-a-z.html")
url_path
# For loop for all the Letters
for(i in 1:length(LETTERS)){
# Query Dinamica
<- list(initial = LETTERS[i], lang = "it")
url_query
# Url Finale
<- httr::modify_url(url_base, path = url_path, query = url_query)
url_completo
# Inizio la Sessione
<- rvest::session(url_completo)
sess
# Lettura dell' HTML
<- rvest::read_html(sess)
html
# Inizializziamo una lista dove salvare le pagine
<- list()
all_pages
# Il primo elemento può essere importato subito
1]] <- singola_pagina(html)
all_pages[[
# Cerchiamo il pulsante "successivo" sarà la condizione per il ciclo while
<- find_next_page(html)
next_page <- !is.null(next_page)
condition <- 2
page
# Se la condizione è vera il ciclo inizia
while(condition){
# Cambio sessione
<- rvest::session_follow_link(sess, xpath = next_page)
sess
# Lettura del nuovo HTML
<- rvest::read_html(sess)
html
# Importazione della pagina
<- singola_pagina(html)
all_pages[[page]]
# Cerchiamo il pulsante "successivo" sarà la condizione per il ciclo while
<- find_next_page(html)
next_page <- !is.null(next_page)
condition <- page + 1
page
}
<- dplyr::bind_rows(all_pages)
all_stocks[[i]]
}
::bind_rows(all_stocks)
dplyr
}
Nota 2: Gestire più pagine
<- function(html){
find_prev_page
# Identificazione di tutti i nodi
<- rvest::html_nodes(html, css = "li a")
prev_page_button
# Filtro individuando l'indice con nodo con attributo title="Precedente"
<- which(rvest::html_attr(prev_page_button, "title") == "Precedente")
index_button
# Restituisce l'xpath del nodo selezionato o NULL
if(purrr::is_empty(index_button)){
NULL
else {
} <- prev_page_button[index_button]
prev_page_button ::xml_path(prev_page_button)
xml2
} }
<- function(html){
find_max_page
# Identificazione di tutti i nodi
<- rvest::html_nodes(html, css = "li.m-pagination__item a")
prev_page_button
# Filtro individuando tutti i numeri delle pagine
<- as.integer(rvest::html_text(prev_page_button))
index_button
# Restituisce il numero massimo di pagine o NULL
if(purrr::is_empty(index_button)){
NULL
else {
} max(index_button, na.rm = TRUE)
} }