Apify-style Google Maps scraper for Rust. Drives a real headless Chrome via the Chrome DevTools Protocol — no API key required.
Until now there has been no production-quality Rust crate for scraping Google Maps. The official Places API is paid, and Apify is JavaScript-only. This crate fills the gap with the same approach used by Apify's compass~crawler-google-places actor, written natively in Rust.
- 🌍 Search Google Maps with arbitrary text queries.
- 🔄 Auto-scrolls the results feed until exhausted.
- 🪟 Clicks each result to grab address + phone + website from the panel.
- 🚪 Auto-dismisses the cookie consent banner on first visit.
- 🛡️ Sets
--disable-blink-features=AutomationControlledto reduce detection. - 📦 Returns clean
Placestructs with German address parsing built-in.
- Chrome / Chromium installed locally — unless you connect to a remote Chrome
via
browserless_url/BROWSERLESS_URL(see Remote Chrome), in which case no local browser is needed.- macOS: detected at
/Applications/Google Chrome.app/.... - Linux:
apt install chromium-browser. - Windows: detected in
Program Files.
- macOS: detected at
- Override the binary location with the
CHROMEenv var if needed.
[dependencies]
google-maps-scraper = "0.4"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }use google_maps_scraper::{MapsScraper, ScraperConfig};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let scraper = MapsScraper::launch(ScraperConfig::default()).await?;
let places = scraper.search("coffee shop Berlin").await?;
for p in &places {
println!("{} – {:?} – {:?}", p.name, p.website, p.phone);
}
Ok(())
}# use google_maps_scraper::{MapsScraper, ScraperConfig};
# #[tokio::main]
# async fn main() -> Result<(), Box<dyn std::error::Error>> {
let scraper = MapsScraper::launch(ScraperConfig::default()).await?;
let places = scraper.search_many(&[
"bakery Munich",
"bakery Hamburg",
"bakery Frankfurt",
]).await?;
println!("{} unique places across all 3 queries", places.len());
# Ok(()) }Results are automatically deduplicated by website domain (or maps URL when no website).
ScraperConfig is #[non_exhaustive] (new options arrive without breaking you),
so start from default() and set what you need:
# use google_maps_scraper::{MapsScraper, ScraperConfig};
# use std::time::Duration;
# #[tokio::main]
# async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut cfg = ScraperConfig::default();
cfg.headless = false; // see Chrome window for debugging
cfg.max_scroll_iterations = 50; // load more results
cfg.enrich = true; // click each place for website/phone
cfg.between_query_delay = Duration::from_secs(3);
cfg.place_panel_delay = Duration::from_millis(2000);
cfg.place_panel_jitter = Duration::from_millis(750); // random extra 0..=750ms per place
cfg.max_places = Some(50); // cap unique places per query (None = unlimited)
cfg.nav_timeout = Duration::from_secs(30); // fail instead of hanging on a stalled page
cfg.proxy = Some("http://user:pass@host:port".into()); // or set the PROXY_URL env var
cfg.user_agent = None; // None = Chrome's own current UA (recommended)
cfg.browserless_url = None; // or set BROWSERLESS_URL to use a remote Chrome
cfg.language = Some("en".into()); // pin the Maps UI language (default "en")
let scraper = MapsScraper::launch(cfg).await?;
# Ok(()) }The language option pins Google's UI language via the hl= parameter on every
navigation. Without it Google picks the language from your exit IP's geo — which
silently breaks label-based extraction behind non-EN/DE proxies. Leave it at the
default unless you have a reason not to.
Set enrich: false for a 5–10× speedup if you only need names + maps URLs (no website / phone).
For high-volume scraping, set proxy (or the PROXY_URL environment variable) to route
Chrome through a residential proxy and reduce the chance of being soft-banned.
If you don't have Chrome installed locally, point the scraper at a remote Chrome over
the DevTools WebSocket. Set browserless_url (or the BROWSERLESS_URL environment
variable) and MapsScraper::launch will connect to it instead of launching a browser:
# use google_maps_scraper::{MapsScraper, ScraperConfig};
# #[tokio::main]
# async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cfg = ScraperConfig {
browserless_url: Some("ws://localhost:3000".into()), // or wss://chrome.browserless.io?token=...
..Default::default()
};
let scraper = MapsScraper::launch(cfg).await?;
# Ok(()) }When connecting to a remote Chrome, local launch arguments (headless, proxy, window
size, user agent) are controlled by the remote endpoint — configure those there.
pub struct Place {
pub name: String,
pub address: Option<String>,
pub postcode: Option<String>, // "NNNNN City" (German postal format); else None
pub city: Option<String>, // parsed alongside postcode; else None
pub phone: Option<String>,
pub website: Option<String>,
pub maps_url: Option<String>,
pub latitude: Option<f64>, // parsed from the maps_url @lat,lng segment
pub longitude: Option<f64>,
pub place_id: Option<String>, // stable Google Place ID (cross-run dedup,
// joins against the official Places API)
pub cid: Option<u64>, // stable Google CID (maps.google.com/?cid=…)
pub rating: Option<f32>, // average star rating (0.0–5.0)
pub reviews_count: Option<u32>, // number of reviews
pub category: Option<String>, // primary business category
pub source_query: Option<String>,
}(Place is #[non_exhaustive] — new optional fields arrive in minor releases
without breaking your build.)
Place derives Serialize + Deserialize so you can write straight to JSONL.
cargo run --example scrape_oneGoogle detects bot traffic. Rough survival guide:
- 🟢 1–30 queries per session: usually fine.
- 🟡 30–100 queries: feed may start returning empty. Restart the browser between batches.
- 🔴 100+ queries from one IP: expect to be soft-banned (search returns 0 results). Use a residential proxy or Browserless cloud Chrome.
The crate ships with the most reliable selectors at the time of writing. Google's DOM shifts occasionally — file an issue if the scraper stops returning data.
| Tool | Lang | Pricing | Notes |
|---|---|---|---|
| Google Places API | any | $32/1000 + $17/1000 details | Official; paid. Full coverage. |
Apify compass~crawler-google-places |
JS | $5/1000 results | Battle-tested. Requires Apify account. |
| google-maps-scraper | Rust | $0 | Self-hosted; brittler; this crate. |
googlescraper (Python) |
Python | $0 | Less maintained. |
- ✅ Proxy support via
ScraperConfig::proxy/PROXY_URL. - ✅ Coordinates (
latitude/longitude) parsed from the maps URL. - ✅ Remote Chrome (Browserless) support via
ScraperConfig::browserless_url/BROWSERLESS_URL. - ✅ Richer place data: rating, review count, category.
- ✅ Stable identifiers (
place_id/cid) for cross-run dedup and Places-API joins. - ✅ Deterministic UI language via
ScraperConfig::language(hl=pinning). - Headed-mode debugging helper that opens DevTools.
- Opening hours extraction.
- Concurrent multi-page scraping inside one browser.
- Stealth plugin (
puppeteer-extra-plugin-stealth-equivalent).
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT License (LICENSE-MIT)
at your option.