Skip to content

Repository files navigation

πŸ—ΊοΈ Google Maps Review Intelligence Platform

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

License: MIT Python 3.10+ Docker Streamlit


πŸ—οΈ Architecture β€” 24/7 Production Collector

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)]
Loading

Read the full war story: Fixing OOM crashes in a 24/7 scraper


🎯 What This System Does

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

πŸ“Š Real Data β€” Collected & Growing (as of 2 July 2026)

 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.


πŸš€ Quick Start

git clone https://github.com/ahmadafif5321/GoogleMapScrapper.git
cd GoogleMapScrapper
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

The One-Liner

python main.py full "clinic in Cyberjaya"

Scrapes reviews from Google Maps, runs sentiment analysis, generates insights. Ready for dashboard.


🧰 Command Arsenal

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

collect β€” The Heavy Lifter

# 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

analyze β€” Two Sentiment Engines

# 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 transformer

generate-queries β€” Maximum Coverage

Generates 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

πŸ“ˆ The Dashboard β€” 8 Pages with Animations

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

✨ What's New in v2.0

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

πŸ”„ The Collector Engine

β”Œβ”€ 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.


πŸ“ Architecture

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

βš™οΈ Configuration β€” config.py

Scraper Tuning

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"

NLP Pipeline

SENTIMENT_MODEL = "cardiffnlp/twitter-xlm-roberta-base-sentiment"
EMBEDDING_MODEL = "paraphrase-multilingual-MiniLM-L12-v2"
MAX_REVIEWS_PER_PLACE = 300

Aspect Categories β€” 9 Dimensions

service Β· price Β· quality Β· ambiance Β· location Β· wait_time Β· menu_variety Β· portion Β· delivery

Each with bilingual keywords (EN + BM/ID). Extensible: add your own in config.py.


πŸ”§ Usage Recipes

Scrape clinics across 10 cities

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 dashboard

Run 24/7 collector in background

nohup python main.py collect -q scraper/queries_full.txt --continuous --cycle-hours 6 &
python main.py stats      # check progress

Export everything for Excel / Power BI

python 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.csv

Target specific competitors

echo "Klinik Contoh Cyberjaya" > targets.txt
echo "Hospital Contoh" >> targets.txt
python main.py full "Klinik Contoh Cyberjaya" "Hospital Contoh" "Klinik Contoh 2 Sepang"

Use Google Maps share links directly

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 Guide

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.


πŸ› Troubleshooting

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

πŸ“„ License

MIT β€” see LICENSE

πŸ™ Credits

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages