Le guide du scraping web avec Rust pour les débutants
Mihai Maxim le 17 octobre 2022
Rust est-il adapté au web scraping ?
Rust est un langage de programmation conçu pour être rapide et efficace. Contrairement à C ou C++, Rust dispose d'un gestionnaire de paquets et d'un outil de construction intégrés. Il dispose également d'une excellente documentation et d'un compilateur convivial avec des messages d'erreur utiles. Il faut un certain temps pour s'habituer à la syntaxe. Mais une fois que vous y serez parvenu, vous vous rendrez compte que vous pouvez écrire des fonctionnalités complexes avec seulement quelques lignes de code. Le scraping web avec Rust est une expérience enrichissante. Vous avez accès à de puissantes bibliothèques de scraping qui font le gros du travail à votre place. Par conséquent, vous pouvez passer plus de temps sur les parties amusantes, comme la conception de nouvelles fonctionnalités. Dans cet article, je vais vous guider à travers le processus de construction d'un scraper web avec Rust.
Comment installer Rust
L'installation de Rust est assez simple. Visitez le site Install Rust - Rust Programming Language (rust-lang.org) et suivez le tutoriel recommandé pour votre système d'exploitation. Le contenu de la page varie en fonction du système d'exploitation que vous utilisez. À la fin de l'installation, ouvrez un nouveau terminal et lancez rustc --version. Si tout s'est bien passé, vous devriez voir le numéro de version du compilateur Rust installé.
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>.
Il s'agit de la structure de projet par défaut :
- Vous écrivez du code dans des fichiers .rs.
- Vous gérez les dépendances dans le fichier Cargo.toml.
- Visitez crates.io : Rust Package Registry pour trouver des paquets pour Rust.
Construire un scraper web avec Rust
Voyons maintenant comment utiliser Rust pour construire un scraper. La première étape consiste à définir un objectif clair. Qu'est-ce que je veux extraire ? L'étape suivante consiste à décider de la manière dont vous souhaitez stocker les données extraites. La plupart des gens les enregistrent au format .json, mais vous devriez généralement considérer le format qui convient le mieux à vos besoins individuels. Une fois ces deux exigences définies, vous pouvez avancer en toute confiance dans l'implémentation de n'importe quel scraper. Pour mieux illustrer ce processus, je propose de construire un petit outil qui extrait les données Covid du site web COVID Live - Coronavirus Statistics - Worldometer (worldometers.info). Il doit analyser les tableaux des cas signalés et stocker les données au format .json. Nous créerons ce scraper ensemble dans les chapitres suivants.
Récupérer du HTML avec des requêtes HTTP
Pour extraire les tableaux, il faut d'abord récupérer le code HTML contenu dans la page web. Nous utiliserons la bibliothèque "reqwest" pour récupérer le HTML brut du site web.
Tout d'abord, ajoutez-le comme dépendance dans le fichier Cargo.toml :
reqwest = { version = "0.11", features = ["blocking", "json"] }
Définissez ensuite votre url cible et envoyez votre demande :
let url = "https://www.worldometers.info/coronavirus/" ;
let response = reqwest::blocking::get(url).expect("Could not load url.") ;
La caractéristique de "blocage" garantit que la demande est synchrone. Par conséquent, le programme attendra qu'elle soit terminée avant de poursuivre les autres instructions.
let raw_html_string = response.text().unwrap() ;
Utiliser des sélecteurs CSS pour localiser des données
Vous avez obtenu toutes les données brutes nécessaires. Vous devez maintenant trouver un moyen de localiser les tableaux des cas signalés. La bibliothèque Rust la plus populaire pour ce type de tâche s'appelle "scraper". Elle permet l'analyse HTML et l'interrogation avec des sélecteurs CSS.
Ajoutez cette dépendance à votre fichier Cargo.toml :
scraper = "0.13.0"
Ajoutez ces modules à votre fichier main.rs.
use scraper::Selector ;
use scraper::Html ;
Utilisez maintenant la chaîne HTML brute pour créer un fragment HTML :
let html_fragment = Html::parse_fragment(&raw_html_string) ;
Nous sélectionnerons les tableaux qui affichent les cas déclarés pour aujourd'hui, hier et il y a deux jours.

Ouvrez la console du développeur et identifiez les identifiants des tables :

Au moment de la rédaction de cet article, l'identifiant pour aujourd'hui est : "main_table_countries_today".
Les deux autres identifiants de table sont :
"main_table_countries_yesterday" et "main_table_countries_yesterday2".
Définissons maintenant quelques sélecteurs :
let table_selector_string = "#main_table_countries_today, #main_table_countries_yesterday, #main_table_countries_yesterday2" ;
let table_selector = Selector::parse(table_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() ;
Passez la chaîne table_selector_string à la méthode html_fragment select pour obtenir les références de toutes les tables :
let all_tables = html_fragment.select(&table_selector) ;
En utilisant les références des tableaux, créez une boucle qui analyse les données de chaque tableau.
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
}
}
}
Analyse des données
Le format dans lequel vous stockez les données dicte la manière dont vous les analysez. Pour ce projet, il s'agit de .json. Par conséquent, nous devons placer les données du tableau dans des paires clé-valeur. Nous pouvons utiliser les noms des en-têtes de table comme clés et les lignes de la table comme valeurs.
Utilisez la fonction .text() pour extraire les en-têtes et les stocker dans un vecteur :
//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", ...]
Extrayez les valeurs des lignes de la même manière :
//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", ...]
...]
Utilisez la fonction zip() pour créer une correspondance entre les valeurs de l'en-tête et de la ligne :
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"), ...]
...
]
Stockez maintenant les paires zipped_array (clé, valeur) dans un IndexMap :
serde = {version="1.0.0",features = ["derive"]}
indexmap = {version="1.9.1", features = ["serde"]} (ajouter ces dépendances)
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 est un excellent choix pour stocker les données d'une table car il préserve l'ordre d'insertion des paires (clé, valeur).
Sérialisation des données
Maintenant que vous pouvez créer des objets de type json avec des données de table, il est temps de les sérialiser en .json. Avant de commencer, assurez-vous que toutes ces dépendances sont installées :
serde = {version="1.0.0",features = ["derive"]}
serde_json = "1.0.85"
indexmap = {version="1.9.1", features = ["serde"]}
Stocker chaque table_data dans un vecteur tables_data :
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);
Définir un conteneur struct pour les tables_data :
#[derive(Serialize)]
struct FinalTableObject {
tables: IndexMap<String, Vec<IndexMap<String, String>>>,
}
Instanciation de la structure :
let final_table_object = FinalTableObject{tables: tables_data};
Sérialise la structure en une chaîne .json :
let serialized = serde_json::to_string_pretty(&final_table_object).unwrap() ;
Écriture de la chaîne .json sérialisée dans un fichier .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),
}
Et voilà, vous avez terminé. Si tout s'est bien passé, votre fichier .json de sortie devrait ressembler à ceci :
{
"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)
Faire des ajustements pour s'adapter à d'autres cas d'utilisation
Si vous m'avez suivi jusqu'ici, vous avez probablement réalisé que vous pouvez utiliser ce scraper sur d'autres sites web. Le scraper n'est pas lié à un nombre de colonnes de tableau spécifique ou à une convention de nommage. Il ne repose pas non plus sur de nombreux sélecteurs CSS. Il ne devrait donc pas être nécessaire de procéder à de nombreuses modifications pour le faire fonctionner avec d'autres tableaux, n'est-ce pas ? Testons cette théorie.

We need a selector for the <table> tag.

Si class="wikitable sortable jquery-tablesorter", vous pouvez changer le table_selector en :
let table_selector_string = ".wikitable.sortable.jquery-tablesorter" ;
let table_selector = Selector::parse(table_selector_string).unwrap() ;
This table has the same <thead> <tbody> structure, so there is no reason to change the other selectors.
Le scraper devrait maintenant fonctionner. Faisons un essai :
{
"tables": []
}
Le webcraping avec Rust est amusant, n'est-ce pas ?
Comment cela pourrait-il échouer ?
Creusons un peu plus loin :
Le moyen le plus simple de déterminer ce qui n'a pas fonctionné est d'examiner le code HTML renvoyé par la requête GET :
let url = "https://en.wikipedia.org/wiki/List_of_countries_by_population_in_2010" ;
let response = reqwest::blocking::get(url).expect("Could not load 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()) ;

Le code HTML renvoyé par la requête GET est différent de celui que nous voyons sur le site web actuel. Le navigateur offre un environnement pour l'exécution de Javascript et la modification de la présentation de la page. Dans le contexte de notre scraper, nous obtenons la version non modifiée de la page.
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 table_selector_string = ".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>
Maintenant, donnons-lui une autre tournure :
{
"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"
},
...
]
]
C'est mieux !
Résumé
J'espère que cet article constitue un bon point de référence pour le web scraping avec Rust. Même si le riche système de types et le modèle de propriété de Rust peuvent être un peu écrasants, ils ne sont en aucun cas inadaptés au web scraping. Vous bénéficiez d'un compilateur convivial qui vous oriente constamment dans la bonne direction. Vous trouverez également une documentation abondante et bien rédigée : Le langage de programmation Rust - Le langage de programmation Rust (rust-lang.org).
La création d'un scraper web n'est pas toujours un processus simple. Vous serez confronté au rendu Javascript, aux blocages IP, aux captchas et à bien d'autres problèmes. Chez WebScraping API, nous vous fournissons tous les outils nécessaires pour lutter contre ces problèmes courants. Êtes-vous curieux de découvrir comment cela fonctionne ? Vous pouvez essayer notre produit gratuitement à l'adresse WebScrapingAPI - Product. Vous pouvez également nous contacter à l'adresse WebScrapingAPI - Contact. Nous nous ferons un plaisir de répondre à toutes vos questions !
Nouvelles et mises à jour
Restez au courant des derniers guides et nouvelles sur le web scraping en vous inscrivant à notre lettre d'information.
We care about the protection of your data. Read our <l>Privacy Policy</l>.Privacy Policy.

Articles connexes

Explorez la comparaison approfondie entre Scrapy et Selenium pour le web scraping. De l'acquisition de données à grande échelle à la gestion de contenus dynamiques, découvrez les avantages, les inconvénients et les caractéristiques uniques de chacun. Apprenez à choisir le meilleur framework en fonction des besoins et de l'échelle de votre projet.


Apprenez à récupérer des sites web dynamiques en JavaScript à l'aide de Scrapy et de Splash. De l'installation à l'écriture d'un spider, en passant par la gestion de la pagination et des réponses Splash, ce guide complet propose des instructions pas à pas pour les débutants comme pour les experts.


Explorez le pouvoir de transformation du web scraping dans le secteur financier. Des données sur les produits à l'analyse des sentiments, ce guide donne un aperçu des différents types de données web disponibles pour les décisions d'investissement.
