Tutorial10 min read

Automated Web Scraping

Short answer

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
  1. The answer in five steps
  2. What is automated web scraping?
  3. The parts of an automated scraper
  4. Example: a scraper that checks its data and alerts you
  5. Schedule it with GitHub Actions or cron
  6. Validation rules that catch broken runs
  7. Monitoring and alerts
  8. Running scrapers in the cloud: the IP question
  9. Ways to automate web scraping
  10. What automation costs
  11. 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:

  1. One command that runs once and exits

    With status 0 on success and 1 on failure.

  2. A scheduler

    That starts it: cron on a server, or a GitHub Actions schedule with no server (cron syntax, UTC, every 5 minutes at most).

  3. Validation rules

    Inside the script that refuse to save a bad run.

  4. A failure alert

    A message to Slack, Discord or email when the script raises an error.

  5. 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

PartJobTypical tools
TriggerStart the run on time, or when an event happenscron, Windows Task Scheduler, GitHub Actions, cloud schedulers, Apache Airflow
FetcherDownload pages; retry timeouts and 429/5xx errors with backoffRequests with a retry adapter, Scrapy, Playwright
IP rotationSpread requests so no single IP hits the site's limitsA rotating proxy gateway
ExtractorPick the fields out of each pageBeautiful Soup, Scrapy selectors, Playwright locators
ValidatorCheck counts, types, ranges and drift; stop bad data getting inAssertions in the script, schema checks
StorageKeep each run with a timestampCSV, a database, object storage
Failure alertTell you when a run fails or the data looks wrongSlack or Discord webhooks, email
HeartbeatTell you when no run happenedHealthchecks.io, Sentry Crons, UptimeRobot
Easy Trial: 1 GB for $2.95.
Try for the price of 1 GB, no subscription, traffic never expires.

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.

Python
# 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 trouble

What each part does when something breaks:

What breaksWhat the script doesWhat you get
The site times out or answers 429, 500, 502, 503 or 504Retries 3 times, waiting longer each timeNothing, if a retry works
The layout changes and the selectors find nothingvalidate stops the run; failed.html is savedA webhook message and a failed heartbeat
Prices come back as blanks or nonsenseThe range rule (0 to 1,000) failsThe same
Half the products disappearThe 50% drop rule failsThe same
The server is off or the schedule is deletedNothing runsThe 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:

YAML
# .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 as timezone: "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 17 helps.
  • 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.csv and last_run.json must 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>&1

More 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:

RuleExample thresholdWhat it catches
Row countat least 10 rows on a page that lists 20Blocks, empty pages, selectors that no longer match
Required fieldsevery row has a title and a priceLayout changes that move one field
Ranges and typesprice 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
Driftrows not more than 50% below the last run; prices not doubling overnightPartial pages, a currency or unit switch
Freshnessthe newest row is dated todayA job that stopped writing without failing
Duplicatesone row per stable key, such as the product URLThe 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 except branch 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 .../fail a 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

  1. 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.

  2. 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.

  3. Workflow orchestrators

    Apache Airflow runs multi-step pipelines (scrape, clean, load, report) with dependencies, retries and history. Worth it when you run many scrapers.

  4. Frameworks with built-in automation

    Scrapy and Crawlee bring retries, throttling, queues and export; you add the trigger.

  5. No-code cloud scrapers

    Point-and-click apps with a schedule option; see no-code web scrapers.

  6. 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.

Easy Trial: 1 GB for $2.95.

Try for the price of 1 GB, no subscription, traffic never expires.