Resumen del tutorial de Scraping con Spring Boot.
-
Realizar una pequeña introducción al scraping de páginas web.
-
Conocer la estructura de una página web.
-
Aprender a usar jsoup.
-
Realizar una API Rest de manera rápida y sencilla obteniendo los datos de otra página web.
-
Utilizar scraping en una página que cargue los datos de forma dinámica.
El Web scraping es una técnica utilizada mediante programas de software para extraer información de sitios web. Usualmente, estos programas simulan la navegación de un humano por Internet, ya sea utilizando el protocolo HTTP manualmente, o incrustando un navegador en una aplicación. Esta técnica se enfoca más en la transformación de datos sin estructura en la web (como el formato HTML) en datos estructurados que pueden ser almacenados y analizados en una base de datos, en una hoja de cálculo o en alguna otra fuente de almacenamiento. Alguno ejemplos de uso del web scraping son la comparación de precios en tiendas, la monitorización de datos relacionados con el clima de cierta región, la detección de cambios en sitios webs y la integración de datos en sitios webs.
A lo largo del desarrollo de la práctica vamos realizar scraping de una página web y expondremos los datos como una pequeña API Rest con Spring Boot de manera muy sencilla.
Note
|
Podemos partir del proyecto creado en el Tutorial de Spring Boot. |
Para crear un proyecto con todas las configuraciones predefinidas vamos a entrar en la página start.spring.io y poned las configuraciones que salen a continuación para añadir los paquetes solo tendréis que poner sus nombres en el campo de “Search for dependencies”.
-
Pulsamos en ADD DEPENDENCIES para buscar y añadir las siguientes dependencias:
- Spring Web
-
Permite construir aplicaciones web utilizando un contenedor Apache Tomcat.
-
Una vez seleccionemos las dependencias pulsaremos en GENERATE para descargar un .zip con nuestro proyecto, el cual se puede extraer en el directorio que queramos.
Para trabajar con Spring Boot en Visual Studio Code vamos a instalar un conjunto de extensiones que nos hagan más facil la vida.
Para ello visitamos la página https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack para instalar el Extension Pack for Java.
Pulsaremos en Install
para incorporar las siguientes extensiones:
-
Language Support for Java™ by Red Hat
-
Debugger for Java
-
Test Runner for Java
-
Maven for Java
-
Project Manager for Java
-
Visual Studio IntelliCode
Una vez instalada nos aparece la ventana de Get Started donde podemos instalar una versión de JDK si no tenemos ya una.
Note
|
Se recomienda instalar una versión igual o superior a JDK 11 |
También necesitaremos instalar el soporte para Spring Boot mediante el siguiente pack de extensiones.
Pulsaremos en Install
para incorporar las siguientes extensiones:
-
Spring Boot
-
Spring Initializr Java
-
Spring Boot Dashboard
El siguiente paso será abrir la carpeta del proyecto en Visual Studio code. Podemos comprobar que el proyecto se ha cargado correctamente cuando aparezca en los tabs de JAVA PROJECTS, MAVEN PROJECTS y SPRING BOOT DASHBOARD.
Important
|
Si en la pestaña PROBLEMS nos dice que no encuentra un JDK para la versión del proyecto Spring Boot generado, podemos modificarla cambiando la versión en la siguiente linea del pom.xml |
<properties>
<java.version>17</java.version>
</properties>
Vamos a extraer los datos estadísticos sobre la COVID-19 de la entrada de Wikipedia COVID-19 pandemic by country and territory.
Para extraer los datos vamos a utilizar la libreria jsoup que permite extraer datos de una página web mediante consultas por identificador, etiqueta y selectores. Por lo tanto, tenemos que incluir la siguiente dependencia en el pom.xml
.
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.13.1</version>
</dependency>
El siguiente paso que vamos a hacer un DTO (Data Transfer Object). Estas clases son de tipo POJO (Plain Old Java Object) que es una clase que contiene un conjunto de atributos y métodos, y que se utilizan para estructurar los datos que vamos a extraer / devolver.
CovidDataDto.java
.package com.example.demo.dto;
public class CovidDataDto {
public String country; (1)
public Integer cases; (2)
public Integer deaths; (3)
public Integer recovered; (4)
public CovidDataDto(String country, Integer cases, Integer deaths, Integer recovered) { (5)
this.country = country;
this.cases = cases;
this.deaths = deaths;
this.recovered = recovered;
}
public CovidDataDto() {
}
}
-
Nombre del país.
-
Número de casos.
-
Número de muertes.
-
Número de recuperados.
-
Constructor de la clase.
Como veis hemos creado una clase CovidDataDto
con los campos country, cases, deaths y recovered.
A continuación vamos a crear una clase que nos permita extraer los datos de la página COVID-19 pandemic by country and territory.
Lo primero que vamos a hacer es analizar la página web para ver que selectores vamos a utilizar para extraer los datos que nos interesan.
En la sección Statistics > Total cases, deaths, and death rates by country
podemos utilizar las Herramientas de desarrollo
para ver que selectores vamos a utilizar. Como se ve en la imagen, la tabla se encuentra dentro de una capa llamada covid-19-cases-deaths-and-rates-by-location
y los elementos que nos interesan son los hijos de la etiqueta tbody
.
También podemos examinar el formato que tiene cada fila de la tabla para asi extraer el nombre del país y los datos de casos, muertes y recuperados.
A continuación podemos ver la clase CovidDataService
que nos permite extraer los datos de la página web.
CovidDataService.java
.package com.example.demo..services;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.stereotype.Component;
import es.ual.dra.scrapping.dto.CovidDataDto;
@Component("covidDataService") (1)
public class CovidDataService {
public List<CovidDataDto> retrieveCovidData() {
List<CovidDataDto> covidData = new ArrayList<>();
try {
Document webPage = Jsoup.connect("https://en.wikipedia.org/wiki/COVID-19_pandemic_by_country_and_territory")
.get(); (2)
Element tbody = webPage.getElementById("covid-19-cases-deaths-and-rates-by-location").getElementsByTag("tbody").get(0); (3)
List<Element> rows = tbody.children().subList(2, tbody.children().size()); (4)
for (Element row : rows) { (5)
Elements ths = row.getElementsByTag("th");
if(ths.isEmpty()) (6)
continue;
String country = ths.get(0).text(); (7)
Elements tds = row.getElementsByTag("td");
if (tds.size() < 3) (8)
continue;
Integer cases = toIntOrNull(tds.get(1).text()); (9)
Integer deaths = toIntOrNull(tds.get(2).text()); (9)
Integer recovered = toIntOrNull(tds.get(3).text()); (9)
covidData.add(new CovidDataDto(country, cases, deaths, recovered)); (10)
}
return covidData;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private Integer toIntOrNull(String replace) {
try {
return Integer.parseInt(replace.replace(",", ""));
} catch (NumberFormatException e) {
return null;
}
}
}
-
Anotación de componente.
-
Conexión con la página web.
-
Selección del elemento tbody de la capa con id
covid-19-cases-deaths-and-rates-by-location
. -
Selección de los elementos hijos de la etiqueta
tbody
que empiezan en la posición 2. -
Bucle for para recorrer los elementos hijos de la etiqueta
tbody
, es decir, cada país. -
Si el elemento
th
está vacío, se salta a la siguiente iteración. No es un país. -
Selección del elemento
th
que contiene el nombre del país. -
Si el número de elementos
td
es menor que 3, se salta a la siguiente iteración. Pertenece al pie de la tabla. -
Selección del elemento
td
que contiene el número de casos, muertes o recuperados. -
Se crea un nuevo objeto
CovidDataDto
con los datos del país.
Por último para la realización de este pequeño ejemplo vamos a crear un controlador que nos permita acceder a los datos mediante la url http://localhost:8080/covid/data.
CoviDataController.java
.package com.example.demo.controllers;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import es.ual.dra.scrapping.dto.CovidDataDto;
import es.ual.dra.scrapping.services.CovidDataService;
@RestController (1)
@RequestMapping("/covid") (2)
public class CovidDataController {
@Autowired
private CovidDataService covidDataService; (3)
@GetMapping("data") (4)
public ResponseEntity<List<CovidDataDto>> getCovidData() {
return new ResponseEntity<List<CovidDataDto>>(covidDataService.retrieveCovidData(),
HttpStatus.OK); (5)
}
}
-
Anotación de controlador REST.
-
Anotación de mapeo de petición en la URL
/covid
. -
Inyección de dependencia de la clase
CovidDataService
. -
Mapeo de petición GET a la URL
/covid/data
. -
Devuelve una lista de objetos
CovidDataDto
con los datos del scraping realizados por el servicio.
Para lanzar la aplicación podemos utilizar la pestaña SPRING BOOT DASHBOARD e iniciarla en modo normal o depuración.
Note
|
Si veis que las modificaciones sobre las clases no se aplican, podeis hacer un |
Si accedemos a la url http://localhost:8080/covid/data
nos devolverá una lista de objetos CovidDataDto
con los datos del scraping realizados por el servicio.
Con algo tan sencillo como lo que estáis viendo hemos conseguido levantar una API REST completamente funcional con los datos recuperados de la página web de Wikipedia.
En este ejercicio vamos a extraer los datos de los grados de la web de la Universidad de Almería. La gran diferencia entre esta web y la de la Wikipedia es que en la web de la UAL los datos se obtiene mediante llamadas a una API REST. En estos casos no podemos usar jsoup
directamente, porque este solo obtiene el HTML que devuelve el servidor, y no aplica los cambios que produce el javascript que se ejecuta en la web.
Podemos ver como la página sin Javascript no tiene los datos cargados.
Para solucionar este problema vamos a utilizar Playwright, una librería que nos permite abrir un navegador web, interactuar con él y recuperar el HTML tras la ejecución de las llamadas a la API REST. Con este HTML, utilizamos jsoup
para extraer los datos como en el ejemplo anterior.
En primer lugar, vamos a crear una clase GradosDto
que represente los datos de un grado: su nombre y su código.
GradosDto.java
.package com.example.demo.dto;
public class GradosDto { (1)
private String nombre; (2)
private String codigo; (3)
public GradosDto(String nombre, String codigo) {
this.nombre = nombre;
this.codigo = codigo;
}
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
public String getCodigo() {
return codigo;
}
public void setCodigo(String codigo) {
this.codigo = codigo;
}
}
-
Clase DTO para representar los datos de un grado.
-
Atributo
nombre
del grado. -
Atributo
codigo
del grado.
Ahora vamos a crear una clase GradosService
que nos permita extraer los datos de los grados de la Universidad de Almería. Para ello vamos a utilizar la librería Playwright
que importaremos en nuestro pom.xml
.
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.17.1</version>
</dependency>
Además necesitamos un selector que nos permita detectar cuando se ha cargado la página de la Universidad de Almería. Podemos inspeccionar el último elemento de la lista de grados y copiar su selector mediante el botón derecho y la opción Copy > Copy selector
.
body > div > div > div.container.main > div > section > div:nth-child(2) > div:nth-child(19) > div:nth-child(1) > ul > li:nth-child(6) > a > span.ng-binding
Lo último que necesitamos es el selector de los grados, así como su formato. En este caso podemos utilizar .sinvinetas > li > a
.
Con estos datos podemos crear nuestra clase GradosService
:
GradosService.java
.package com.example.demo.services;
import java.util.ArrayList;
import java.util.List;
import com.microsoft.playwright.Browser;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.Playwright;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.stereotype.Component;
import es.ual.dra.scrapping.dto.GradosDto;
@Component("gradosService")
public class GradosService {
public List<GradosDto> retrieveGrados() {
List<GradosDto> gradosList = new ArrayList<>();
Playwright playwright = Playwright.create();
Browser browser = playwright.webkit().launch();
Page page = browser.newPage();
page.navigate("https://www.ual.es/estudios/grados"); (1)
page.waitForSelector(
"body > div > div > div.container.main > div > section > div:nth-child(2) > div:nth-child(17) > div:nth-child(2) > div:nth-child(9) > div > ul > li:nth-child(2) > a > span"); (2)
Document webPage = Jsoup.parse(page.content()); (3)
Elements grados = webPage.select(".sinvinetas > li > a"); (4)
for (Element grado : grados) {
if (grado == null)
continue;
Element nombrElement = grado.selectFirst(".ng-binding"); (5)
if (nombrElement == null)
continue;
String nombre = nombrElement.text();
String codigo = grado.attr("href").replace("/estudios/grados/presentacion/", ""); (6)
gradosList.add(new GradosDto(nombre, codigo)); (7)
}
return gradosList;
}
}
-
Cargamos la página de grados la Universidad de Almería.
-
Esperamos a que se cargue el selector del último de los grados.
-
Obtenemos el HTML de la página y se lo pasamos a
Jsoup
para extraer los datos. -
Seleccionamos los grados.
-
Obtenemos el nombre del grado.
-
Obtenemos el código del grado.
-
Creamos un objeto
GradosDto
con los datos del grado.
Para terminar vamos a crear la clase GradosController
que nos permita devolver los datos de los grados de la Universidad de Almería.
GradosController.java
.package com.example.demo.controllers;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import es.ual.dra.scrapping.dto.GradosDto;
import es.ual.dra.scrapping.services.GradosService;
@RestController
@RequestMapping("/grados") (1)
public class GradosController {
@Autowired
private GradosService gradosService; (2)
@GetMapping("data") (1)
public ResponseEntity<List<GradosDto>> getGrados() {
return new ResponseEntity<List<GradosDto>>(gradosService.retrieveGrados(),
HttpStatus.OK); (3)
}
}
-
Mapeamos la ruta
/grados/data
para obtener los datos de los grados. -
Inyectamos el servicio de grados.
-
Devolvemos la lista de grados.
Si accedemos a la url http://localhost:8080/grados/data
nos devolverá una lista de objetos GradosDto
con los datos del scraping realizados por el servicio.
Busca alguna fuente de datos para realizar scraping que puedas utilizar en tu proyecto individual.
Tip
|
Puede ser de ayuda https://www.baeldung.com/gson-save-file |