Guía para principiantes sobre Web Scraping con Rust

Mihai Maxim el 17 oct 2022

¿Es Rust adecuado para el web scraping?

Rust es un lenguaje de programación diseñado para ser rápido y eficaz. A diferencia de C o C++, Rust cuenta con un gestor de paquetes integrado y una herramienta de compilación. También tiene una excelente documentación y un compilador amigable con útiles mensajes de error. Cuesta un poco acostumbrarse a la sintaxis. Pero una vez que lo hagas, te darás cuenta de que puedes escribir funcionalidades complejas con sólo unas pocas líneas de código. Web scraping con Rust es una experiencia enriquecedora. Obtienes acceso a potentes bibliotecas de scraping que hacen la mayor parte del trabajo pesado por ti. Como resultado, puedes pasar más tiempo en las partes divertidas, como el diseño de nuevas características. En este artículo, te guiaré a través del proceso de construcción de un raspador web con Rust. 

Cómo instalar el óxido

Instalar Rust es un proceso bastante sencillo. Visita Instalar Rust - Lenguaje de programación Rust (rust-lang.org) y sigue el tutorial recomendado para tu sistema operativo. La página muestra contenidos diferentes en función del sistema operativo que estés utilizando. Al final de la instalación, asegúrate de abrir un terminal nuevo y ejecutar rustc --version. Si todo ha ido bien, deberías ver el número de versión del compilador de Rust instalado.

Since we will be building a web scraper, let’s create a Rust project with Cargo. Cargo is Rust’s build system and package manager. If you used the official installers provided by rust-lang.org, Cargo should be already installed. Check whether Cargo is installed by entering the following into your terminal:  cargo --version.  If you see a version number, you have it! If you see an error, such as command not found, look at the documentation for your method of installation to determine how to install Cargo separately. To create a project, navigate to the desired project location and run cargo new <project name>.

Esta es la estructura por defecto del proyecto:

  •   El código se escribe en archivos .rs.
  •   Las dependencias se gestionan en el archivo Cargo.toml.
  •   Visita crates.io: Rust Package Registry para encontrar paquetes para Rust.

Construir un raspador web con Rust

Ahora echemos un vistazo a cómo podrías usar Rust para construir un scraper. El primer paso es definir un propósito claro. ¿Qué quiero extraer? El siguiente es decidir cómo quieres almacenar los datos extraídos. La mayoría de la gente los guarda como .json, pero en general deberías considerar el formato que mejor se adapte a tus necesidades individuales. Una vez resueltos estos dos requisitos, puedes avanzar con confianza en la implementación de cualquier scraper. Para ilustrar mejor este proceso, propongo que creemos una pequeña herramienta que extraiga datos Covid del sitio web COVID Live - Coronavirus Statistics - Worldometer (worldometers.info). Debería analizar las tablas de casos notificados y almacenar los datos como .json. Crearemos este scraper juntos en los siguientes capítulos.

Obtención de HTML con peticiones HTTP

Para extraer las tablas, primero necesitaremos obtener el HTML que está dentro de la página web. Utilizaremos la caja/biblioteca "reqwest" para obtener el HTML en bruto de la página web.

En primer lugar, añádalo como dependencia en el archivo Cargo.toml:

reqwest = { version = "0.11", features = ["bloqueo", "json"] }

A continuación, defina su url de destino y envíe su solicitud:

let url = "https://www.worldometers.info/coronavirus/";
let response = reqwest::blocking::get(url).expect("No se pudo cargar la url.");

La función de "bloqueo" garantiza que la petición sea sincrónica. Como resultado, el programa esperará a que se complete y luego continuará con las demás instrucciones. 

let raw_html_string = response.text().unwrap();

Uso de selectores CSS para localizar datos

Tienes todos los datos brutos necesarios. Ahora tienes que encontrar una manera de localizar las tablas de casos reportados. La librería de Rust más popular para este tipo de tareas se llama "scraper". Permite analizar HTML y realizar consultas con selectores CSS.

Añada esta dependencia a su archivo Cargo.toml:

rascador = "0.13.0"

Añade estos módulos a tu archivo main.rs.

use scraper::Selector;
use scraper::Html;

Ahora utiliza la cadena HTML sin procesar para crear un fragmento HTML:

let html_fragment = Html::parse_fragment(&raw_html_string);

Seleccionaremos las tablas que muestran los casos notificados hoy, ayer y hace dos días.

blog-image

Abra la consola de desarrollador e identifique los identificadores de las tablas:

blog-image

En el momento de escribir este artículo, el id para hoy es: "main_table_countries_today".

Los otros dos identificadores de tabla son:
"main_table_countries_yesterday" y "main_table_countries_yesterday2"

Ahora vamos a definir algunos selectores:

let tabla_selector_string = "#tabla_principal_paises_hoy, #tabla_principal_paises_ayer, #tabla_principal_paises_ayer2";

let tabla_selector = Selector::parse(tabla_selector_string).unwrap();

let head_elements_selector = Selector::parse("thead>tr>th").unwrap();

let row_elements_selector = Selector::parse("tbody>tr").unwrap();

let row_element_data_selector = Selector::parse("td, th").unwrap();

Pasa la cadena table_selector_string al método html_fragment select para obtener las referencias de todas las tablas:

let all_tables = html_fragment.select(&table_selector);

Utilizando las referencias de las tablas, crea un bucle que analice los datos de cada tabla.

for table in all_tables{
let head_elements = table.select(&head_elements_selector);
for head_element in head_elements{
//parse the header elements
}

let head_elements = table.select(&head_elements_selector);
for row_element in row_elements{
for td_element in row_element.select(&row_element_data_selector){
//parse the individual row elements
}
}
}

Análisis de los datos

El formato en el que se almacenan los datos dicta la forma de analizarlos. Para este proyecto, es .json. En consecuencia, tenemos que poner los datos de la tabla en pares clave-valor. Podemos utilizar los nombres de cabecera de la tabla como claves y las filas de la tabla como valores. 

Utilice la función .text() para extraer las cabeceras y almacenarlas en un Vector:

//for table in tables loop
let mut head:Vec<String> = Vec::new();

let head_elements = table.select(&head_elements_selector);

for head_element in head_elements{
let mut element = head_element.text().collect::<Vec<_>>().join(" ");
element = element.trim().replace("\n", " ");
head.push(element);
}


//head
["#", "Country, Other", "Total Cases", "New Cases", "Total Deaths", ...]

Extraiga los valores de las filas de forma similar:

//for table in tables loop
let mut rows:Vec<Vec<String>> = Vec::new();

let row_elements = table.select(&row_elements_selector);

for row_element in row_elements{
let mut row = Vec::new();
for td_element in row_element.select(&row_element_data_selector){
let mut element = td_element.text().collect::<Vec<_>>().join(" ");
element = element.trim().replace("\n", " ");
row.push(element);
}
rows.push(row)

}
//rows
[...
["", "World", "625,032,352", "+142,183", "6,555,767", ...]
...
["2", "India", "44,604,463", "", "528,745", ...]
...]

Utilice la función zip() para crear una correspondencia entre los valores de cabecera y fila:

for row in rows {
let zipped_array = head.iter().zip(row.iter()).map(|(a, b)|
(a,b)).collect::<Vec<_>>();
}

//zipped_array
[
...
[("#", ""), ("Country, Other", "World"), ("Total Cases", "625,032,352"), ("New Cases", "+142,183"), ("Total Deaths", "6,555,767"), ...]
...
]

Ahora almacene los pares zipped_array (clave, valor) en un IndexMap:

serde = {version="1.0.0",features = ["derive"]}

indexmap = {version="1.9.1", features = ["serde"]} (añade estas dependencias)
use indexmap::IndexMap;

//use this to store all the IndexMaps
let mut table_data:Vec<IndexMap<String, String>> = Vec::new();
for row in rows {
let zipped_array = head.iter().zip(row.iter()).map(|(a, b)|
(a,b)).collect::<Vec<_>>();
let mut item_hash:IndexMap<String, String> = IndexMap::new();
for pair in zipped_array{
//we only want the non empty values
if !pair.1.to_string().is_empty(){
item_hash.insert(pair.0.to_string(), pair.1.to_string());
}
}
table_data.push(item_hash);

//table_data
[
...
{"Country, Other": "North America", "Total Cases": "116,665,220", "Total Deaths": "1,542,172", "Total Recovered": "111,708,347", "New Recovered": "+2,623", "Active Cases": "3,414,701", "Serious, Critical": "7,937", "Continent": "North America"}
,
{"Country, Other": "Asia", "Total Cases": "190,530,469", "New Cases": "+109,009", "Total Deaths": "1,481,406", "New Deaths": "+177", "Total Recovered": "184,705,387", "New Recovered": "+84,214", "Active Cases": "4,343,676", "Serious, Critical": "10,640", "Continent": "Asia"}
...
]

IndexMap es una gran elección para almacenar los datos de la tabla porque preserva el orden de inserción de los pares (clave, valor).

Serialización de los datos

Ahora que puedes crear objetos tipo json con datos de tablas, es hora de serializarlos a .json. Antes de empezar, asegúrate de que tienes todas estas dependencias instaladas:

serde = {version="1.0.0",features = ["derivar"]}
serde_json = "1.0.85"
indexmap = {version="1.9.1", features = ["serde"]}

Almacena cada dato_tabla en un vector datos_tabla:

let mut tables_data: Vec<Vec<IndexMap<String, String>>> = Vec::new();

For each table:
//fill table_data (see previous chapter)
tables_data.push(table_data);

Definir un struct contenedor para los tables_data:

 #[derive(Serialize)]
struct FinalTableObject {
tables: IndexMap<String, Vec<IndexMap<String, String>>>,
}

Instanciar la estructura:

 let final_table_object = FinalTableObject{tables: tables_data};

Serializa la estructura a una cadena .json:

let serialized = serde_json::to_string_pretty(&final_table_object).unwrap();

Escribe la cadena .json serializada en un archivo .json:

use std::fs::File;
use std::io::{Write};

let path = "out.json";

let mut output = File::create(path).unwrap();

let result = output.write_all(serialized.as_bytes());

match result {

Ok(()) => println!("Successfully wrote to {}", path),

Err(e) => println!("Failed to write to file: {}", e),

}

Y ya está. Si todo ha ido bien, su salida .json debe ser similar:

{
"tables": [
[ //table data for #main_table_countries_today
{
"Country, Other": "North America",
"Total Cases": "116,665,220",
"Total Deaths": "1,542,172",
"Total Recovered": "111,708,347",
"New Recovered": "+2,623",
"Active Cases": "3,414,701",
"Serious, Critical": "7,937",
"Continent": "North America"
},
...
],
[...table data for #main_table_countries_yesterday...],
[...table data for #main_table_countries_yesterday2...],
]
}

You can find the whole code for the project at [Rust][A simple <table> scraper] (github.com)

Adaptación a otros usos

Si me has seguido hasta aquí, probablemente te habrás dado cuenta de que puedes utilizar este scraper en otros sitios web. El raspador no está vinculado a una tabla específica de columnas o convención de nomenclatura. Además, no depende de muchos selectores CSS. Así que no debería ser necesario hacer muchos ajustes para que funcione con otras tablas, ¿verdad? Pongamos a prueba esta teoría.

blog-image

We need a selector for the <table> tag.

blog-image

Si class="wikitable sortable jquery-tablesorter", podrías cambiar el table_selector por:

let tabla_selector_string = ".wikitable.sortable.jquery-tablesorter";
let tabla_selector = Selector::parse(tabla_selector_string).unwrap();

This table has the same <thead> <tbody> structure, so there is no reason to change the other selectors.

El rascador debería funcionar ahora. Vamos a probarlo:

{
"tables": []
}

Webscraping con Rust es divertido, ¿verdad? 

¿Cómo podría fallar? 

Profundicemos un poco más:

La forma más sencilla de averiguar qué ha fallado es mirar el HTML devuelto por la petición GET:

let url = "https://en.wikipedia.org/wiki/List_of_countries_by_population_in_2010";


let response = reqwest::blocking::get(url).expect("No se ha podido cargar la url.");

et raw_html_string = response.text().unwrap();

let path = "debug.html";


let mut output = File::create(path).unwrap();

let result = output.write_all(raw_html_string.as_bytes());
blog-image

El HTML devuelto por la petición GET es diferente del que vemos en la página web real. El navegador ofrece un entorno para que se ejecute Javascript y altere el diseño de la página. En el contexto de nuestro scraper, obtenemos la versión no alterada.

Our table_selector did not work because the “jquery-tablesorter” class is injected dynamically by Javascript. Also, you can see that the <table> structure is different. The <thead> tag is missing. The table head elements are now found in the first <tr> of the <tbody>. Thus, they will be picked up by the row_elements_selector.

Removing “jquery-tablesorter” from the table_selector is not enough, we also need to handle the missing <tbody> case:

let cadena_selector_tabla = ".wikitable.sortable";
 if head.is_empty() {
head=rows[0].clone();
rows.remove(0);
}// take the first row values as head if there is no <thead>

Ahora vamos a darle otra vuelta:

{
"tables": [
[
{
"Rank": "--",
"Country / territory": "World",
"Population 2010 (OECD estimate)": "6,843,522,711"
},
{
"Rank": "1",
"Country / territory": "China",
"Population 2010 (OECD estimate)": "1,339,724,852",
"Area (km 2 ) [1]": "9,596,961",
"Population density (people per km 2 )": "140"
},
{
"Rank": "2",
"Country / territory": "India",
"Population 2010 (OECD estimate)": "1,182,105,564",
"Area (km 2 ) [1]": "3,287,263",
"Population density (people per km 2 )": "360"
},
...
]
]

¡Así está mejor!

Resumen

Espero que este artículo proporcione un buen punto de referencia para el web scraping con Rust. Aunque el rico sistema de tipos y el modelo de propiedad de Rust pueden ser un poco abrumadores, no es en absoluto inadecuado para el web scraping. Tienes un compilador amigable que constantemente te apunta en la dirección correcta. También encontrarás mucha documentación bien escrita: The Rust Programming Language - El lenguaje de programación Rust (rust-lang.org).

Construir un raspador web no siempre es un proceso sencillo. Se enfrentará a la renderización de Javascript, bloqueos de IP, captchas y muchos otros contratiempos. En WebScraping API, le proporcionamos todas las herramientas necesarias para combatir estos problemas comunes. ¿Tienes curiosidad por saber cómo funciona? Puede probar nuestro producto de forma gratuita en WebScrapingAPI - Producto. O puede ponerse en contacto con nosotros en WebScrapingAPI - Contacto. Estaremos encantados de responder a todas sus preguntas.

Noticias y actualidad

Manténgase al día de las últimas guías y noticias sobre raspado web suscribiéndose a nuestro boletín.

We care about the protection of your data. Read our <l>Privacy Policy</l>.Privacy Policy.

Artículos relacionados

miniatura
Ciencia del Web ScrapingScrapy vs. Selenium: Guía completa para elegir la mejor herramienta de Web Scraping

Explore la comparación en profundidad entre Scrapy y Selenium para el scraping web. Desde la adquisición de datos a gran escala hasta la gestión de contenido dinámico, descubra los pros, los contras y las características únicas de cada uno. Aprenda a elegir el mejor marco de trabajo en función de las necesidades y la escala de su proyecto.

WebscrapingAPI
avatar de autor
WebscrapingAPI
14 min leer
miniatura
GuíasTutorial de Scrapy Splash: Dominar el arte del scraping de sitios web renderizados en JavaScript con Scrapy y Splash

Aprenda a scrapear sitios web dinámicos con JavaScript utilizando Scrapy y Splash. Desde la instalación hasta la escritura de una araña, el manejo de la paginación y la gestión de las respuestas de Splash, esta completa guía ofrece instrucciones paso a paso tanto para principiantes como para expertos.

Ștefan Răcila
avatar de autor
Ștefan Răcila
6 min leer
miniatura
Casos prácticosUtilizando Web Scraping para Datos Alternativos en Finanzas: Guía completa para inversores

Explore el poder transformador del web scraping en el sector financiero. Desde datos de productos hasta análisis de opiniones, esta guía ofrece información sobre los distintos tipos de datos web disponibles para tomar decisiones de inversión.

Mihnea-Octavian Manolache
avatar de autor
Mihnea-Octavian Manolache
13 min leer