Rust est un langage de programmation conçu pour la vitesse et l'efficacité. Contrairement au C ou au C++, Rust dispose d'un gestionnaire de paquets et d'un outil de compilation intégrés. Il offre également une excellente documentation et 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 habitué, vous vous rendrez compte que vous pouvez écrire des fonctionnalités complexes en quelques lignes de code seulement. Le web scraping avec Rust est une expérience enrichissante. Vous avez accès à de puissantes bibliothèques de scraping qui se chargent de la plupart des tâches fastidieuses à votre place. Vous pouvez ainsi consacrer plus de temps aux aspects ludiques, comme la conception de nouvelles fonctionnalités. Dans cet article, je vais vous guider à travers le processus de création d'un scraper web avec Rust.
Rust est-il adapté au web scraping ?
Comment installer Rust
L'installation de Rust est un processus assez simple. Rendez-vous sur Install Rust - Rust Programming Language (rust-lang.org) et suivez le tutoriel recommandé pour votre système d'exploitation. La page affiche un contenu différent en fonction du système d'exploitation que vous utilisez. À la fin de l'installation, assurez-vous d'ouvrir un tout nouveau terminal et d'exécuter rustc --version. Si tout s'est bien passé, vous devriez voir le numéro de version du compilateur Rust installé.
Puisque nous allons créer un scraper web, créons un projet Rust avec Cargo. Cargo est le système de compilation et le gestionnaire de paquets de Rust. Si vous avez utilisé les installateurs officiels fournis par rust-lang.org, Cargo devrait déjà être installé. Vérifiez si Cargo est installé en saisissant la commande suivante dans votre terminal : cargo --version. Si vous voyez un numéro de version, c'est qu'il est bien installé ! Si vous obtenez une erreur, telle que « commande introuvable », consultez la documentation relative à votre méthode d'installation pour savoir comment installer Cargo séparément. Pour créer un projet, accédez à l'emplacement souhaité et exécutez cargo new <nom du projet>.
Voici la structure de projet par défaut :
- Vous écrivez le code dans des fichiers .rs.
- Vous gérez les dépendances dans le fichier Cargo.toml.
- Rendez-vous sur crates.io : registre des paquets Rust pour trouver des paquets pour Rust.
Créer un scraper web avec Rust
Voyons maintenant comment vous pourriez utiliser Rust pour créer un scraper. La première étape consiste à définir un objectif clair. Que souhaite-je extraire ? L'étape suivante consiste à décider comment vous souhaitez stocker les données extraites. La plupart des gens les enregistrent au format .json, mais vous devriez généralement choisir le format qui correspond le mieux à vos besoins individuels. Une fois ces deux exigences définies, vous pouvez vous lancer en toute confiance dans la mise en œuvre de n'importe quel scraper. Pour mieux illustrer ce processus, je propose que nous créions un petit outil qui extrait les données sur le 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 allons créer ce scraper ensemble dans les chapitres suivants.
Récupération du code HTML à l'aide de requêtes HTTP
Pour extraire les tableaux, vous devez d'abord récupérer le code HTML contenu dans la page web. Nous utiliserons la bibliothèque « reqwest » pour récupérer le code HTML brut du site web.
Commencez par l'ajouter en tant que dépendance dans le fichier Cargo.toml :
reqwest = { version = "0.11", features = ["blocking", "json"] }
Définissez ensuite votre URL cible et envoyez votre requête :
let url = "https://www.worldometers.info/coronavirus/";let response = reqwest::blocking::get(url).expect("Could not load url.");
La fonctionnalité « blocking » garantit que la requête est synchrone. Par conséquent, le programme attendra qu’elle soit terminée avant de passer aux instructions suivantes.
let raw_html_string = response.text().unwrap();
Utilisation de sélecteurs CSS pour localiser les données
Vous disposez désormais de toutes les données brutes nécessaires. Il vous faut 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 syntaxique du code HTML et l’interrogation à l’aide de 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 allons sélectionner les tableaux qui affichent les cas signalés pour aujourd'hui, hier et avant-hier.
Ouvrez la console de développement et identifiez les identifiants des tableaux :
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();Transmettez la chaîne table_selector_string à la méthode select de html_fragment pour obtenir les références de toutes les tables :
let all_tables = html_fragment.select(&table_selector);
À l'aide des références des tables, créez une boucle qui analyse les données de chaque table.
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 détermine la manière dont vous les analysez. Pour ce projet, il s’agit du format .json. Par conséquent, nous devons organiser les données de la table en paires clé-valeur. Nous pouvons utiliser les noms des en-têtes de la 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 en-têtes et les valeurs des lignes :
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"), ...]
...
]Enregistrez maintenant les paires (clé, valeur) du zipped_array dans un IndexMap :
serde = {version="1.0.0",features = ["derive"]}indexmap = {version="1.9.1", features = ["serde"]} (add these dependencies)
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 du tableau, 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 à partir des données du tableau, il est temps de les sérialiser au format .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"]}Stockez 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éfinissez un conteneur struct pour les tables_data :
#[derive(Serialize)]
struct FinalTableObject {
tables: IndexMap<String, Vec<IndexMap<String, String>>>,
}Instanciez la structure :
let final_table_object = FinalTableObject{tables: tables_data};
Sérialisez la structure en une chaîne .json :
let serialized = serde_json::to_string_pretty(&final_table_object).unwrap();
Écrivez 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à, c'est 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...],
]
}Vous pouvez trouver le code complet du projet sur [Rust][Un simple scraper <table>] (github.com)
Ajuster le code pour d'autres cas d'utilisation
Si vous m'avez suivi jusqu'ici, vous avez probablement compris que vous pouvez utiliser ce scraper sur d'autres sites web. Le scraper n'est pas lié à un nombre spécifique de colonnes de tableau ni à une convention de nommage particulière. De plus, il ne repose pas sur de nombreux sélecteurs CSS. Il ne devrait donc pas falloir beaucoup de modifications pour le faire fonctionner avec d'autres tableaux, n'est-ce pas ? Testons cette théorie.
Nous avons besoin d'un sélecteur pour la balise <table>.
Si class="wikitable sortable jquery-tablesorter", vous pouvez modifier le table_selector comme suit :
let table_selector_string = ".wikitable.sortable.jquery-tablesorter";let table_selector = Selector::parse(table_selector_string).unwrap();
Ce tableau a la même structure <thead> <tbody>, il n'y a donc aucune raison de modifier les autres sélecteurs.
Le scraper devrait maintenant fonctionner. Faisons un essai :
{
"tables": []
}Le webscraping avec Rust, c'est sympa, n'est-ce pas ?
Comment cela pourrait-il échouer ?
Creusons un peu plus :
La manière la plus simple de comprendre 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 l'on voit sur le site web réel. Le navigateur offre un environnement permettant à Javascript de s'exécuter et de modifier la mise en page. Dans le contexte de notre scraper, nous obtenons la version non modifiée de celle-ci.
Notre table_selector n'a pas fonctionné car la classe « jquery-tablesorter » est injectée dynamiquement par JavaScript. De plus, vous pouvez constater que la structure <table> est différente. La balise <thead> est manquante. Les éléments d'en-tête du tableau se trouvent désormais dans le premier <tr> de la balise <tbody>. Ils seront donc récupérés par le row_elements_selector.
Supprimer « jquery-tablesorter » du table_selector ne suffit pas, nous devons également gérer le cas où <tbody> est manquant :
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>Essayons à nouveau :
{
"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 constituera une bonne référence pour le web scraping avec Rust. Même si le système de types riche et le modèle de propriété de Rust peuvent sembler un peu intimidants, ils ne sont en aucun cas inadaptés au web scraping. Vous bénéficiez d'un compilateur convivial qui vous guide constamment dans la bonne direction. Vous trouverez également une documentation très bien écrite : The Rust Programming Language - The Rust Programming Language (rust-lang.org).
Construire un scraper web n'est pas toujours un processus simple. Vous serez confronté au rendu Javascript, aux blocages d'IP, aux captchas et à de nombreux autres obstacles. Chez WebScraping API, nous vous fournissons tous les outils nécessaires pour surmonter ces problèmes courants. Vous souhaitez découvrir comment cela fonctionne ? Vous pouvez essayer notre produit gratuitement sur WebScrapingAPI - Produit. Vous pouvez également nous contacter sur WebScrapingAPI - Contact. Nous serons ravis de répondre à toutes vos questions !




