Zurück zum Blog
Anleitungen
Mihai MaximLast updated on Mar 31, 20267 min read

Der Einsteigerfreundliche Leitfaden zum Web-Scraping mit Rust

Der Einsteigerfreundliche Leitfaden zum Web-Scraping mit Rust

Eignet sich Rust gut für das 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 bietet es eine hervorragende Dokumentation und einen benutzerfreundlichen Compiler mit hilfreichen Fehlermeldungen. Es dauert zwar eine Weile, sich an die Syntax zu gewöhnen. Aber sobald 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 bereichernde Erfahrung. Man erhält Zugriff auf leistungsstarke Scraping-Bibliotheken, die einem den Großteil der Arbeit abnehmen. Dadurch kann man mehr Zeit mit den spannenden Aspekten verbringen, wie zum Beispiel dem Entwerfen neuer Funktionen. In diesem Artikel werde ich Sie durch den Prozess der Erstellung eines Web-Scrapers mit Rust führen. 

So installieren Sie Rust

Die Installation von Rust ist ein ziemlich unkomplizierter Vorgang. Besuche Install Rust – Rust Programming Language (rust-lang.org) und folge dem für dein Betriebssystem empfohlenen Tutorial. Die Seite zeigt je nach verwendetem Betriebssystem unterschiedliche Inhalte an. Stelle am Ende der Installation sicher, dass du ein brandneues Terminal öffnest und „rustc --version“ ausführst. Wenn alles geklappt hat, solltest du die Versionsnummer des installierten Rust-Compilers sehen.

Da wir einen Web-Scraper erstellen werden, erstellen wir ein Rust-Projekt mit Cargo. Cargo ist das Build-System und der Paketmanager von Rust. Wenn Sie die offiziellen Installationsprogramme von rust-lang.org verwendet haben, sollte Cargo bereits installiert sein. Überprüfen Sie, ob Cargo installiert ist, indem Sie Folgendes in Ihr Terminal eingeben: cargo --version. Wenn Sie eine Versionsnummer sehen, ist es installiert! Wenn eine Fehlermeldung wie „Befehl nicht gefunden“ angezeigt wird, schau in der Dokumentation zu deiner Installationsmethode nach, wie du Cargo separat installieren kannst. Um ein Projekt zu erstellen, wechsle in das gewünschte Projektverzeichnis und führe cargo new <Projektname> aus.

Dies ist die Standard-Projektstruktur:

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

Erstellen eines Web-Scrapers mit Rust

Schauen wir uns nun an, wie Sie mit Rust einen Scraper erstellen können. Der erste Schritt besteht darin, einen klaren Zweck zu definieren. Was möchte ich extrahieren? Als Nächstes müssen Sie entscheiden, wie Sie die gescrapten Daten speichern möchten. Die meisten speichern sie als .json, aber Sie sollten generell das Format wählen, das Ihren individuellen Anforderungen am besten entspricht. Wenn diese beiden Anforderungen geklärt sind, können Sie getrost mit der Implementierung eines beliebigen Scrapers fortfahren. Um diesen Prozess besser zu veranschaulichen, schlage ich vor, dass wir ein kleines Tool erstellen, das Covid-Daten von der Website „COVID Live – Coronavirus Statistics – Worldometer“ (worldometers.info) extrahiert. Es soll die Tabellen mit den gemeldeten Fällen auswerten und die Daten als .json speichern. Wir werden diesen Scraper in den folgenden Kapiteln gemeinsam erstellen.

HTML mit HTTP-Anfragen abrufen

Um die Tabellen zu extrahieren, müssen Sie zunächst den HTML-Code der Webseite abrufen. Wir werden die „reqwest“-Crate/Bibliothek verwenden, um den Roh-HTML-Code von der Website abzurufen.

Füge sie zunächst als Abhängigkeit in die Datei „Cargo.toml“ ein:

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

Definieren Sie dann Ihre Ziel-URL und senden Sie Ihre Anfrage:

let url = "https://www.worldometers.info/coronavirus/";
let response = reqwest::blocking::get(url).expect("Could not load url.");

Die „blocking“-Funktion stellt sicher, dass die Anfrage synchron ausgeführt wird. Das Programm wartet daher, bis sie abgeschlossen ist, und fährt dann mit den weiteren Anweisungen fort. 

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

Daten mit CSS-Selektoren finden

Sie haben alle erforderlichen Rohdaten erhalten. Nun müssen Sie einen Weg finden, die Tabellen mit den gemeldeten Fällen zu lokalisieren. Die beliebteste Rust-Bibliothek für diese Art von Aufgabe heißt „scraper“. Sie ermöglicht das Parsen von HTML und das Abfragen mit CSS-Selektoren.

Füge diese Abhängigkeit zu deiner Cargo.toml-Datei hinzu:

scraper = "0.13.0"

Füge diese Module zu deiner main.rs-Datei hinzu.

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

Verwenden Sie nun die HTML-Rohzeichenfolge, 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 für heute, gestern und vor zwei Tagen anzeigen.

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

Zum Zeitpunkt der Erstellung dieses Artikels lautet die ID für heute: „main_table_countries_today“.

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

Definieren wir nun einige Selektoren:

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 select-Methode von html_fragment, um die Referenzen aller Tabellen zu erhalten:

let all_tables = html_fragment.select(&table_selector);

Erstellen Sie mithilfe der Tabellenreferenzen eine Schleife, die die Daten aus jeder Tabelle auswertet.

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

Auswerten der Daten

Das Format, in dem Sie die Daten speichern, bestimmt die Art und Weise, wie Sie sie auswerten. Bei diesem Projekt ist es .json. Folglich müssen wir die Tabellendaten in Schlüssel-Wert-Paare umwandeln. Wir können die Namen der Tabellenköpfe 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 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 Zuordnung zwischen Spaltenüberschriften und Zeilenwerten zu erstellen:

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 (Schlüssel, Wert)-Paare des zipped_array in einem 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 eignet sich hervorragend zum Speichern der Tabellendaten, da die Einfügungsreihenfolge der (Schlüssel, Wert)-Paare beibehalten wird.

Serialisieren der Daten

Nachdem Sie nun 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 alle folgenden Abhängigkeiten installiert sind:

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

Speichern Sie jede table_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 Struktur-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};

Serialisiere die Struktur in eine .json-Zeichenkette:

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

    }

Und schon sind Sie fertig. Wenn alles geklappt hat, 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...],
  ]
}

Den gesamten Code für das Projekt finden Sie unter [Rust][Ein einfacher <table>-Scraper] (github.com)

Anpassungen für andere Anwendungsfälle

Wenn du mir bis hierher gefolgt bist, hast du wahrscheinlich erkannt, dass du diesen Scraper auch auf anderen Websites verwenden kannst. Der Scraper ist nicht an eine bestimmte Anzahl von Tabellenspalten oder eine bestimmte Namenskonvention gebunden. Außerdem ist er nicht auf viele CSS-Selektoren angewiesen. Es sollte also nicht viel Feinarbeit erfordern, ihn für andere Tabellen zum Laufen zu bringen, oder? Lass uns diese Theorie testen.

Wir benötigen einen Selektor für das <table>-Tag.

Wenn class="wikitable sortable jquery-tablesorter" ist, könntest du den table_selector wie folgt ändern:

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

Diese Tabelle hat dieselbe Struktur <thead> <tbody>, daher gibt es keinen Grund, die anderen Selektoren zu ändern.

Der Scraper sollte nun funktionieren. Probieren wir es aus:

{
  "tables": []
}

Webscraping mit Rust macht Spaß, nicht wahr? 

Wie könnte das schiefgehen? 

Schauen wir etwas genauer hin:

Der einfachste Weg, herauszufinden, was schiefgelaufen ist, besteht darin, sich den HTML-Code anzusehen, der von der GET-Anfrage zurückgegeben wird:

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

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 und das Layout der Seite verändern kann. Im Kontext unseres Scrapers erhalten wir die unveränderte Version davon.

Unser table_selector hat nicht funktioniert, weil die Klasse „jquery-tablesorter“ dynamisch durch JavaScript eingefügt wird. Außerdem ist zu erkennen, dass die Struktur von <table> anders ist. Das <thead>-Tag fehlt. Die Tabellenkopf-Elemente befinden sich nun im ersten <tr> des <tbody>. Daher werden sie vom row_elements_selector erfasst.

Das Entfernen von „jquery-tablesorter“ aus dem table_selector reicht nicht aus, wir müssen auch den Fall des fehlenden <tbody> behandeln:

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>

Versuchen wir es nun noch einmal:

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

Das ist besser!

Zusammenfassung

Ich hoffe, dass dieser Artikel eine gute Anlaufstelle für das Web-Scraping mit Rust darstellt. Auch wenn Rusts umfangreiches Typsystem und sein Eigentumsmodell etwas überwältigend sein können, ist es keineswegs ungeeignet für das Web-Scraping. Man erhält einen benutzerfreundlichen Compiler, der einen ständig in die richtige Richtung weist. Außerdem findet man eine Menge gut geschriebener Dokumentation: The Rust Programming Language – The Rust Programming Language (rust-lang.org).

Das Erstellen eines Web-Scrapers ist nicht immer ein einfacher Prozess. Sie werden mit JavaScript-Rendering, IP-Sperren, Captchas und vielen anderen Hindernissen konfrontiert. Bei WebScraping API stellen wir Ihnen alle notwendigen Tools zur Verfügung, um diese häufigen Probleme zu bewältigen. Sind Sie neugierig, wie das funktioniert? Sie können unser Produkt kostenlos unter WebScrapingAPI – Produkt ausprobieren. Oder Sie kontaktieren uns unter WebScrapingAPI – Kontakt. Wir beantworten gerne alle Ihre Fragen!

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

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

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.