Millipede
Examples

HTTP crawl

Fan out through a deduplicated 100-page mock-site queue with concurrent HTTP workers.

This crawler builds a 100-page local mock site whose links form a binary tree, then fetches it with eight-way concurrency and sessions enabled by default. The handler performs URLs-only extraction and passes raw URLs to EnqueueLinker; queue deduplication allows the graph to fan out while ensuring every page is crawled exactly once.

cargo run -p millipede --example http_crawl
millipede/examples/http_crawl.rs
//! Crawls a 100-page mock site with `HttpCrawler`, sessions enabled by default, and
//! `EnqueueLinker` in URLs-only mode. Queue-level dedup lets the binary tree fan out while each
//! page is crawled exactly once.

use std::sync::Arc;

use wiremock::{Mock, MockServer, ResponseTemplate, matchers::path};

fn extract_links(body: &str) -> Vec<String> {
    // Phase 3 extracts URLs only; DOM parsing arrives with `HtmlCrawler` in Phase 5.
    body.split("href=\"")
        .skip(1)
        .filter_map(|fragment| fragment.split_once('"').map(|(url, _)| url.to_owned()))
        .collect()
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let server = MockServer::start().await;
    let server_uri = server.uri();

    for i in 0..100 {
        let mut body = String::new();
        for child in [2 * i + 1, 2 * i + 2] {
            if child < 100 {
                body.push_str(&format!("href=\"{server_uri}/page/{child}\"\n"));
            }
        }
        Mock::given(path(format!("/page/{i}")))
            .respond_with(ResponseTemplate::new(200).set_body_string(body))
            .mount(&server)
            .await;
    }

    let kind = millipede::HttpKind::builder().build()?;
    let crawler = millipede::Crawler::builder(kind)
        .max_concurrency(8)
        .storage_client(Arc::new(millipede::MemoryStorageClient::new()))
        .request_handler(|ctx: millipede::HttpContext| async move {
            let body = ctx.response.text().into_owned();
            let links = extract_links(&body);
            if !links.is_empty() {
                let _ = ctx.enqueue.options().raw_urls(links).send().await?;
            }
            Ok(())
        })
        .failed_request_handler(|ctx: millipede::FailedRequestContext| async move {
            eprintln!("failed to crawl {}: {}", ctx.request.url, ctx.error);
            Ok(())
        })
        .build()
        .await?;

    let stats = crawler.run(format!("{server_uri}/page/0")).await?;
    println!(
        "requests_finished={} requests_failed={} requests_retries={}",
        stats.requests_finished, stats.requests_failed, stats.requests_retries,
    );

    anyhow::ensure!(
        stats.requests_finished == 100,
        "expected 100 finished requests, got {}",
        stats.requests_finished
    );
    anyhow::ensure!(
        stats.requests_failed == 0,
        "expected no failed requests, got {}",
        stats.requests_failed
    );
    Ok(())
}

Next steps

View source on GitHub

On this page