A Python web scraper built from scratch with Requests, BeautifulSoup, and Playwright, then tested against protected sites and a documented rate limit to show where a self-hosted scraper stops working. Includes the same targets retrieved through Zenrows Fetch for comparison.
This repository accompanies the Zenrows article How to Build a Self-Hosted Web Scraper From Scratch.
- Static page scraper using Requests and BeautifulSoup with rotating user agents, full browser header sets, and exponential backoff retries
- Randomized delays between requests to avoid frequency-based blocks
- Structured extraction into dataclasses, written out to CSV
- Playwright headless browser rendering for pages that populate their content with JavaScript
- Diagnostic tests against three protected sites, showing Cloudflare challenge, Varnish edge block, JavaScript challenge, and DataDome responses
- A rate-limit test against GitHub's documented 60 requests per hour for unauthenticated clients
- The same protected targets retrieved through Zenrows Fetch with
mode=auto - Every test runs as its own stage, so you can run one step at a time
- Python 3.10 or above
- A Zenrows API key for
zenrows_fetch.pyonly, available on the free plan - Chromium, installed through Playwright during setup
git clone https://github.com/ZenRows/self-hosted-web-scraper.git
cd self-hosted-web-scraperpython3 -m venv .venv# macOS and Linux
source .venv/bin/activate
# Windows
.venv\Scripts\activatepip install -r requirements.txtpython3 -m playwright install chromiumCopy .env.example to .env in the project root and add your key:
cp .env.example .envZENROWS_API_KEY=your_zenrows_api_key
Only zenrows_fetch.py reads this. scraper.py runs without any credentials.
Get a key by signing up for Zenrows. The free plan includes 5,000 credits every month, with no credit card required.
.
├── scraper.py
├── zenrows_fetch.py
├── requirements.txt
├── .env.example
├── .gitignore
├── LICENSE
└── README.md
scraper.pyis the full build: static scraping, Playwright rendering, the protected site tests, and the rate-limit test, each as a stagezenrows_fetch.pyretrieves the same protected targets through Zenrows Fetch.env.exampleis the template for your.envfile
The project moves through four stages, each one testing the scraper against a harder target.
Static HTML page
↓
Requests + BeautifulSoup → works
↓
JavaScript-rendered page
↓
Requests returns empty fields → Playwright renders → works
↓
Protected pages (Cloudflare, Varnish, DataDome)
↓
Requests returns 403 → Playwright times out, gets a challenge page, or returns a shell
↓
Rate-limited endpoint
↓
59 requests succeed, the 60th returns 403
↓
Zenrows Fetch (mode=auto) → full content on the protected targets
The parsing logic stays the same throughout. Only the fetch layer changes between stages.
Name the stage to run. With no argument, scraper.py runs static.
python3 scraper.py static # scrape three pages of the demo store into products.csv
python3 scraper.py dynamic # Requests on the JavaScript-rendered page (empty fields)
python3 scraper.py rendered # Playwright on the JavaScript-rendered page
python3 scraper.py protected # Requests on the three protected targets
python3 scraper.py ratelimit # up to 70 requests to GitHub's API until the hourly limit hits
python3 scraper.py browser # Playwright on the three protected targetspython3 zenrows_fetch.pypython3 scraper.py static writes products.csv to the project root with the name, price, and URL of every product scraped across three pages. The other stages print their results to the terminal: empty fields from Requests on the JavaScript page, the rendered products from Playwright, 403s and challenge pages from the protected targets, and the rate-limit cutoff.
zenrows_fetch.py prints the response size and page title for each protected target.
Responses from live sites vary by IP, session, time, and each site's configuration, so your output may not match the article exactly.
- Python
- Requests
- BeautifulSoup
- Playwright
- Zenrows
This repository accompanies the Zenrows article:
How to Build a Self-Hosted Web Scraper From Scratch