Google Maps Scraper GitHub: How to Monitor Local Business Review Trends with Python
The fastest way to track how local businesses are perceived on Google Maps is to snapshot public review data on a schedule and diff the results. You can do this with a small Python workflow built around the open-source Google Maps scraper for Python maintained by the data-scrape profile. This article shows you how to collect rating, review count, and review text fields, store snapshots, and surface trends without writing a full scraping engine from scratch.
TL;DR
- Clone the data-scrape/scrape-google-maps repository and run it with documented inputs.
- Schedule a Python script to fetch public business records, normalize rating and review fields, and append dated snapshots to JSONL files.
- Diff consecutive snapshots to detect rating drops, review-count spikes, and sentiment shifts.
- Pair it with the Yellow Pages scraper when you need additional local business context such as category or contact signals.
- Respect Google Maps terms, robots directives, and privacy rules; this workflow is for public data research only.
Why review trends matter for local market research
A single star-rating snapshot tells you almost nothing. A time series of rating, review count, and review text tells you:
- Whether a competitor is improving or deteriorating in customer perception.
- When a new business starts accumulating reviews rapidly.
- Which service issues recur across multiple listings in the same neighborhood.
- How your own locations trend after operational changes.
The hard part is not the analysis. It is getting consistent, timestamped records without maintaining a browser farm, parsing layout changes, or managing proxies. That is where a repository-led workflow helps.
The limits of manual tracking and one-off exports
Manual checks fail for anything beyond a handful of listings because:
- Ratings are live and change daily.
- Review counts move as new reviews are added or filtered.
- Copy-pasting from a browser loses provenance and timestamps.
- One-off CSV exports become stale the moment they are saved.
A script that runs on a schedule solves the freshness problem, but only if the underlying scraper is maintained. Writing your own Google Maps parser is fragile because markup and anti-bot measures change. Using a maintained open-source repository lets you focus on the analysis layer instead of the extraction layer.
What the data-scrape Google Maps scraper provides
The data-scrape/scrape-google-maps repository contains a Python-based Google Maps scraping workflow. It is designed for public business records: names, addresses, ratings, review counts, phone numbers, websites, hours, and review samples where publicly visible.
Key characteristics:
- Repository-led, not a managed service. You run it in your own environment.
- Input-driven. You supply a search query or list of place identifiers.
- Output is structured JSON/JSONL that maps cleanly into Python data frames or databases.
- No invented endpoints. The README and example scripts define the inputs the repository actually accepts.
Because it is open source, you can inspect how fields are parsed, adapt it to your own validation rules, and store results however you want.
Workflow design
A review-trend workflow has four stages:
- Input preparation: define the businesses or search area you want to track.
- Extraction: run the scraper and collect public business records.
- Normalization: keep only the fields needed for trend analysis and add timestamps.
- Diffing and alerting: compare the latest snapshot to the previous one and flag changes.
The Python script below assumes you have cloned the repository, installed its documented dependencies, and set your inputs through environment variables. It does not contain hardcoded credentials or fake endpoints.
Environment setup
# Clone the repository
git clone https://github.com/data-scrape/scrape-google-maps.git
cd scrape-google-maps
# Create a virtual environment and install documented dependencies
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
Create a project directory for snapshots next to the repository:
mkdir -p review-snapshots/data
Set inputs through environment variables instead of editing the script:
export SEARCH_QUERY="coffee shops in Seattle, WA"
export SNAPSHOT_DIR="./review-snapshots/data"
export RUN_DATE="2026-09-17"
Runnable Python workflow
The script runs the repository's scraper, normalizes each record, writes a dated JSONL snapshot, and prints businesses whose rating or review count changed since the previous run.
import json
import os
import subprocess
from datetime import datetime, timezone
from pathlib import Path
SEARCH_QUERY = os.environ["SEARCH_QUERY"]
SNAPSHOT_DIR = Path(os.environ["SNAPSHOT_DIR"])
RUN_DATE = os.environ.get("RUN_DATE", datetime.now(timezone.utc).strftime("%Y-%m-%d"))
SNAPSHOT_DIR.mkdir(parents=True, exist_ok=True)
def run_scraper(query: str, output_path: Path) -> None:
"""Run the repository scraper with the documented CLI.
The exact command depends on the repository README. Adjust the script
name and flags to match the current version of data-scrape/scrape-google-maps.
"""
cmd = [
"python",
"scrape_google_maps.py",
"--query", query,
"--output", str(output_path),
]
subprocess.run(cmd, check=True)
def normalize_record(raw: dict, run_date: str) -> dict:
"""Keep only the fields needed for review-trend analysis."""
return {
"place_id": raw.get("place_id") or raw.get("id"),
"name": raw.get("name"),
"address": raw.get("address"),
"rating": _to_float(raw.get("rating")),
"review_count": _to_int(raw.get("review_count") or raw.get("reviews")),
"category": raw.get("category"),
"phone": raw.get("phone"),
"website": raw.get("website"),
"sample_reviews": raw.get("reviews_list", [])[:5],
"snapshot_date": run_date,
"fetched_at": datetime.now(timezone.utc).isoformat(),
}
def _to_float(value):
try:
return float(value)
except (TypeError, ValueError):
return None
def _to_int(value):
try:
return int(str(value).replace(",", "").replace("+", ""))
except (TypeError, ValueError):
return None
def write_snapshot(records: list, run_date: str) -> Path:
path = SNAPSHOT_DIR / f"google_maps_reviews_{run_date}.jsonl"
with path.open("w", encoding="utf-8") as f:
for record in records:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
return path
def load_previous_snapshot(current_path: Path) -> dict:
"""Load the most recent snapshot before the current one."""
files = sorted(SNAPSHOT_DIR.glob("google_maps_reviews_*.jsonl"))
for file in reversed(files):
if file.name != current_path.name:
records = {}
with file.open(encoding="utf-8") as f:
for line in f:
rec = json.loads(line)
records[rec.get("place_id")] = rec
return records
return {}
def diff_snapshots(current: list, previous: dict) -> list:
changes = []
for rec in current:
place_id = rec.get("place_id")
prev = previous.get(place_id)
if not prev:
changes.append({"type": "new", "place_id": place_id, "name": rec["name"]})
continue
rating_delta = (rec["rating"] or 0) - (prev["rating"] or 0)
review_delta = (rec["review_count"] or 0) - (prev["review_count"] or 0)
if rating_delta or review_delta:
changes.append({
"type": "changed",
"place_id": place_id,
"name": rec["name"],
"rating_delta": round(rating_delta, 2),
"review_delta": review_delta,
"previous_rating": prev["rating"],
"current_rating": rec["rating"],
"previous_review_count": prev["review_count"],
"current_review_count": rec["review_count"],
})
return changes
def main() -> None:
raw_output = SNAPSHOT_DIR / f"raw_{RUN_DATE}.json"
run_scraper(SEARCH_QUERY, raw_output)
raw_records = json.loads(raw_output.read_text(encoding="utf-8"))
normalized = [normalize_record(r, RUN_DATE) for r in raw_records]
snapshot_path = write_snapshot(normalized, RUN_DATE)
previous = load_previous_snapshot(snapshot_path)
changes = diff_snapshots(normalized, previous)
print(f"Snapshot written: {snapshot_path}")
print(f"Records collected: {len(normalized)}")
print(f"Changes since last run: {len(changes)}")
for change in changes[:10]:
print(json.dumps(change, ensure_ascii=False))
if __name__ == "__main__":
main()
Representative output
A single normalized record looks like this:
{
"place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
"name": "Seattle Coffee Works",
"address": "2401 Utah Ave S, Seattle, WA 98134",
"rating": 4.6,
"review_count": 1247,
"category": "Coffee shop",
"phone": "+1 206-624-1975",
"website": "https://seattlecoffeeworks.com",
"sample_reviews": [
{"text": "Great single-origin espresso.", "rating": 5, "relative_time": "2 weeks ago"},
{"text": "Long line on weekends.", "rating": 4, "relative_time": "1 month ago"}
],
"snapshot_date": "2026-09-17",
"fetched_at": "2026-09-17T10:00:00+00:00"
}
A diff entry looks like this:
{
"type": "changed",
"place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
"name": "Seattle Coffee Works",
"rating_delta": -0.1,
"review_delta": 12,
"previous_rating": 4.7,
"current_rating": 4.6,
"previous_review_count": 1235,
"current_review_count": 1247
}
Trend analysis checklist
When you review the diff, ask these questions:
- Did the rating move by more than 0.1 stars? Small moves are often noise; larger moves indicate a real perception shift.
- Did the review count jump faster than usual? A sudden spike may follow a marketing campaign, a PR issue, or a policy change.
- Are negative keywords clustering in recent sample reviews? Look for repeated terms like "wait," "cold," "rude," or "closed."
- Are new competitors appearing in the search results? A "new" record in the diff may signal market entry.
- Are review counts flat for a long period? Stagnation can mean a listing is losing visibility or the business is inactive.
Comparison: build, repository, or managed service
| Approach | Best for | Maintenance burden | Data ownership | Cost model |
|---|---|---|---|---|
| Build from scratch | Teams with dedicated scraping engineers | High: parsers, proxies, anti-bot | Full | Infrastructure + labor |
| Repository workflow (data-scrape/scrape-google-maps) | Developers who want inspectable code and local storage | Medium: updates come from the repo | Full | Your own infrastructure |
| Managed scraper API | Teams that need scale without operations | Low: provider handles extraction | Provider-dependent | Provider pricing (verify on official site) |
The repository workflow is the middle path. You keep ownership of the data and the logic, but you do not start from a blank page.
Use cases
Competitor monitoring for multi-location brands
Track rating and review velocity for every competitor location in a city. Identify which neighborhoods have improving or deteriorating service quality.
Francheise quality assurance
Compare review trends across your own franchise locations. A location that falls behind the network average may need operational attention.
Local SEO research
Understand which businesses in a category have the most review momentum. Review count and recency are signals that influence local search visibility.
Market entry analysis
Before opening a new location, collect review trends for incumbent businesses. A saturated market with declining ratings is different from a growing market with rising ratings.
Compliance and maintenance notes
- Public data only. This workflow collects information that is publicly visible on Google Maps listings. Do not attempt to access private accounts, restricted pages, or non-public data.
- Respect terms and robots directives. Google Maps has terms of service and technical protections. Run requests at a reasonable pace and review the latest terms before deploying.
- No CAPTCHA evasion. If the site presents a challenge, stop and reduce frequency or switch to an officially supported data source.
- Keep scraper code current. The data-scrape/scrape-google-maps repository may update selectors and parsing logic. Pull updates before production runs.
- Store provenance. Always keep the raw output alongside normalized snapshots so you can trace a trend back to its source.
- Privacy. Remove or anonymize personal information from review samples before sharing reports internally or externally.
FAQ
Is there an official Google Maps API for this?
Yes. The Google Places API returns business details, ratings, and review counts for supported place IDs. It has quota and pricing limits that vary by project. Use it when you need guaranteed uptime and structured fields. Use the repository workflow when you want full control over storage and scheduling.
What fields are returned by the scraper?
The repository typically returns name, address, rating, review count, category, phone, website, hours, and a sample of review text. Check the current README for the exact field list because it changes as the repository evolves.
How often should I run the workflow?
For most local-market research, daily or weekly snapshots are enough. Running more frequently increases the chance of hitting rate limits without adding much analytical value.
What happens when Google Maps changes its layout?
The repository maintainers update the parsing logic. Pull the latest version, run a test extraction, and compare the output fields to your previous snapshots before scheduling a production run.
Can I combine this with other local data sources?
Yes. The data-scrape/yellow-pages-scraper repository is useful for adding category, contact, or business-age context to your Google Maps records. Match records by name and address rather than assuming a shared identifier.
How do I alert when a competitor's rating drops?
Add a threshold check to the diff_snapshots function. For example, emit a notification when rating_delta <= -0.2 or when review_delta exceeds a baseline. Send the alert to email, Slack, or a queue.
Can this run in the cloud on a schedule?
Yes. You can run the script from a cron job, GitHub Actions, or a small cloud VM. Keep the scraper repository as a dependency and pin to a specific commit for reproducibility.
CTA and related repositories
If you want a maintainable, code-first way to track local business review trends, start with the open-source Google Maps scraper for Python from the data-scrape profile. Combine it with the Yellow Pages scraper when you need broader local business context, and adapt the diffing logic to your own alerting rules.
Top comments (0)