Loading...

Data Scraping & Sync

A Price Scraper That Survives The Site Changing Its Markup

Scheduled competitor price collection into Postgres, with proxy rotation, schema validation, and an alert when a selector silently stops matching.

Python / FastAPIAdvancedA day for the first target, an hour for each one afterUpdated July 30, 2026
How the data moves
01ScheduleCron inside the container, staggered per target
02FetchScrapingBee renders and rotates the exit IP
03ExtractPer-site parser returns a typed record
04ValidatePydantic rejects nulls and impossible prices
05UpsertPostgres history table, one row per observation

Python · Playwright · ScrapingBee · Supabase · Docker

View GitHub Repo: MIT licensed. The repository is the skeleton; the gated bundle adds the working parsers and the deploy files.

Copy Docker Compose: Redis holds the per-target rate limit and the last-seen hash, so a restart does not re-scrape everything.

The Problem

Someone checks four competitor sites every Monday and pastes prices into a spreadsheet. The obvious fix is a scraper, and the obvious scraper works for three weeks, then a site ships a redesign and it starts writing nulls that nobody notices until the sheet is useless.

Prerequisites & Tool Stack

Everything this blueprint calls. Check you have them before you start - the usual reason a build stalls halfway is a key that takes a day to get approved.

ScrapingBee API key

Handles rendering and proxy rotation as one call. Running your own browser fleet is a second project, not a step in this one.

Free tier: 1,000 credits

Postgres database

Stores an observation history rather than the current price, which is what makes trend queries possible later.

Free tier: Two projects, 500MB each

A VPS to run the container

Scheduled scraping wants a long-running host. Serverless timeouts and cold starts fight this workload.

Free tier: From about $4.50 a month

Only if you need it

Residential proxies

Only for targets that block datacentre ranges outright. Most do not - try without first.

Some links above are partner links and we earn a commission if you sign up through them. It costs you nothing extra, and it does not decide what goes in a blueprint - the self-hosted option is recommended wherever it is genuinely the better call.

What It Costs To Run

Monthly, at the volumes this blueprint was tested against. Worth comparing against what the manual version of this work costs you in hours.

ComponentCostAt what volume
ScrapingBee~$29/moFreelance plan, roughly 4 targets checked hourly
Hetzner CX22$4.59/moContainer plus Redis
Supabase$0 - $25/moFree until the history table gets large

The files

Get the working parsers and deploy bundle

The repository shows the structure. This bundle is the part that takes the day: real parsers, the validation schema, and the alerting that catches a silent break.

Four working site parsers, written against live markup

.env template with every key the container reads

Pydantic schemas and the SQL for the history table

The selector-drift alert, which is the reason this keeps working

The download link arrives by email and works for 30 days. We send notes on new blueprints, roughly monthly, and one click unsubscribes. See ourprivacy policy.

Technical Breakdown

The decisions that matter, in the order you will meet them. Everything here is a thing that broke in testing before it was a rule.

01.One parser per target, one interface

Each site gets its own module returning the same typed record. Shared "smart" extraction across sites looks elegant for two targets and becomes unmaintainable at five, because the exception handling for one site leaks into all of them.

python
from decimal import Decimal
from pydantic import BaseModel, field_validator

class Observation(BaseModel):
    sku: str
    price: Decimal
    currency: str
    in_stock: bool
    source_url: str

    @field_validator("price")
    @classmethod
    def sane_price(cls, v: Decimal) -> Decimal:
        # A selector that stops matching returns 0, not an exception.
        # Without this the pipeline writes zeroes for a week in silence.
        if v <= 0 or v > Decimal("1000000"):
            raise ValueError(f"implausible price: {v}")
        return v

02.Validate before you write, not after

Every extraction goes through the schema. A site redesign then surfaces as a validation error with a URL attached, rather than as a column of zeroes discovered a fortnight later by someone building a chart.

03.Alert on the extraction rate, not on exceptions

A scraper rarely fails loudly. It fails by matching nothing. Track the share of fields successfully extracted per target per run, and page someone when it drops below 90 percent of the trailing average.

python
ratio = extracted / max(attempted, 1)
if ratio < 0.9 * trailing_average(target, days=7):
    alert(
        f"{target}: extraction dropped to {ratio:.0%} "
        f"({extracted}/{attempted}) - selectors likely stale"
    )

04.Store observations, never current state

Append a row per check with a timestamp. Updating one row in place makes today cheap and makes every historical question - when did they drop the price, how often do they change it - impossible to answer.

05.Stagger, back off, and respect robots

Randomised delays per target, exponential backoff on 429 and 503, and a hard daily cap per host. Politeness here is also self-interest: the fastest way to lose a data source is to make yourself expensive to serve.

When It Goes Wrong

The difference between a demo and something you can leave running is entirely in this table. Every row is a failure the blueprint handles explicitly rather than hoping about.

FailureWhat happens
Target returns 403Retry once through a residential exit, then mark the run degraded and alert. No silent skip.
Selector matches nothingValidation rejects the record, the run is logged as a partial, and the drift alert fires on the trend.
Postgres unreachableObservations buffer to Redis and flush on the next successful connection.
Container restarts mid-runPer-target last-success timestamp in Redis, so the schedule resumes rather than restarting the sweep.

Payload Example

One row written per observation.

json
{
  "target": "competitor-a",
  "sku": "NW-4410-BLK",
  "price": "289.00",
  "currency": "USD",
  "in_stock": true,
  "source_url": "https://example.com/p/nw-4410",
  "observed_at": "2026-07-30T06:14:02Z",
  "extraction_ratio": 1.0,
  "via": "scrapingbee:us"
}
On this page

Download the files


Platform: Python / FastAPI

Category: Data Scraping & Sync

Level: Advanced — You deploy things. Docker, DNS, and logs are familiar ground.

Build time: A day for the first target, an hour for each one after

Not sure this is the right platform?

For work a visual editor handles badly: parsing, retries at volume, anything CPU-bound.

Browse All Blueprints

Done-For-You

Need this pointed at targets that fight back?

Public catalogue pages are the easy case. Logged-in pricing, regional variants, and sites with active bot defences are a different piece of work, and one worth scoping properly.

A TechZapp sprint is a fixed-scope, fixed-price week. A senior engineer builds it in your environment, hands over the repository and the runbook, and you own every part of it afterwards. No platform of ours to keep paying for.

What the sprint covers

Parsers written and tested against your actual targets, however awkward

Deployed with monitoring, drift alerts, and a dashboard your team can read

A written note on the legal and robots position for each target before we start

Sprint pricing

$1,500 - $2,000

One sprint per four targets


Fixed scope agreed before we start

Built in your environment, not ours

Repository, infrastructure, and runbook handed over

Two weeks of support after handover included

Scope This Sprint

Tell us what you are integrating with. We reply within one business day, and we will say plainly if the blueprint above already covers it.