Production-ready Node.js (plain JavaScript) scraper for stays (resorts, homestays, hotels) in Varkala, Kerala. Uses Playwright and stores results in Neon PostgreSQL via Drizzle ORM.
- Scrapes Google Maps for: resorts, homestays, hotels in Varkala
- Saves: name, rating, total reviews, address, phone, website, category, lat/lng, Google Maps URL, image URLs
- Anti-blocking: headful browser, random delays, user-agent rotation, stealth hints, sequential scraping, retry with exponential backoff
- Infinite scroll: scrolls the results list until no new places load; deduplicates by name + address
- Database: Drizzle ORM + Neon Postgres, batch insert with conflict ignore on
google_maps_url - Optional: proxy support, rate limiter
- Node.js >= 18
- Neon PostgreSQL database
- Playwright browsers (installed via
npx playwright install)
cd maps-scraper
npm installnpx playwright install chromiumCopy the example env and set your Neon connection string:
cp .env.example .envEdit .env:
DATABASE_URL=postgresql://user:password@host/database?sslmode=requireGet DATABASE_URL from Neon Console → your project → Connection string.
Optional:
PROXY_URL– HTTP/HTTPS proxy (e.g.http://user:pass@host:port)DELAY_MIN_MS/DELAY_MAX_MS– Random delay range in ms (default 2000–5000)
Push schema to your Neon database (creates/updates tables):
npm run db:pushOr use migrations:
npm run db:generate # generate migration files in ./drizzle
npm run db:migrate # run migrationsDrizzle config is in drizzle.config.js; schema is in src/db/schema.js.
node index.jsOr:
npm startThis will:
- Open a headful Chromium window (required for anti-blocking)
- Search Google Maps for “resorts in Varkala”, “homestays in Varkala”, “hotels in Varkala”
- Scroll each search result list to the end and collect place URLs
- Open each place, extract details, and deduplicate
- Batch-insert into the
staystable (duplicates ongoogle_maps_urlare ignored) - Log progress and final counts
Logs look like:
Starting Google Maps scraper (Varkala stays)Searching: resorts in VarkalaScraped 120 placesSaved to DB: 120 stays (duplicates on google_maps_url ignored)Done in 1234.5 s
/src
/config
env.js # env vars
index.js
/db
index.js # Drizzle client, upsertStays()
schema.js # stays table
/scraper
browser.js # launch Playwright (headful, UA rotation, proxy)
parser.js # parse place cards and detail panel
scraper.js # search, scroll, scrape one-by-one, dedupe
/utils
delay.js # random/fixed delay
logger.js # simple logger
retry.js # retry with exponential backoff
rateLimit.js # optional rate limiter
index.js # CLI entry: run scraper → save to DB
drizzle.config.js
package.json
.env.example
- Page load: retry with exponential backoff (configurable in
retry.js) - Single place: on failure, log and skip; scraper continues
- DB: batch insert with
onConflictDoNothingongoogle_maps_url; duplicate rows are skipped
The scraper does not persist progress by default. To add resume:
- Save the list of
google_maps_urlalready in the DB (or from a previous run) and skip those when iterating inscraper.js, or - Run periodically; duplicates are ignored on insert, so re-runs are safe.
DATABASE_URL=postgresql://user:password@ep-xxx.region.aws.neon.tech/neondb?sslmode=require
# PROXY_URL=
# DELAY_MIN_MS=2000
# DELAY_MAX_MS=5000| Command | Effect |
|---|---|
npm run db:generate |
Generate SQL migrations in ./drizzle |
npm run db:migrate |
Run pending migrations |
npm run db:push |
Push schema to DB (no migration files) |
npm run db:studio |
Open Drizzle Studio UI |
MIT