Automated Web Scraping
Automated web scraping is a scraper that runs by itself: a scheduler starts it, it fetches pages with retries, checks the data against rules (at least 10 rows, every row with a title and a price, no drop of more than 50% from the last run), stores it, and alerts you when something breaks, with no one clicking "run". A common setup is a Python script, a GitHub Actions or cron schedule, a chat webhook for failures and a heartbeat monitor that notices when the job did not run at all. Unattended scrapers need IPs that keep working night after night; ProxyField residential proxies rotate the IP on every request from $1.95/GB, so start with the Easy Trial: 1 GB for $2.95.
- residential IPs
- 412M+
- from, traffic never expires
- $1.95/GB
- Easy Trial
- 1 GB for $2.95
On this page 11 sections
- The answer in five steps
- What is automated web scraping?
- The parts of an automated scraper
- Example: a scraper that checks its data and alerts you
- Schedule it with GitHub Actions or cron
- Validation rules that catch broken runs
- Monitoring and alerts
- Running scrapers in the cloud: the IP question
- Ways to automate web scraping
- What automation costs
- Automated Web Scraping FAQ
The answer in five steps
To make a scraper run on its own, check its data and alert you, build these five pieces:
-
One command that runs once and exits
With status 0 on success and 1 on failure.
-
A scheduler
That starts it: cron on a server, or a GitHub Actions
schedulewith no server (cron syntax, UTC, every 5 minutes at most). -
Validation rules
Inside the script that refuse to save a bad run.
-
A failure alert
A message to Slack, Discord or email when the script raises an error.
-
A heartbeat
A ping to a cron monitor such as Healthchecks.io, Sentry Crons or UptimeRobot after each good run, so you hear about it when the job stops running and sends nothing.
The script and the workflow below do all five.
What is automated web scraping?
Automated web scraping means the whole collection job runs without a person: deciding when to run, fetching, extracting, checking, storing and reporting. A scraper you start by hand each morning is a script; the same scraper started by a scheduler, retrying failures, validating its output and alerting on problems is an automated pipeline.
Manual scraping is a person copying data from pages. Semi-automated scraping is a tool you run by hand, such as a browser extension. Fully automated scraping is a scheduled job that delivers fresh data every hour, day or week, and tells you only when something needs attention.
The parts of an automated scraper
| Part | Job | Typical tools |
|---|---|---|
| Trigger | Start the run on time, or when an event happens | cron, Windows Task Scheduler, GitHub Actions, cloud schedulers, Apache Airflow |
| Fetcher | Download pages; retry timeouts and 429/5xx errors with backoff | Requests with a retry adapter, Scrapy, Playwright |
| IP rotation | Spread requests so no single IP hits the site's limits | A rotating proxy gateway |
| Extractor | Pick the fields out of each page | Beautiful Soup, Scrapy selectors, Playwright locators |
| Validator | Check counts, types, ranges and drift; stop bad data getting in | Assertions in the script, schema checks |
| Storage | Keep each run with a timestamp | CSV, a database, object storage |
| Failure alert | Tell you when a run fails or the data looks wrong | Slack or Discord webhooks, email |
| Heartbeat | Tell you when no run happened | Healthchecks.io, Sentry Crons, UptimeRobot |
Example: a scraper that checks its data and alerts you
This Python script fetches a page through the ProxyField gateway with automatic retries, validates the rows against five rules, saves the page that broke the parser, posts an alert to a webhook on any failure, and pings a heartbeat URL only after a good run. It targets books.toscrape.com, a public scraping sandbox, so you can try it before you point it at your own pages. Secrets come from environment variables, never from the code.
# pip install requests beautifulsoup4
import csv, datetime, json, os, pathlib, requests
from bs4 import BeautifulSoup
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
URL = "https://books.toscrape.com/"
PROXY = os.environ.get("PROXY_URL") # http://<login>-country-us:<password>@gate.proxyfield.io:7000
WEBHOOK = os.environ.get("ALERT_WEBHOOK") # a Slack incoming webhook URL
HEARTBEAT = os.environ.get("HEARTBEAT_URL") # e.g. https://hc-ping.com/<your-check-uuid>
STATE = pathlib.Path("last_run.json")
session = requests.Session()
if PROXY:
session.proxies = {"http": PROXY, "https": PROXY}
retry = Retry(total=3, backoff_factor=2, status_forcelist=[429, 500, 502, 503, 504])
session.mount("https://", HTTPAdapter(max_retries=retry))
def alert(message):
if WEBHOOK:
requests.post(WEBHOOK, json={"text": message}, timeout=10) # Discord webhooks take "content"
if HEARTBEAT:
requests.post(HEARTBEAT + "/fail", data=message, timeout=10)
def parse(html):
rows = []
for card in BeautifulSoup(html, "html.parser").select("article.product_pod"):
link, price = card.select_one("h3 a"), card.select_one("p.price_color")
rows.append({"title": link.get("title") if link else None,
"price": float(price.get_text(strip=True).lstrip("£")) if price else None})
return rows
def validate(rows):
errors = []
if len(rows) < 10:
errors.append(f"only {len(rows)} rows, expected at least 10")
bad = [r for r in rows if not r["title"] or r["price"] is None or not 0 < r["price"] < 1000]
if bad:
errors.append(f"{len(bad)} rows without a title or with a price outside 0-1000")
if STATE.exists():
last = json.loads(STATE.read_text())["rows"]
if len(rows) < last * 0.5:
errors.append(f"{len(rows)} rows against {last} last run: a drop of more than 50%")
return errors
try:
response = session.get(URL, timeout=30)
response.raise_for_status()
rows = parse(response.content)
errors = validate(rows)
if errors:
pathlib.Path("failed.html").write_bytes(response.content) # the page that broke the run
raise ValueError("; ".join(errors))
except Exception as error:
alert(f"scraper failed: {error}")
raise SystemExit(1) # a non-zero exit fails the cron job or the workflow run
today = datetime.date.today().isoformat()
with open("prices.csv", "a", newline="") as f:
csv.writer(f).writerows([today, r["title"], r["price"]] for r in rows)
STATE.write_text(json.dumps({"date": today, "rows": len(rows)}))
if HEARTBEAT:
requests.get(HEARTBEAT, timeout=10) # "ran, and the data passed": silence after this means troubleWhat each part does when something breaks:
| What breaks | What the script does | What you get |
|---|---|---|
| The site times out or answers 429, 500, 502, 503 or 504 | Retries 3 times, waiting longer each time | Nothing, if a retry works |
| The layout changes and the selectors find nothing | validate stops the run; failed.html is saved | A webhook message and a failed heartbeat |
| Prices come back as blanks or nonsense | The range rule (0 to 1,000) fails | The same |
| Half the products disappear | The 50% drop rule fails | The same |
| The server is off or the schedule is deleted | Nothing runs | The heartbeat service alerts you when the expected ping does not arrive |
Requests does not retry failed requests by default; the mounted adapter adds it. With a rotating gateway, each retry also leaves from a new IP, unless you set a sticky session in the username (-session-<id>).
Schedule it with GitHub Actions or cron
GitHub Actions runs the script on GitHub's machines, with no server of your own. Put the three secrets (PROXY_URL, ALERT_WEBHOOK, HEARTBEAT_URL) in the repository's Actions secrets and add this file:
# .github/workflows/scrape.yml
name: scrape
on:
schedule:
- cron: "17 6 * * *" # 06:17 UTC every day
workflow_dispatch: {} # a "Run workflow" button for tests
jobs:
scrape:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with: { python-version: "3.12" }
- run: pip install requests beautifulsoup4
- run: python scrape.py
env:
PROXY_URL: ${{ secrets.PROXY_URL }}
ALERT_WEBHOOK: ${{ secrets.ALERT_WEBHOOK }}
HEARTBEAT_URL: ${{ secrets.HEARTBEAT_URL }}
- if: failure()
uses: actions/upload-artifact@v7
with: { name: failed-page, path: failed.html, retention-days: 7 }
- uses: actions/upload-artifact@v7
with: { name: prices, path: prices.csv }What to expect from the schedule, per GitHub's workflow syntax reference and events reference:
- The cron line has five fields (minute, hour, day of month, month, day of week) and runs in UTC unless you add a
timezone, such astimezone: "America/New_York". - The shortest interval is once every 5 minutes, and a run can start late when GitHub is busy, most often at the top of the hour; an off-the-hour minute such as
17helps. - Scheduled workflows run on the latest commit of the default branch.
- In a public repository, scheduled workflows are turned off after 60 days without repository activity.
- When the script exits with 1, the run fails and GitHub notifies the user who last changed the cron line, by email if their notification settings allow it.
- The runner is wiped after each run, so
prices.csvandlast_run.jsonmust go to storage (an artifact, a bucket or a database) if the next run needs them.
cron on a Linux server does the same in one line, with a log:
17 6 * * * cd /home/me/scraper && /usr/bin/python3 scrape.py >> scrape.log 2>&1More schedulers (systemd timers, AWS Lambda with EventBridge, Apache Airflow) and how to stop two runs from overlapping: scheduling web scrapers.
Validation rules that catch broken runs
An automated scraper that saves wrong data silently is worse than one that fails. Write rules with numbers, not intentions:
| Rule | Example threshold | What it catches |
|---|---|---|
| Row count | at least 10 rows on a page that lists 20 | Blocks, empty pages, selectors that no longer match |
| Required fields | every row has a title and a price | Layout changes that move one field |
| Ranges and types | price is a number between 0 and a ceiling you set (1,000 above) | A currency symbol or a "call for price" text parsed as a number |
| Drift | rows not more than 50% below the last run; prices not doubling overnight | Partial pages, a currency or unit switch |
| Freshness | the newest row is dated today | A job that stopped writing without failing |
| Duplicates | one row per stable key, such as the product URL | The same item on two result pages |
Cleaning follows the same rules every run: one currency format, one date format, trimmed text, normalized units. Keep the raw values too, so you can rerun the cleaning if a rule was wrong.
Monitoring and alerts
Two kinds of alert cover every failure:
- Alert on failure. The script's
exceptbranch posts to a chat webhook. A Slack incoming webhook takes{"text": "..."}; a Discord webhook takes{"content": "..."}. Email works too, through the scheduler's own failure notice. - Alert on silence. A heartbeat monitor expects a ping every day (or every hour) and alerts you when one is missing. Healthchecks.io gives each check a URL: a request to
https://hc-ping.com/<uuid>marks a good run and.../faila failed one. Sentry Crons and UptimeRobot offer the same kind of check. Silence catches what no failure alert can: a deleted schedule, an expired server, a workflow GitHub turned off.
Two habits make alerts useful: keep the raw HTML of failed pages for a few days (the workflow keeps failed.html for 7), and track the share of failed requests per site, because a rising share means the site is limiting you or changed.
Running scrapers in the cloud: the IP question
Cloud servers and CI runners make automation easy, but their IP ranges are well known, and many sites score traffic from them as automated and block it or serve CAPTCHAs. A scraper that works from your laptop can fail from a GitHub runner for that reason alone. Route the cloud scraper's requests through rotating proxies: residential proxies for protected sites, rotating datacenter proxies where the site allows datacenter ranges.
ProxyField supports both a username and password and an IP allowlist. GitHub runners and serverless functions change IPs between runs, so they use the username and password from a secret; a fixed server can use the allowlist and keep no proxy password at all.
Ways to automate web scraping
-
A script on a schedule
A Python script started by cron, a systemd timer, Windows Task Scheduler or GitHub Actions. The simplest and most common setup.
-
Cloud functions or containers
The same script as a serverless function or a container, started by the cloud provider's scheduler. Watch time limits (an AWS Lambda run stops at 15 minutes) and the reputation of cloud IP ranges.
-
Workflow orchestrators
Apache Airflow runs multi-step pipelines (scrape, clean, load, report) with dependencies, retries and history. Worth it when you run many scrapers.
-
Frameworks with built-in automation
Scrapy and Crawlee bring retries, throttling, queues and export; you add the trigger.
-
No-code cloud scrapers
Point-and-click apps with a schedule option; see no-code web scrapers.
-
Scraping APIs on a timer
Your scheduler calls an API that returns pages or parsed data; the provider handles fetching and IPs.
What automation costs
An automated scraper costs its compute, its storage and its proxy traffic, and the traffic grows with every run. Estimate it as runs × pages × page size: a daily run over 2,000 pages of 150 KB is about 300 MB a day, or about 9 GB a month. Plain HTML fetches keep it low; browser rendering raises it unless you block images and fonts. A failed run that retries uses traffic too, which is one more reason to stop early when validation fails.
ProxyField residential proxies give a new IP on every request or a sticky session from 30 s to 36 h, run unlimited concurrent threads, and target country, region and city, with a 99.6% success rate, according to our service data. They cost $2.95/GB for 1 GB, $2.79 from 10 GB and $2.65 from 50 GB, down to $1.95/GB from 1,000 GB, with no subscription; 9 GB a month at the 1–9 GB rate is $26.55. The traffic never expires, so a paused schedule wastes nothing; all purchases are final. What each job costs: proxies for web scraping. Whether a site may be scraped: is web scraping legal.
Automated Web Scraping FAQ
How do I automate web scraping?
ProxyField's rotating residential proxies, from $1.95/GB, give an automated scraper a new IP on every request; automating the scraper itself takes 5 steps. Put the scraper in one command that exits with 0 or 1, start it with a scheduler such as cron or GitHub Actions, add validation rules, post failures to a webhook, and ping a heartbeat monitor after every good run.
How can I fully automate a web scraper so it runs on its own, checks its data and alerts me when something breaks?
Schedule the script with GitHub Actions (cron syntax in UTC, every 5 minutes at most) or cron, and make it check its rows before saving, for example at least 10 rows, a title and a price in each, and no drop of more than 50% from the last run. On any failure it posts to a Slack or Discord webhook and exits with 1; after a good run it pings a heartbeat monitor such as Healthchecks.io, which alerts you when the ping does not arrive.
What is web scraping automation?
Running the whole scraping job without a person: a scheduler triggers it, the scraper fetches pages with retries, validates and cleans the data, stores it and reports problems. The result is fresh data on a fixed schedule instead of a script someone remembers to run.
What is the difference between manual and automated web scraping?
Manual scraping is a person copying data from pages, or starting a tool by hand each time. Automated scraping runs on a schedule, recovers from errors and checks its own output, so it scales to thousands of pages and keeps a consistent history.
What is a heartbeat check for a scraper?
A monitor that expects a ping from your scraper on a schedule, for example once a day, and alerts you when the ping is missing. It catches the failures that send no error at all, such as a deleted schedule or a server that is off. Healthchecks.io, Sentry Crons and UptimeRobot offer this kind of check.
What is automated data collection?
Gathering data with software instead of by hand, from websites, APIs, files or sensors, on a schedule or as events happen. Automated web scraping is the part of it that collects data from web pages.
Why is automated data extraction important?
Because the data that matters, such as prices, rankings and stock, changes daily or hourly, and only a scheduled job can follow it. Automation also makes the data consistent from run to run, which you need to compare periods or train models.
How can data validation be automated?
Put rules with numbers into every run: a minimum row count, required fields, value ranges, a maximum drop against the last run and a freshness date. Make the job stop and alert when a rule fails, so broken runs never reach your database.
How can I automate data cleaning?
Write the cleaning rules once and apply them in every run: one currency and number format, one date format, trimmed text, normalized units and deduplication on a stable key such as a product URL. Keep the raw values too, so you can rerun the cleaning if a rule was wrong.
Does Python Requests retry automatically?
No. By default Requests tries each request once. Mount an HTTPAdapter with a urllib3 Retry policy, such as Retry(total=3, backoff_factor=2, status_forcelist=[429, 500, 502, 503, 504]), to retry timeouts and those status codes with a wait that grows after each failure.
Show 1 more questionShow fewer questions
Does automation always improve data quality?
No. Automation repeats whatever the scraper does, including its mistakes, and a layout change can fill a database with blanks overnight. Quality improves only when the automated job validates its output and alerts you when something looks wrong.
Related pages
Easy Trial: 1 GB for $2.95.
Try for the price of 1 GB, no subscription, traffic never expires.