Scrape thousands of Google Maps reviews β NLP sentiment & topic analysis β 8-page animated dashboard β Actionable business insights
Bilingual β 2.2M+ reviews across 16 areas β 17,900+ places β 5,100+ lines of Python β 38GB of collected data β and still collecting 24/7
This isn't a one-off script. It runs as a systemd service, 24/7, against a client's business listings β a supervised loop that spins up Docker scraper containers, merges results with DuckDB, and re-schedules queries based on how much new data they're actually yielding. It's resource-capped end to end because an earlier version wasn't: the merge step once ballooned past 10GB of RAM and the kernel OOM-killer stalled the entire host. Every cap below (systemd memory limits, per-container Docker caps, DuckDB spill-to-disk) exists because of that incident.
flowchart TB
subgraph systemd["systemd unit β MemoryHigh=4G Β· MemoryMax=6G Β· Nice=10"]
LOOP[collector loop]
end
LOOP -->|spawns| D1["scraper container\n(docker --memory 4g)"]
LOOP -->|spawns| D2["scraper container\n(docker --memory 4g)"]
D1 --> RAW[(raw batches)]
D2 --> RAW
RAW --> MERGE["DuckDB incremental merge\nmemory_limit=3GB β spills to disk"]
MERGE --> DATA[(deduped dataset)]
Read the full war story: Fixing OOM crashes in a 24/7 scraper
| Stage | Tool | Output |
|---|---|---|
| Collect | Docker scraper engine, batch scheduler | 2.2M+ reviews from 17,900+ places |
| Analyze | XLM-RoBERTa sentiment, LDA topics, KeyBERT keywords | Sentiment scores, 9 aspect categories, topic clusters |
| Visualize | 8-page animated dashboard with maps, radar, time-series | Rankings, maps, trends, word clouds, radar plots, comparison matrices |
| Export | CSV/Parquet/JSON + HTML Executive Report | Excel-ready files + standalone HTML report |
| Loop | 24/7 continuous collector with auto-resume | Fresh data every N hours, forever |
2,778 places Β· 480,201 reviews β Petaling Jaya
1,588 places Β· 255,199 reviews β Subang Jaya
2,254 places Β· 202,642 reviews β Kajang
1,780 places Β· 201,417 reviews β Cheras
1,164 places Β· 189,989 reviews β Putrajaya
1,546 places Β· 167,528 reviews β Cyberjaya
991 places Β· 152,989 reviews β Bangi
694 places Β· 145,813 reviews β Bangsar
988 places Β· 53,990 reviews β Puchong
849 places Β· 51,302 reviews β Seri Kembangan
380 places Β· 22,322 reviews β Dengkil
376 places Β· 15,733 reviews β Sepang
551 places Β· 11,616 reviews β Nilai
173 places Β· 5,072 reviews β Salak Tinggi
+ other areas (Serdang, unclassified suburbs, β¦)
ββββββββ βββββββββββ
17,914 places Β· 2,241,989 unique reviews Β· 1,267 merged batches
Still growing, unattended. The collector runs 24/7 as a systemd service with a yield-aware scheduler: each of the 470 active queries is re-scheduled based on how many new reviews its last run actually produced β a query yielding 3,000 fresh reviews comes back in hours, while a saturated one cools down for weeks. Growth from 455K reviews (June 2026) to 2.24M (July 2026) happened without manual intervention.
git clone https://github.com/ahmadafif5321/GoogleMapScrapper.git
cd GoogleMapScrapper
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txtpython main.py full "clinic in Cyberjaya"Scrapes reviews from Google Maps, runs sentiment analysis, generates insights. Ready for dashboard.
| Command | What it does | Example |
|---|---|---|
scrape |
Pull reviews from Google Maps | python main.py scrape "clinic in KL" "klinik in PJ" |
analyze |
Sentiment + topics + insights | python main.py analyze reviews.parquet -s vader |
full |
Scrape then analyze in one shot | python main.py full "cafe in Bandung" |
collect |
Batch collector β hours to days | python main.py collect -q queries.txt -b 3 -d 120 |
dashboard |
Launch 6-page Streamlit UI | python main.py dashboard -p 8080 |
export |
Dump everything as CSV | python main.py export |
generate-queries |
Auto-spawn 332 search terms | python main.py generate-queries |
stats |
Live collection progress | python main.py stats |
# One-shot: work through all queries once
python main.py collect -q scraper/queries_full.txt
# 24/7 mode: re-scrape every 6 hours indefinitely
python main.py collect --continuous --cycle-hours 6
# Conservative (safe, slower)
python main.py collect -b 2 -d 180
# Aggressive (fast, riskier)
python main.py collect -b 5 -d 60
# Resume from crash: just run the same command
python main.py collect -q scraper/queries_full.txt --continuous
# Start fresh
python main.py collect --reset| Flag | Default | Description |
|---|---|---|
-q |
scraper/queries.txt |
Query file (one per line) |
-b |
2 |
Batch size β queries per Docker run |
-d |
180 |
Cooldown between batches (seconds) |
-m |
0 (unlimited) |
Stop after N batches |
--continuous |
off | Re-run all queries every cycle |
--cycle-hours |
24 |
Hours between cycles |
--reset |
off | Wipe progress, start fresh |
# VADER β instant, no GPU, English/Malay
python main.py analyze reviews.parquet -s vader
# XLM-RoBERTa Transformer β multilingual, higher accuracy
python main.py analyze reviews.parquet -s transformerGenerates 332 queries across 8+ cities Γ 25 medical categories (BM + EN), or 500+ queries for broader coverage. Pre-built files included:
| File | Queries | Target |
|---|---|---|
scraper/queries_100places.txt |
50 | ~100-200 places, quick run |
scraper/queries_full.txt |
332 | ~500-1,000+ places, comprehensive |
scraper/queries_expansion.txt |
80+ | Additional cities, extra coverage |
scraper/queries_ondemand.txt |
Custom | Specific businesses / Google share links |
Launch with python main.py dashboard. Opens at http://localhost:8501.
| Page | What You See |
|---|---|
| π Executive Summary | Animated KPI cards, top 3 insights, health gauges, 1-click HTML report export |
| π Overview & Rankings | Place leaderboard with gradient tables, rating histogram, top/bottom performers |
| π Sentiment Analysis | Donut charts, stacked sentiment-by-rating, per-place breakdowns, sentiment health score |
| π Aspect Analysis | 9-dimension aspect breakdown (service, price, quality, ambiance, location, etc.) |
| π Keywords & Topics | Word cloud, n-gram phrases, LDA topic clusters, keyword-sentiment correlation |
| βοΈ Competitor Comparison | Side-by-side benchmarks, radar charts, aspect heatmap matrix, competitive advantages |
| π‘ Insights & Roadmap | Strengths, complaint categories, prioritized improvement roadmap, CSV/HTML export |
| πΊοΈ Map View | Interactive geographic map with sentiment overlay, place clustering, location rankings |
| Feature | Description |
|---|---|
| CSS Animations | Fade-in, slide-in, pulse, glow, shimmer, counter animations across all pages |
| Animated KPI Cards | Gradient cards with auto-animated counters, trend arrows, hover effects |
| Global Filter Bar | Filter by rating range, sentiment, category, and place β affects all charts |
| Map View | Plotly scatter mapbox with sentiment coloring, review count sizing, zoom/pan |
| Radar Charts | Multi-dimensional spider chart for competitor aspect comparison |
| Sentiment Health Score | Single metric: positive% β negative% with gauge visualization |
| HTML Report Generator | One-click standalone executive report with KPI tables and recommendations |
| Donut & Gauge Charts | Modern chart types replacing basic pie/bar where appropriate |
| Professional Theme | Custom CSS: gradient hero banners, scrollbar styling, card shadows, tooltips |
ββ CYCLE ββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β ββ BATCH LOOP βββββββββββββββββββββββββββββββββββββββ β
β β β β
β β 1. Pull N queries from queue β β
β β 2. Spin up Docker scraper ββββββββββββββββββββ β β
β β 3. Google Maps β JSON β RESUME-ABLE β β β
β β 4. Parse β parquet β Every batch β β β
β β 5. Save state to JSON β saved to disk β β β
β β 6. Cooldown (120s default) ββββββββββββββββββββ β β
β β 7. Repeat until queue empty β β
β β β β
β β Every 10 batches: MERGE + EXPORT CSV β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β If --continuous: wait N hours β reset queue β repeat β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Crash-proof: progress saved after every single batch. Kill the process, reboot, Ctrl+C β re-run the exact same command and it picks up where it left off.
State file: scraper/collector_state.json β tracks every query status, timestamps, and counts.
GoogleMapScrapper/
β
βββ main.py βΈ CLI hub β 9 commands, argparse-driven
βββ config.py βΈ Settings: scraper limits, aspect cats, NLP models
β
βββ scraper/
β βββ run_scraper.py βΈ Docker interface to gosom/google-maps-scraper
β βββ collector.py βΈ Batch scheduler, merge engine, CSV exporter
β βββ query_generator.py βΈ Auto-generates 332+ queries across cities
β βββ queries_*.txt βΈ Pre-built query files (100 / full / expansion / ondemand)
β βββ collector_state.json βΈ Runtime: progress + yield-aware schedule (17.9K+ places, 2.2M+ reviews)
β
βββ analytics/
β βββ preprocess.py βΈ Text cleaning, tokenization, filtering
β βββ sentiment.py βΈ VADER + XLM-RoBERTa, aspect-based scoring
β βββ topics.py βΈ TF-IDF, KeyBERT, LDA, n-gram extraction
β βββ insights.py βΈ Negative review mining, strengths, recommendations
β βββ compare.py βΈ Cross-entity competitor benchmarking
β βββ pipeline.py βΈ Full orchestrator: raw β insights in one call
β
βββ dashboard/
β βββ app.py βΈ Streamlit 8-page interactive web UI (v2.0)
β βββ components.py βΈ Reusable animated components (KPI cards, filters, reports)
β βββ styles.py βΈ Custom CSS (animations, gradients, card themes)
β
βββ data/
β βββ raw/ βΈ Docker scraper JSON output (25GB+)
β βββ processed/ βΈ Parquet + CSV + JSON analytics (13GB+)
β
βββ docker-compose.yml βΈ Scraper container definition
βββ requirements.txt βΈ Python deps
SCRAPER_CONCURRENCY = 4 # Parallel browser tabs
SCRAPER_DEPTH = 15 # Scroll depth per search
SCRAPER_EXTRA_REVIEWS = True # ~300 reviews per place
SCRAPER_TIMEOUT = 7200 # 2h max per run
# Geo-targeting (optional β set to activate)
SCRAPER_GEO = None # "lat,lon"
SCRAPER_RADIUS = None # meters
SCRAPER_GRID_BBOX = None # "minLat,minLon,maxLat,maxLon"SENTIMENT_MODEL = "cardiffnlp/twitter-xlm-roberta-base-sentiment"
EMBEDDING_MODEL = "paraphrase-multilingual-MiniLM-L12-v2"
MAX_REVIEWS_PER_PLACE = 300service Β· price Β· quality Β· ambiance Β· location Β· wait_time Β· menu_variety Β· portion Β· delivery
Each with bilingual keywords (EN + BM/ID). Extensible: add your own in config.py.
python main.py collect -q scraper/queries_full.txt -b 3 -d 120
python main.py analyze data/processed/collected_all_reviews.parquet -s vader
python main.py dashboardnohup python main.py collect -q scraper/queries_full.txt --continuous --cycle-hours 6 &
python main.py stats # check progresspython main.py export
# β data/processed/all_reviews.csv (all reviews)
# β data/processed/places_summary.csv (per-place stats)
# β data/processed/sentiment_by_place.csv
# β data/processed/recommendations.csv
# β data/processed/aspect_summary.csv
# β data/processed/comparison_ranking.csvecho "Klinik Contoh Cyberjaya" > targets.txt
echo "Hospital Contoh" >> targets.txt
python main.py full "Klinik Contoh Cyberjaya" "Hospital Contoh" "Klinik Contoh 2 Sepang"The scraper now accepts Google Maps share URLs (e.g., https://share.google/<share-id>) as queries β drop them in scraper/queries_ondemand.txt and run.
| Scale | Places | Reviews | CPU | RAM | Time |
|---|---|---|---|---|---|
| Small | <30 | <10K | 2 cores | 4GB | 15 min |
| Medium | 30-200 | 10K-60K | 4 cores | 8GB | 1-3 hrs |
| Large | 200-1,000 | 60K-300K | 8 cores | 16GB | 6-24 hrs |
| City-wide | 1,000+ | 300K+ | 16 cores | 32GB | Days |
Sentiment: VADER runs in seconds (even on 100K+ reviews). Transformer needs GPU for large datasets.
| Problem | Fix |
|---|---|
| Docker not running | sudo systemctl start docker |
| Only 8 reviews per place | Re-scrape with latest code (bug fixed) |
| Collector crashed | Re-run same command β auto-resumes |
| Google rate limiting | Increase delay: -d 300, reduce batch: -b 2 |
| Memory errors | Close other apps, reduce -b to 1 |
ModuleNotFoundError |
pip install sentencepiece tiktoken (transformer mode) |
| Port 8501 taken | python main.py dashboard -p 8080 |
| Empty results | Query might be too niche β broaden it |
MIT β see LICENSE
- gosom/google-maps-scraper β the Docker scraper engine
- HuggingFace Transformers β XLM-RoBERTa multilingual sentiment
- Streamlit β dashboard framework
- KeyBERT β keyword extraction