Web Scraping With Rust: reqwest, scraper, and Tokio

Web Scraping With Rust

Scrapeless Universal Scraping API delivers fetched or rendered HTML to Rust clients through a conventional authenticated HTTP request.

TL;DR

  • Rust makes error paths part of the program shape. HTTP failure, missing fields, invalid selectors, and output conversion can be represented as explicit results instead of silent empty values.
  • reqwest handles transport and scraper handles HTML. The crates have separate jobs, which keeps acquisition replaceable when a page requires rendering.
  • Tokio supports bounded concurrent requests. Shared clients and controlled task sets improve throughput without turning discovery into unlimited fan-out.
  • Selectors should compile once. Parse CSS selectors before the record loop and return a startup error when a selector is invalid.
  • Zero matches require diagnosis. A client-rendered shell, wrong page identity, or changed markup can all produce valid HTML with no records.

Where Rust Adds Value

Web scraping with Rust fits unattended collectors where predictable memory use, explicit failures, and controlled concurrency matter more than quick interactive experimentation. Rust does not make selectors more accurate by itself. It makes the boundaries around network, parsing, and storage harder to ignore.

The common stack is reqwest for HTTP, scraper for HTML parsing and CSS selection, and Tokio for the asynchronous runtime. The reqwest documentation recommends reusing a client when making multiple requests so the program benefits from connection pooling. That maps naturally to a crawler service with one configured client shared across tasks.

Keep the parser independent from transport. A function that accepts &str and returns typed records can be tested against saved HTML. The network function can then return server HTML, a rendered document, or a fixture without changing extraction logic.

Map the Rust Scraping Stack

ConcernRust choiceDesign note
HTTPreqwest clientReuse the client and check status before reading data
HTMLscraper crateParse a document tree and query with CSS selectors
Async runtimeTokioDrive bounded network tasks
RecordsStructs plus serdeMake required and optional fields visible
ValidationResult and custom errorsReject wrong pages and implausible match counts

The parser crate builds on an HTML parsing model rather than treating markup as arbitrary text. The HTML parsing specification explains why a document tree can differ from the literal tag sequence: parsers repair malformed nesting and infer elements under defined rules.

Create a Typed Rust Extractor

This code is a local-runtime prerequisite because Rust is not installed in the current workspace. The structure follows the current reqwest and scraper APIs: fetch, convert unexpected status into an error, read text, parse the document, then select fields. Compile it in a Cargo project with reqwest, scraper, Tokio, serde, and serde_json.

use scraper::{Html, Selector};
use serde::Serialize;

#[derive(Serialize)]
struct Record {
    title: String,
    href: Option<String>,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(20))
        .build()?;

    let html = client
        .get("https://example.com/")
        .send()
        .await?
        .error_for_status()?
        .text()
        .await?;

    let document = Html::parse_document(&html);
    let title_selector = Selector::parse("h1")?;
    let link_selector = Selector::parse("a")?;

    let title = document
        .select(&title_selector)
        .next()
        .map(|node| node.text().collect::<String>())
        .ok_or("missing page heading")?;

    let href = document
        .select(&link_selector)
        .next()
        .and_then(|node| node.value().attr("href"))
        .map(str::to_owned);

    println!("{}", serde_json::to_string_pretty(&Record { title, href })?);
    Ok(())
}

The selectors are parsed once, before extraction. A missing heading becomes an error because it is the page-identity field in this example. The link is optional and therefore uses Option<String>. That distinction lets the type system carry part of the data contract.

Represent Page Failure Explicitly

A Rust scraper should distinguish transport failure, unsuccessful HTTP status, unexpected content type, wrong page identity, and selector mismatch. Collapsing those states into an empty vector removes the information needed to repair the pipeline. A custom error enum can keep those categories machine-readable while preserving the original error as context.

The HTTP semantics specification defines response status classes, yet a successful status does not guarantee that the expected business document arrived. Confirm the final URL and a structural marker before parsing repeated rows. Record accepted and rejected counts separately.

  • Compile selector strings during startup. Invalid syntax should prevent the worker from accepting jobs.
  • Treat identity fields as required. A record without its stable source key should not reach storage.
  • Preserve optional values as options. An absent price, author, or timestamp should remain distinct from an empty string.
  • Cap document size deliberately. Large responses need an acquisition limit tied to the expected page class.

Control Tokio Concurrency

Tokio makes it possible to overlap network waits, but a scraping job still needs a fixed budget. A semaphore, buffered stream, or worker queue can cap active requests per host. The client should be cloned cheaply while sharing its internal pool; the task set should remain bounded by design.

Each task returns a structured result containing the source URL and either records or a categorized failure. Collecting that result through one coordinator keeps storage writes and metrics ordered. It also prevents a detached task from failing outside the accounting path.

Keep discovery bounded. A crawler that follows every link without a scope rule can leave the intended site, revisit alternate URL forms, or grow without a stopping condition. Canonicalize allowed URLs, limit path patterns, and store visited identities.

Detect the JavaScript Boundary

reqwest downloads server responses; it does not execute page scripts. The scraper crate parses the body it receives. If a browser displays records that do not appear in page source, the Rust code can succeed at both jobs and still return zero matches. That outcome is a capability mismatch, not necessarily a selector bug.

Rendered acquisition solves the mismatch by returning the post-script document to the same parser. Keep the split visible: one function obtains faithful HTML, and another turns HTML into typed records. This design prevents browser concerns from spreading through normalization and storage code.

Operate Within Source Rules

Before scheduling a Rust crawler, define the allowed hosts, paths, data classes, and request budget. Review terms and applicable law. The Robots Exclusion Protocol provides standardized crawler directives, but it is not a substitute for permission, privacy analysis, or contractual review.

Observability should focus on correctness: response class, page identity, parse duration, container count, accepted records, and categorized failures. Avoid storing entire response bodies in ordinary logs. Fixtures belong in controlled test data, and sensitive values belong outside the crawler’s diagnostic surface.

Keep Provenance Beside the Typed Record

Typed output does not remove the need for provenance. Store the canonical source URL, acquisition time, parser revision, and a compact page-identity result beside each record or batch. Those fields let downstream systems distinguish a genuine value change from a selector change or a different regional page.

Keep the raw text available when normalization can lose meaning. Currency strings, localized numbers, availability labels, and human dates often need source-specific rules. A good Rust model carries both the normalized field and enough original context to audit the conversion. Validation can reject impossible combinations before serialization, such as a numeric amount with no currency when the application requires one.

Schema evolution should be deliberate. Add optional fields first, update consumers, and only then make a field required after the source and pipeline prove it is consistently present. This approach uses Rust’s type system as a data contract without pretending that third-party markup is stable.

Conclusion

Web scraping with Rust is a good fit when the collector must run for long periods with explicit error handling and a controlled resource budget. Reuse a reqwest client, parse selectors once, represent absence with options, and cap Tokio tasks. If client-side JavaScript owns the data, change how the HTML is acquired rather than rewriting the typed parser.

Ready to Connect Rust to Rendered Web Data?

Keep reqwest and scraper as the typed application boundary while Scrapeless handles acquisition for pages that need rendering.

Sign up today and get $5 in free creditno credit card required.

Claim Your $5 Credit →

FAQ

Which Rust crates are used for web scraping?

reqwest is a common HTTP client, scraper provides HTML parsing and CSS selectors, and Tokio runs asynchronous work. Serde is often added when the output or acquisition response is JSON.

Can reqwest execute JavaScript?

No. reqwest sends HTTP requests and returns server responses; it does not run a browser event loop. Use rendered acquisition when scripts create the required content.

Is async Rust required for a small scraper?

No. A blocking client can be suitable for a single sequential job. Async Rust becomes useful when the design has multiple independent network waits and a clear concurrency budget.

How should Rust handle missing scraped fields?

Rust should model required fields as values that must be present and optional fields as Option. Reject records that lack their identity field instead of substituting an ambiguous blank.

Is web scraping with Rust legal?

The language does not change the legal analysis. Limit work to authorized public data, review site terms and robots directives, respect access controls, and obtain legal advice where the collection affects people or regulated data.

References