Your first crawl
This chapter builds a small crawler for an online shop: it walks the product listing page by page, and extracts every product it links to.
Tags
Every request carries a tag that says what kind of page it is, and the
router picks the handler for it by that tag. Tags can be plain strings, but
an enum deriving Tags lets the compiler check them:
use spire::request::Tags;
/// The kinds of page the crawl visits.
#[derive(Debug, Clone, Copy, Tags)]
enum Page {
Listing,
Product,
}
What to extract
#[derive(Select)] turns CSS selectors into a struct. Here, a product is
the main.product element of its page, with a name and an optional price:
use spire::extract::html::Select;
#[derive(Debug, Select)]
#[select("main.product")]
struct Product {
#[select("h1")]
name: String,
#[select(".price")]
price: Option<f64>,
}
The crawl
use spire::backend::ReqwestBackend;
use spire::dataset::InMemDataset;
use spire::extract::html::Select;
use spire::frontier::seed::Seed;
use spire::frontier::{FrontierExt, InMemFrontier};
use spire::handler::{Collect, FollowLinks};
use spire::request::Tags;
use spire::{Crawler, Result, Router};
#[derive(Debug, Clone, Copy, Tags)]
enum Page {
Listing,
Product,
}
#[derive(Debug, Select)]
#[select("main.product")]
struct Product {
#[select("h1")]
name: String,
#[select(".price")]
price: Option<f64>,
}
#[tokio::main]
async fn main() -> Result<()> {
let router = Router::new()
// Two handlers on one listing page, fetched once: follow the next
// page and every product.
.route(Page::Listing, (
FollowLinks::new(Page::Listing).select("a.next"),
FollowLinks::new(Page::Product).select("article.product a"),
))
.route(Page::Product, Collect::<Product>::new())
.require::<Page>();
let crawler = Crawler::new(ReqwestBackend::default(), router)
.with_frontier(InMemFrontier::fifo().dedup().max_depth(5).retry(3).tag_backoff())
.with_dataset(InMemDataset::<Product>::new())
.with_concurrency(16)
.with_seed(Seed::get("https://example.com/products")?.with_tag(Page::Listing));
let stats = crawler.run().await?;
let products = crawler.dataset::<Product>()?.read_all().await?;
println!("{} products in {} requests", products.len(), stats.requests);
Ok(())
}
Step by step:
- The router. Listing pages run two built-in handlers in turn:
FollowLinksqueues the links matching a selector, with the tag to give them. Product pages runCollect, which extracts aProductand stores it.requirefails at startup if a variant ofPagehas no route. - The frontier holds the requests waiting to be crawled, in first-in,
first-out order. On top of it,
dedupskips URLs already seen,max_depthstops five links away from the seed,retrytries failed requests three more times, andtag_backoffpauses a tag when a handler backs off. - The dataset stores what
Collectextracts, in memory. - The seed is the first request: the listing, tagged as one.
runcrawls until nothing is queued or in flight, then returns statistics. The products are then read back from the dataset.
Relative links resolve against the page they were found on, after
redirects. Error statuses fail the request: 5xx and 429 are retried,
honouring Retry-After, and other 4xx are not.
The rest of this book goes through each of these pieces in turn.