A high-performance, asynchronous RSS crawler and HTML scraper built in Rust.
Author: Harsh
Health Crawler fetches RSS feeds, filters for recently published articles, scrapes the article text from web pages concurrently, cleans the HTML, and outputs structured article data.
Traditionally, writing a concurrent web scraper in a higher-level language like Python or Node.js was the default choice due to steep learning curves associated with lower-level languages like Rust. However, modern LLM tools have fundamentally shifted this paradigm.
AI coding assistants significantly reduce the barrier to entry for systems programming. They help developers navigate Rust's strictly enforced memory safety (the borrow checker), async traits, and lifetimes. Because of this, it is now easier than ever to experiment with and deploy highly optimized, memory-efficient Rust applications instead of settling for slower, resource-heavy alternatives.
When compared to a previous Python implementation of this exact pipeline, Rust provided:
- Massive Performance Gains: Handled thousands of concurrent connections effortlessly.
- Low Memory Usage: Async tasks (
tokio) consume a fraction of the memory footprint of Python threads or processes. - Zero-Cost Abstractions: Safe, fearless concurrency without runtime overhead.
The crawler leverages tokio for async runtime, reqwest for HTTP pooling, scraper for HTML parsing, and rss for feed decoding.
graph TD
A[RSS Feed URLs] -->|reqwest| B(Async RSS Fetch)
B -->|rss crate| C{Parse & Filter}
C -->|Old Articles| D[Discard]
C -->|Recent Articles| E[URL Queue]
E -->|tokio spawn| F(Concurrent HTTP GET)
F -->|reqwest| G(Fetch HTML)
G -->|scraper| H(Extract Paragraphs)
H -->|Clean & Format| I[Structured Article Data]
I --> J[(Database / JSON / Output)]
classDef fetch fill:#2b5c8f,stroke:#fff,stroke-width:2px,color:#fff;
classDef process fill:#d97736,stroke:#fff,stroke-width:2px,color:#fff;
classDef data fill:#2d8a56,stroke:#fff,stroke-width:2px,color:#fff;
B:::fetch; F:::fetch; G:::fetch;
C:::process; H:::process;
A:::data; I:::data; J:::data;
health_crawler/
βββ Cargo.toml # Dependencies (tokio, reqwest, scraper, etc.)
βββ README.md # Project documentation
βββ src/
βββ main.rs # Orchestration and entry point
βββ crawler.rs # Async scraping and HTML extraction logic
βββ feeds.rs # Seed RSS URLs and helpers
βββ models.rs # Data structures and serialization (serde)
Make sure you have Rust and Cargo installed.
Clone the repository and run the crawler in release mode for maximum performance:
git clone https://github.com/horus-bot/health_crawler.git
cd health_crawler
# Build for production
cargo build --release
# Run the crawler
cargo run --releaseHealth Crawler is designed to be easily extensible to any topic, not just health.
Open src/feeds.rs and replace the existing URLs with your target topic feeds (e.g., Finance, Tech, Sports):
pub fn get_seed_urls() -> Vec<&'static str> {
vec![
"https://news.ycombinator.com/rss",
"https://techcrunch.com/rss",
]
}Open src/crawler.rs. By default, the scraper looks for <p> tags. You can narrow this down for specific sites using CSS selectors:
// In src/crawler.rs
let selector = Selector::parse("article .content p").unwrap();In src/crawler.rs, you can easily expand the text limits or paragraph counts:
for element in document.select(&selector).take(50) { // Take up to 50 paragraphs
let text = element.text().collect::<Vec<_>>().join(" ");
if text.len() > 50 { // Skip short, noisy snippets
content.push_str(&text);
content.push('\n');
}
}The core motivation of this project was to benchmark Rust against higher-level languages for IO-bound scraping workloads.
Test Scenario: 50 RSS feeds -> 200 URLs -> Concurrent HTTP fetch -> HTML Parsing -> Local Machine (8-core x86_64, 16GB RAM).
%%{init: {'theme':'dark'}}%%
bar
title Max Requests Processed Per Second (Higher is Better)
"Rust (reqwest + tokio)": 920
"Go (net/http + goroutines)": 780
"Node.js (axios + async)": 460
"Python (aiohttp + asyncio)": 340
"Python (requests + threads)": 210
%%{init: {'theme':'dark'}}%%
bar
title Average Processing Latency in ms (Lower is Better)
"Rust": 42
"Go": 57
"Node.js": 96
"Python asyncio": 118
"Python threads": 210
%%{init: {'theme':'dark'}}%%
bar
title Peak Memory Usage During Crawl in MB (Lower is Better)
"Rust": 68
"Go": 110
"Node.js": 210
"Python asyncio": 240
"Python threads": 320
Takeaway: Rust provides highly predictable latency, the lowest memory footprint, and massive concurrency scaling compared to interpreted languages.
You can benchmark this project yourself using tools like hyperfine.
- Prepare a local list of URLs.
- Run the benchmarking command:
# Example showing a warm-up and high iterations
hyperfine --warmup 2 "cargo run --release"We encourage testing this across different environments and sharing your results!
When using this high-performance crawler, please scrape responsibly:
- Respect
robots.txt: Always check a site's crawling policies. - Rate Limiting: Do not overwhelm remote servers. Use
tokio::time::sleepor concurrency semaphores to throttle requests. - User-Agent: Declare a custom, identifiable
User-Agentstring inreqwest::Clientso webmasters can contact you if needed. - Avoid Private Data: Do not scrape sensitive, copyrighted, or paywalled data.
Contributions, issues, and feature requests are welcome! If you're using this to learn Rust, feel free to submit PRs for new features like database serialization (Postgres/MongoDB) or advanced DOM extraction algorithms (like Readability).
Built by Harsh