Zurück zum Blog
Leitfäden
Mihai Maxim17. Oktober 202210 Min. Lesezeit

Der anfängerfreundliche Leitfaden für Web Scraping mit Rust

Der anfängerfreundliche Leitfaden für Web Scraping mit Rust

Eignet sich Rust gut für Web Scraping?

Rust ist eine Programmiersprache, die auf Geschwindigkeit und Effizienz ausgelegt ist. Im Gegensatz zu C oder C++ verfügt Rust über einen integrierten Paketmanager und ein Build-Tool. Außerdem verfügt es über eine ausgezeichnete Dokumentation und einen freundlichen Compiler mit hilfreichen Fehlermeldungen. Es dauert eine Weile, bis man sich an die Syntax gewöhnt hat. Aber wenn man sich daran gewöhnt hat, wird man feststellen, dass man komplexe Funktionen mit nur wenigen Zeilen Code schreiben kann. Web Scraping mit Rust ist eine beeindruckende Erfahrung. Sie erhalten Zugang zu leistungsstarken Scraping-Bibliotheken, die Ihnen den Großteil der Arbeit abnehmen. Das Ergebnis ist, dass Sie mehr Zeit für die Dinge haben, die Ihnen Spaß machen, wie das Entwerfen neuer Funktionen. In diesem Artikel führe ich Sie durch den Prozess der Erstellung eines Web-Scrapers mit Rust. 

Wie wird Rust installiert?

Die Installation von Rust ist ein ziemlich unkomplizierter Prozess. Besuchen Sie Install Rust - Rust Programming Language (rust-lang.org) und folgen Sie der empfohlenen Anleitung für Ihr Betriebssystem. Die Seite zeigt je nach verwendetem Betriebssystem unterschiedliche Inhalte an. Am Ende der Installation sollten Sie ein neues Terminal öffnen und rustc --version ausführen. Wenn alles richtig gelaufen ist, sollten Sie die Versionsnummer des installierten Rust-Compilers sehen.

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>.

Dies ist die Standard-Projektstruktur:

  •   Sie schreiben Code in .rs-Dateien.
  •   Sie verwalten die Abhängigkeiten in der Datei Cargo.toml.
  •   Besuchen Sie crates.io: Rust Package Registry, um Pakete für Rust zu finden.

Bau eines Web Scrapers mit Rust

Schauen wir uns nun an, wie man mit Rust einen Scraper bauen kann. Der erste Schritt besteht darin, einen klaren Zweck zu definieren. Was möchte ich extrahieren? Der nächste Schritt ist die Entscheidung, wie Sie die gesammelten Daten speichern wollen. Die meisten Leute speichern sie als .json, aber Sie sollten generell das Format wählen, das Ihren individuellen Bedürfnissen am besten entspricht. Wenn diese beiden Anforderungen geklärt sind, können Sie getrost mit der Implementierung eines Scrapers fortfahren. Zur besseren Veranschaulichung dieses Prozesses schlage ich vor, ein kleines Tool zu entwickeln, das Covid-Daten von der Website COVID Live - Coronavirus Statistics - Worldometer (worldometers.info) extrahiert. Es sollte die Tabellen der gemeldeten Fälle analysieren und die Daten als .json speichern. Wir werden diesen Scraper in den folgenden Kapiteln gemeinsam erstellen.

Abruf von HTML mit HTTP-Anfragen

Um die Tabellen zu extrahieren, müssen Sie zunächst den HTML-Code der Webseite abrufen. Wir werden die "reqwest"-Kiste/Bibliothek verwenden, um rohes HTML von der Website zu holen.

Fügen Sie es zunächst als Abhängigkeit in der Datei Cargo.toml hinzu:

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

Geben Sie dann Ihre Ziel-URL an und senden Sie Ihre Anfrage:

let url = "https://www.worldometers.info/coronavirus/";
let response = reqwest::blocking::get(url).expect("Die URL konnte nicht geladen werden.");

Die Funktion "Blockieren" sorgt dafür, dass die Anforderung synchron ist. Infolgedessen wartet das Programm, bis die Anfrage abgeschlossen ist, und fährt dann mit den anderen Anweisungen fort. 

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

Verwendung von CSS-Selektoren zum Auffinden von Daten

Sie haben alle erforderlichen Rohdaten. Jetzt müssen Sie einen Weg finden, um die Tabellen mit den gemeldeten Fällen zu finden. Die beliebteste Rust-Bibliothek für diese Art von Aufgabe heißt "Scraper". Sie ermöglicht HTML-Parsing und Abfragen mit CSS-Selektoren.

Fügen Sie diese Abhängigkeit zu Ihrer Cargo.toml-Datei hinzu:

scraper = "0.13.0"

Fügen Sie diese Module zu Ihrer Datei main.rs hinzu.

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

Verwenden Sie nun die rohe HTML-Zeichenkette, um ein HTML-Fragment zu erstellen:

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

Wir wählen die Tabellen aus, die die gemeldeten Fälle von heute, gestern und vor zwei Tagen anzeigen.

Länderstatistik-Tabelle mit COVID-19-Fällen und Todesfällen, mit Filtern für den Zeitraum

Öffnen Sie die Entwicklerkonsole und identifizieren Sie die Tabellen-IDs:

HTML-Ausschnitt, der die Tabellen-ID hervorhebt, die für eine Tabelle mit Länderstatistiken verwendet wird

Zum Zeitpunkt der Erstellung dieses Artikels lautet die ID für den heutigen Tag: "main_table_countries_today".

Die beiden anderen Tabellen-IDs lauten: „main_table_countries_yesterday“ und „main_table_countries_yesterday2“

Lassen Sie uns nun einige Selektoren definieren:

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();

Übergeben Sie den table_selector_string an die Methode html_fragment select, um die Verweise auf alle Tabellen zu erhalten:

let all_tables = html_fragment.select(&table_selector);

Erstellen Sie unter Verwendung der Tabellenreferenzen eine Schleife, die die Daten aus jeder Tabelle analysiert.

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
    }
   }
}

Parsing der Daten

Das Format, in dem Sie die Daten speichern, bestimmt die Art und Weise, wie Sie die Daten analysieren. Für dieses Projekt ist es .json. Folglich müssen wir die Tabellendaten in Schlüssel-Wert-Paaren speichern. Wir können die Namen der Tabellenüberschriften als Schlüssel und die Tabellenzeilen als Werte verwenden. 

Verwenden Sie die Funktion .text(), um die Kopfzeilen zu extrahieren und in einem Vektor zu speichern:

//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", ...]

Extrahieren Sie die Zeilenwerte auf ähnliche Art und Weise:

//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", ...]
...]

Verwenden Sie die Funktion zip(), um eine Übereinstimmung zwischen Kopf- und Zeilenwerten herzustellen:

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"), ...]
...
]

Speichern Sie nun die zipped_array-Paare (Schlüssel, Wert) in einer IndexMap:

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

indexmap = {version="1.9.1", features = ["serde"]}  (füge diese Abhängigkeiten hinzu)
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 ist eine gute Wahl für die Speicherung der Tabellendaten, da die Einfügereihenfolge der (Schlüssel-Wert)-Paare erhalten bleibt.

Serialisierung der Daten

Nun, da Sie json-ähnliche Objekte mit Tabellendaten erstellen können, ist es an der Zeit, diese in .json zu serialisieren. Bevor wir beginnen, stellen Sie sicher, dass Sie alle diese Abhängigkeiten installiert haben:

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

Speichern Sie jede Tabelle_data in einem tables_data-Vektor:

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);

Definieren Sie einen struct-Container für die tables_data:

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

Instanziieren Sie die Struktur:

 let final_table_object = FinalTableObject{tables: tables_data};

Serialisieren Sie die Struktur in einen .json-String:

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

Schreiben Sie die serialisierte .json-Zeichenkette in eine .json-Datei:

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),

    }

Aaaaalso, Sie sind fertig. Wenn alles richtig gelaufen ist, sollte Ihre .json-Ausgabe wie folgt aussehen:

{
  "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)

Anpassung an andere Anwendungsfälle

Wenn Sie mir bis hierher gefolgt sind, haben Sie wahrscheinlich erkannt, dass Sie diesen Scraper auch auf anderen Websites verwenden können. Der Scraper ist nicht an eine bestimmte Anzahl von Tabellenspalten oder Namenskonventionen gebunden. Außerdem ist er nicht auf viele CSS-Selektoren angewiesen. Es sollte also nicht viel Aufwand nötig sein, damit er auch für andere Tabellen funktioniert, oder? Lassen Sie uns diese Theorie testen.

Wikipedia-Tabelle, in der Länder nach Einwohnerzahl aufgelistet sind, mit Spalten für Flaggen und Bevölkerungsdichte

We need a selector for the <table> tag.

HTML-Ausschnitt, der ein sortierbares Tabellenelement aus Wikipedia hervorhebt

Wenn class="wikitable sortable jquery-tablesorter", könnten Sie den table_selector zu ändern:

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.

Der Scraper sollte jetzt funktionieren. Lassen Sie uns einen Testlauf machen:

{
  "tables": []
}

Webscraping mit Rust macht Spaß, nicht wahr? 

Wie kann das scheitern? 

Lassen Sie uns etwas tiefer graben:

Der einfachste Weg, um herauszufinden, was schief gelaufen ist, ist ein Blick auf den HTML-Code, der von der GET-Anforderung zurückgegeben wird:

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


let response = reqwest::blocking::get(url).expect("Die URL konnte nicht geladen werden.");

let 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());
Code-Editor, der den HTML-Quellcode einer Wikipedia-Bevölkerungstabelle mit Spalten und Flaggensymbolen anzeigt

Der von der GET-Anfrage zurückgegebene HTML-Code unterscheidet sich von dem, den wir auf der eigentlichen Website sehen. Der Browser bietet eine Umgebung, in der Javascript ausgeführt werden kann, um das Layout der Seite zu verändern. Im Kontext unseres Scrapers erhalten wir die unveränderte Version der Seite.

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>

Jetzt wollen wir es noch einmal probieren:

{
  "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"
      },
      ...
     ]
]

So ist es besser!

Zusammenfassung

Ich hoffe, dass dieser Artikel einen guten Bezugspunkt für Web Scraping mit Rust darstellt. Auch wenn das umfangreiche Typsystem und das Eigentumsmodell von Rust ein wenig überwältigend sein können, ist es keineswegs ungeeignet für Web Scraping. Sie erhalten einen freundlichen Compiler, der Sie ständig in die richtige Richtung weist. Sie finden auch eine Menge gut geschriebener Dokumentation: Die Rust-Programmiersprache - Die Rust-Programmiersprache (rust-lang.org).

Die Erstellung eines Web Scrapers ist nicht immer ein einfacher Prozess. Sie werden mit Javascript-Rendering, IP-Blöcken, Captchas und vielen anderen Rückschlägen konfrontiert. Bei WebScraping API stellen wir Ihnen alle notwendigen Tools zur Verfügung, um diese allgemeinen Probleme zu bekämpfen. Sind Sie neugierig, wie es funktioniert? Sie können unser Produkt kostenlos unter WebScrapingAPI - Produkt testen. Oder Sie können uns unter WebScrapingAPI - Kontakt kontaktieren. Wir beantworten gerne alle Ihre Fragen!

Über den Autor
Mihai Maxim, Full-Stack-Entwickler bei WebScrapingAPI
Mihai MaximFull-Stack-Entwickler

Mihai Maxim ist Full-Stack-Entwickler bei WebScrapingAPI, wo er in allen Bereichen des Produkts mitwirkt und an der Entwicklung zuverlässiger Tools und Funktionen für die Plattform mitwirkt.

Los geht’s

Sind Sie bereit, Ihre Datenerfassung zu erweitern?

Schließen Sie sich den über 2.000 Unternehmen an, die WebScrapingAPI nutzen, um Webdaten im Unternehmensmaßstab ohne zusätzlichen Infrastrukturaufwand zu extrahieren.