A production-grade, modular, resumable lead-generation scraper built in Python. It collects Google Maps business data, enriches each business's website (emails, social links, tech stack, signals), optionally verifies email deliverability, and exports clean CSV + XLSX — all while surviving crashes, reboots, and long multi-week runs without restarting from zero.
Designed for long-running operation on a 12 GB RAM / 200 GB disk Linux VPS (e.g. Oracle Cloud). Historical runs have reached ~200,000 records over ~2 weeks.
- Google Maps collection — streams listings per query, opening each business to capture name, category, phone, website, hours, address/geo, rating/reviews, claimed status, description, plus code, place ID, and more.
- Normalization + deduplication — canonical URLs, country-aware phones, and a multi-signal identity resolver that removes duplicates without wrongly collapsing multi-location chains.
- Filtering — keep/reject businesses early (AND/OR/NOT conditions) so the engine never wastes VPS resources enriching records you don't want.
- Website enrichment — HTTP-first fetching with a smart priority crawler
- sitemap, escalating to Playwright only when JS is required or HTTP is blocked.
- Email + signals + tech stack — extracts/cleans emails, detects social profiles, technologies, and configurable business signals.
- Optional MX / SMTP verification — both off by default, both safe.
- Atomic export + checkpoint — every record is committed to CSV and a SQLite checkpoint transactionally; a crash resumes at the exact right place.
- Quality gate + summary — automated data-quality report and
run_summary.json.
| Resource | Minimum | Recommended |
|---|---|---|
| RAM | 4 GB | 8–12 GB |
| Disk | 20 GB | 100–200 GB |
| OS | Linux (Ubuntu/Debian preferred) | |
| Python | 3.9+ | 3.10–3.12 |
git clone <your-repo-url> && cd Fiverr-Automation
./setup.shsetup.sh installs system packages, creates a .venv, installs Python
dependencies, and downloads the Chromium browser for Playwright.
Edit config.yaml (one human-readable file). Secrets go in .env
(copy .env.example). See GUIDE.md for a plain-English walkthrough of
every setting.
Set your job name and queries:
job:
client_name: my_campaign
output_filename: my_campaign
queries:
- "dentists in Dallas, TX"
- "dentists in Houston, TX"source .venv/bin/activate
python main.pyThe engine:
- validates your config (and aborts with a clear message if anything is wrong),
- detects any existing checkpoint and resumes if present,
- streams plain-English progress to the terminal (which search is running,
each business found, and how many were exported) plus full detail in
scraper.log.
To run it in the background (survive SSH disconnects), use tmux — see
docs/RUN_WITH_TMUX.md for a plain-English guide.
Just run python main.py again. The engine reads the SQLite checkpoint in the
job's output/ folder and:
- skips already-completed queries,
- never re-discovers or duplicate-commits existing records,
- resumes unfinished work (including the exact record that was mid-process).
It is safe against tmux drops, VPS reboots, browser crashes, network
outages, and Ctrl-C.
By default the browser runs invisibly (maps.headless: true). To watch it
live and solve CAPTCHAs by hand, switch on a separate virtual screen:
./vnc-screen.sh— starts a dedicated screen (display:2, non-common port43873) that never touches your existing:1screen.- Set
maps.headless: falseinconfig.yaml. python main.py— the browser appears on that screen.- Connect TightVNC to
YOUR-VPS-IP:43873and click the CAPTCHA when it shows.
See docs/VNC_SETUP.md for the full plain-English walkthrough.
Everything lands in output/<client_name>/:
| File | Purpose |
|---|---|
<client_name>.csv |
Primary source of truth — append-safe records |
<client_name>.xlsx |
Convenience spreadsheet (built at the end) |
filtered_records.csv |
Records rejected by filters (with reason) |
failed_records.csv |
Records that failed validation |
run_summary.json |
Job statistics |
quality_report.json |
Final data-quality gate results |
scraper.log |
Persistent diagnostics |
checkpoint.db / .json / backup |
Resumable state |
source .venv/bin/activate
pip install -r requirements.txt # if not already done
python -m pytest tests/ -qCore logic (normalization, dedup, filters, signals, checkpoint recovery, config validation, CSV integrity, status classification) is tested with mocks — no live Google Maps needed.
.
├── main.py # entrypoint
├── config.yaml # the ONE config file you edit
├── .env.example # secrets template (copy to .env)
├── requirements.txt
├── setup.sh
├── vnc-screen.sh # visible Scraper Engine screen (TightVNC)
├── README.md
├── GUIDE.md # plain-English config guide
├── docs/architecture.md # design decisions
├── docs/VNC_SETUP.md # visible-screen + CAPTCHA walkthrough
├── docs/RUN_WITH_TMUX.md # background running + reading progress
├── scraper/
│ ├── config.py # config load + validation
│ ├── models.py # schema + status model
│ ├── pipeline.py # orchestration
│ ├── maps/ # Google Maps collector
│ ├── websites/ # fetcher, crawler, enricher, tech detect
│ ├── email/ # extraction + MX/SMTP
│ ├── signals/ # signal detection engine
│ ├── filters/ # filter engine
│ ├── dedup/ # identity resolution
│ ├── checkpoint/ # SQLite resumable state
│ ├── validation/ # record + quality gate
│ ├── export/ # CSV/XLSX/summary writers
│ ├── browser/ # browser lifecycle + proxy
│ └── utils/ # normalize, logging, retry, dns cache
├── tests/ # pytest suite (mocked)
└── output/ # runtime outputs (git-ignored)
| Symptom | Fix |
|---|---|
Playwright is not installed |
Run ./setup.sh (or python -m playwright install chromium) |
| Config error message | Reads the message — it names the exact key, bad value, and safe range |
| Job seems stuck | Check scraper.log; every stage has a timeout, nothing hangs forever |
| Duplicate records after restart | Should not happen — ensure you're using the same output/ dir |
| Memory creeping up | Lower concurrency.*, keep browser_restart_after_queries small |
found 0 listing place URLs on an EU VPS |
The GDPR consent wall hid the feed — the collector now auto-dismisses it; git pull and retry |
- Google Maps and external sites change their markup; the collector uses
layered (primary → alternate → fallback) selectors and returns
N/Arather than guessing. - CAPTCHA / anti-bot challenges are classified and preserved, never bypassed, and never mistaken for a dead website.
- Wappalyzer tech detection uses
wappalyzer-python3(maintained fork, 1,400+ fingerprints), with a built-in regex fallback if it's ever unavailable. - No paid APIs or third-party verification services required.