web scraping I
Hace tiempo que quería escribir en el blog algún ejemplo de web scraping o rascado güeb. Para esto se me ha ocurrido un ejemplo sencillo: nuestro objetivo será crear una página web con R que tomará “prestados” los datos de una página de cotizaciones. Para desarrollar este ejemplo usaremos web scraping, Shiny con flex y algunas cosas curiosas como la función invalidateLater
para que la app se actualice automáticamente cada cierto tiempo.
Empecemos:
¿Qué es el web scraping?
Se denomina web scraping (rascado web) a la automatización de la descarga de datos de Internet mediante programación. Es una forma de obtener datos de páginas no accesibles, sin necesidad de que nos den los datos en formatos accesibles como *.csv
.
Lo cierto es que el web scrapping es superútil, pues muchas veces necesitamos descargar los datos de una web, pero ésta no los da accesibles, aunque los muestra. Entonces nos las tenemos que ingeniar para extraerlos directamente del código de la página, del html bruto, y con R esto es más sencillo de lo que parece.
El web scrapping es un buen ejercicio de programación y también de manifestar nuestra libertad… pues no hay que buscar mucho para comprobar que son las instituciones públicas las que más dificultades ponen para dar los datos libres y llegan al descaro de distribuir tablas en pdf en lugar de formatos directamente de trabajo para evitar a toda costa la transparencia de la información.
Así que, ¡luchemos por la transparencia !, descarguemos datos!… y vamos con un poco de ingeniería inversa.
Objetivo
Nuestro objetivo es extraer las cotizaciones del Ibex 35 en tiempo real de una web y con ellos montar nuestra propia web dinámica.
Usaremos web scrapping y Shiny con flexdashboard para montar nuestra web. Un punto importante es que queremos que nuestra web con datos “prestados” se actualice cada 60 segundos automáticamente.
Manos a la obra
Cogeremos los datos de la web de bolsa de Madrid en este enlace .
Usaremos la librería RVest
. Este paquete es uno de los más completos para web scrapping y contiene funciones como read_html()
que descarga el código html en bruto de una web como xml jerárquico. Esto nos permite acceder a los datos internos sin problemas. Una web es -al fin y al cabo- una sucesión jerárquica de nodos que esta función nos desgrana.
XPath
Un punto importante en el en el trabajo web scrapping con RVest
es identificar la parte de la página que contiene los datos y queremos leer. La función read_html()
lee el código fuente de la página y lo almacena como un conjunto de nodos ordenados. Para ver qué nodo nos interesa, una solución es saber algo de CSS o de html, con esto podemos ver el código en cualquier navegador y buscar la etiqueta XML o nodo que buscamos.
Hay algunas herramientas de serie en los navegadores, por ejemplo en Firefox podemos situarnos encima del punto de la web que queremos husmear y pulsar botón derecho en inspeccionar. Así abrimos la ventana de inspección de código html.
En el navegador Chrome, hay algunos gadget como selectorgadget que pueden ser muy útiles en este proceso.
HTML es el código fuente de las webs, y normalmente cada parte de una página tiene un nombre o etiqueta que lo define. Bueno en realidad las webs son XML, que es la generalización del lenguaje de etiquetas. XML es un formato genérico de etiquetado, que incluye otros como HTML, CSS, pero no quiero que nos perdamos en esto. Lo que necesitamos saber, es cómo se llama la parte que la página que vamos a extraer, y en la siguiente imagen te muestro cómo hacerlo con Firefox. Vamos a la web que buscamos www.bolsamadrid.es, y cuando estamos encima de la tabla de cotizaciones pulsamos el botón derecho, después en inspeccionar –> Copiar –> XPath.
De esta forma podemos acceder a cualquier parte de una web, averiguar su identificador o XPath y entonces extraerlo automáticamente con la R y la librería RVest
de la siguiente forma:
library(rvest)
# seleccionamos la url de la web en la que haremos web scraping
url.ibex<-"https://www.bolsamadrid.es/esp/aspx/Mercados/Precios.aspx?indice=ESI100000000"
# lee la web entera como nodos jerárquicos
pag <- read_html(url.ibex)
# Extrae el XPath que hemos obtenido
tabla <- html_nodes(pag,xpath='//*[@id="ctl00_Contenido_tblAcciones"]')
class(tabla)
## [1] "xml_nodeset"
Usamos la función read_html()
de RVest
, para leer la web en bruto, y después la función html_nodes()
, para acceder a un punto de esa web. Para identificar el nodo o punto usamos su matrícula en XML o XPath que hemos extraído según el proceso que explicamos antes.
Usar XPath es la forma general, pero html_nodes()
es una función que busca también etiquetas genéricas html como por ejemplo la etiqueta table, que identifica las tablas de datos web. Usando esto en combinación con otra función de RVest
llamada html_table()
podemos leer directamente las tablas como data_frame a R, vamos a ver cómo:
library(rvest)
# seleccionamos la url de la web en la que haremos web scraping
url.ibex<-"https://www.bolsamadrid.es/esp/aspx/Mercados/Precios.aspx?indice=ESI100000000"
# lee la web entera como nodos
pag <- read_html(url.ibex)
# extraemos los nodos que son de tipo tabla
tabla <- html_nodes(pag, "table")
# de ellos nos quedamos con la tabla num 5
tabla <- html_table(tabla [[5]], dec = ",")
head(tabla)
## Nombre Últ. % Dif. Máx. Mín. Volumen Efectivo (miles \200) Fecha
## 1 ACCIONA 112.800 0.53 112.800 111.800 31.988 3.593,11 10/12/2020
## 2 ACERINOX 8.864 -1.01 8.962 8.828 154.840 1.374,10 10/12/2020
## 3 ACS 26.840 -1.76 27.300 26.820 90.108 2.431,95 10/12/2020
## 4 AENA 136.900 -0.07 138.700 136.400 39.238 5.377,95 10/12/2020
## 5 ALMIRALL 10.770 1.22 10.770 10.630 120.436 1.288,17 10/12/2020
## 6 AMADEUS 61.700 1.28 62.160 60.820 213.271 13.104,06 10/12/2020
## Hora
## 1 11:17:01
## 2 11:31:51
## 3 11:32:53
## 4 11:32:46
## 5 11:31:48
## 6 11:31:52
Con apenas unas líneas hemos conseguido tener los datos de una web en un formato accesible para su tratamiento en R. Una vez tenemos la tabla en formato adecuado podemos limpiar, ordenar o filtrar lo que busquemos.
Crear una web dinámica con los datos extraídos
Una vez tenemos los datos, vamos a hacer la web que automatice el proceso cada cierto tiempo y que muestre la tabla de forma personalizada, para esto voy a usar flexdashboard, ya sabéis que me gusta usar flex para hacer mis aplicaciones de R.
Y ¿cómo hacemos que se actualice cada 60 segundos?
Actualizar web automáticamente
Bueno, para eso se me ha ocurrió usar una función de Shiny invalidateLater
.
Esta función hace que una variable reactiva no funcione durante un tiempo determinado, … que si le damos la vuelta es lo mismo a: que solo reaccione cada x milisegundos. Es decir debemos crear una variable reactiva que tenga la función actualizar tabla, y esta variable solo se activará cada cierto tiempo con invalidateLater
. Algo así:
# variable reactiva que llama a su vez a la función de web scrapping
actualizar <- reactive({
invalidateLater(1000*60,session) # se actualiza cada 60s
# Llama a la función que descarga el fichero y hace web scraping
lee_web(url.ibex2)
})
Resultado final
Vamos a juntarlo todo. Antes, soy un poco pijotero con los formatos, a veces paso más tiempo ajustando formato que con la programación, en fin, cada uno tiene sus vicios, así que el código de muestro final de la aplicación tiene más líneas para el formato de la tabla que para el ejercicio de web scrapping, que es bastante simple.
Si sois espartanos del color, quitad todo lo superfluo y con apenas 5 líneas de código tendréis una aplicación que muestra los datos de cotizaciones del IBEX35 en casi “tiempo real”.
Respecto al formato, en RMarkdown puedes añadir etiquetas de formato de estilo CSS directamente, la línea <style>........</style>
define un nuevo estilo para las tablas, en el que reduzco el tamaño de fuente y dejo los scroll.
También es interesante el formato que se muestra la tabla, pues incorporo en dos de las columnas opciones gráficas. En la columna Dif, se muestran de rojo los valores que bajan y de verde los que suben usando el comando formatStyle
, y styleInterval
, que sirve para definir escalas en un DT
.
En la columna Efectivo
se dibuja una barra según el valor de Efectivo, esto lo hacemos con styleColorBar
y los parámetros que veréis en la muestra.
Creo que el resultado final es bastante atractivo, y es un ejemplo bueno de los resultados tan profesionales que podemos lograr con R, y Shiny.
Solo recordaros que para publicar la app, podemos o correrla en local, para lo que solo necesitamos RSTUDIO, o montar un servidor Shiny, como vimos en este post por ejemplo y publicarla.
Personalmente la mejor opción es usar las 5 app gratuitas que nos dejan con la cuenta de https://www.shinyapps.io. Para publicarlo allí se hace desde RSTUDIO directamente clikcando en publicar cuando corremos la app en local (arriba a la derecha), y antes registrandose en la web.
Este es el código final:
Codigo completo del ejemplo:
---
title: "App_Bolsa"
output:
flexdashboard::flex_dashboard:
theme: spacelab
orientation: columns
vertical_layout: fill
editor_options:
chunk_output_type: console
runtime: shiny
---
'''{r setup, include=FALSE}
library(flexdashboard)
library(DT)
library(rvest)
library(dplyr)
'''
<style> .datatables{ overflow: auto; font-size: 7pt} </style>
# BolsaMadrid {data-width=600}
Lee los datos de la web de bolsa de Madrid en tiempo real.
'''{r tiempo}
# display con la fecha de lectura de la web
textOutput("contador")
'''
### Cotización Ibex 35
'''{r bolsaMadrid_chunk}
## vamos a leer los datos que da la web de Bolsa de Madrid del IBEX 35 en tiempo real retrasado 15 min
url.ibex2<-"https://www.bolsamadrid.es/esp/aspx/Mercados/Precios.aspx?indice=ESI100000000"
# función que lee la web y da una tabla con los datos
lee_web <- function(ruta) {
tmp1 <- read_html(ruta) # lee la web
tmp1 <- html_nodes(tmp1, "table")
#Leemos la tabla las comas son decimales
ibex1 <- html_table(tmp1[[5]], dec = ",")
#Cambiamos los nombres a más simples
nombres<-c("Nombre", "Ult","Dif","Max" ,"Min","Volumen","Efectivo","Fecha","Hora")
names(ibex1)<-nombres
#Limpiamos la tabla y corregimos columnas
ibex1$Efectivo<-gsub(".","",ibex1$Efectivo, fixed = TRUE) # quito puntos
ibex1$Efectivo<-gsub(",",".",ibex1$Efectivo, fixed = TRUE) #cambio coma por punto
ibex1$Efectivo<-as.numeric(ibex1$Efectivo) # convierto en numerico
ibex1$Fecha<- as.Date(strptime(ibex1$Fecha, "%d/%m/%Y")) # convierto a fecha
nombres<-gsub(".","",nombres, fixed = TRUE)
names(ibex1)<-nombres
return(ibex1)
}
# variable reactiva que llama a su vez a la función de scrapping
ibex1 <- reactive({
invalidateLater(1000*30,session)
# Actualiza la fecha cada vez que
output$contador<- renderText({as.character(as.POSIXct(Sys.time()))})
lee_web(url.ibex2)
})
# pinta la tabla de los datos del IBEX
renderDataTable({
DT::datatable(ibex1() , options = list(pageLength = 35,
bPaginate = TRUE), rownames= FALSE) %>% # Opciones formato de tabla básicas
formatStyle("Dif",# formto de la col de % Dif
background = styleInterval(c(0),c("coral","lightgreen"))) %>%
formatStyle("Efectivo", # formato de la columna de efectivo
background = styleColorBar(range(ibex1()$Efectivo), 'lightblue'),
backgroundSize = '98% 60%',
backgroundRepeat = 'no-repeat',
backgroundPosition = 'center') %>%
formatStyle(names(ibex1()), lineHeight='50%') # reduce el alto de TODAS las filas
})