DEV Community

Anakin
Anakin

Posted on

Building an E-Commerce Web Data Pipeline That Does Not Lie to You

You scrape a competitor product page, load the price into a dashboard, and everything looks fine. Then someone asks why the alert fired at 3 a.m. for a “70% price drop” that never happened. The page changed currency based on location, your parser grabbed the crossed-out price, and the pipeline treated bad data as a market signal.

Public web data is useful in e-commerce, but the hard part is not just collecting it. The hard part is making sure it means what you think it means.

Treat public web data as an unreliable input

E-commerce teams use public web data for common jobs:

  • Price and promotion monitoring
  • Product assortment tracking
  • Stock availability checks
  • Review and rating analysis
  • Search result and marketplace ranking observation
  • Ad placement verification

All of those sound straightforward until you look at the actual pages.

The same product may have different prices by region, login state, device type, or shipping destination. A product card may show a sale price, list price, unit price, subscription price, and marketplace seller price in the same DOM subtree. Availability can mean “in stock online,” “available for pickup,” or “ships in 3 weeks.”

If you store only price: 19.99, you lose the context needed to debug later.

A better record looks more like this:

{
  "url": "https://example.com/products/sku-123",
  "sku": "sku-123",
  "title": "Example Running Shoe",
  "price": 79.99,
  "currency": "USD",
  "price_type": "sale",
  "availability": "in_stock",
  "seller": "Example Store",
  "country": "US",
  "captured_at": "2026-08-18T12:30:00Z",
  "source": "product_page",
  "raw_hash": "f4b8..."
}
Enter fullscreen mode Exit fullscreen mode

That extra context makes the data less convenient to model, but much easier to trust.

Parse for evidence, not just fields

A fragile scraper grabs the first price-looking string from the page. A safer scraper checks structured data first, validates the value, then falls back to HTML selectors only when needed.

Here is a small Python example using JSON-LD and a few basic checks:

import json
import re
from decimal import Decimal, InvalidOperation

import requests
from bs4 import BeautifulSoup

PRICE_RE = re.compile(r"[^0-9.]")


def parse_decimal(value):
    if value is None:
        return None
    cleaned = PRICE_RE.sub("", str(value))
    try:
        return Decimal(cleaned)
    except InvalidOperation:
        return None


def extract_product(url):
    response = requests.get(
        url,
        headers={"User-Agent": "Mozilla/5.0 price-monitor/1.0"},
        timeout=15,
    )
    response.raise_for_status()

    soup = BeautifulSoup(response.text, "html.parser")

    for script in soup.select('script[type="application/ld+json"]'):
        try:
            data = json.loads(script.string or "{}")
        except json.JSONDecodeError:
            continue

        items = data if isinstance(data, list) else [data]
        for item in items:
            if item.get("@type") != "Product":
                continue

            offers = item.get("offers") or {}
            if isinstance(offers, list):
                offers = offers[0] if offers else {}

            price = parse_decimal(offers.get("price"))
            currency = offers.get("priceCurrency")

            if price is None or price <= 0:
                raise ValueError(f"Invalid price parsed from {url}: {offers.get('price')}")

            return {
                "url": url,
                "title": item.get("name"),
                "price": str(price),
                "currency": currency,
                "availability": offers.get("availability"),
                "source": "json_ld"
            }

    raise ValueError(f"No Product JSON-LD found for {url}")
Enter fullscreen mode Exit fullscreen mode

This code is not enough for every store. Some pages render prices client-side. Some expose stale JSON-LD. Some marketplace pages contain multiple products. But the pattern matters: prefer structured evidence, validate what you parse, and fail loudly when the page does not match your assumptions.

A bad failure mode looks like this:

price = None
currency = None
job_status = success
Enter fullscreen mode Exit fullscreen mode

That is how dashboards learn to lie.

Expect blocking, drift, and partial data

Public pages change. Selectors break. Anti-bot systems return CAPTCHAs, 403 responses, or soft blocks where the server returns HTTP 200 with a page that says “verify you are human.” If your collector only checks status codes, it will store the CAPTCHA page as if it were a product page.

Add checks for symptoms:

def looks_blocked(html):
    markers = [
        "captcha",
        "verify you are human",
        "access denied",
        "unusual traffic",
    ]
    text = html.lower()
    return any(marker in text for marker in markers)
Enter fullscreen mode Exit fullscreen mode

Then classify the result:

  • success: parsed and validated
  • blocked: CAPTCHA, access denied, or bot challenge
  • not_found: product removed or URL invalid
  • parse_error: page loaded but schema changed
  • validation_error: parsed data failed business rules

For teams that need managed e-commerce extraction rather than maintaining selectors, retries, and block detection themselves, Wire treats product collection as structured jobs with trackable failures instead of silent CSV gaps.

The important part is the failure taxonomy. Whether you build or buy collection, your downstream systems need to know the difference between “competitor is out of stock” and “our scraper got blocked.”

Dedupe products before comparing prices

Competitor monitoring often fails because teams compare URLs instead of products. The same item may appear under multiple URLs, colors, bundles, sellers, or tracking parameters.

Use a matching key when you have one:

  • GTIN, UPC, EAN, ISBN
  • Manufacturer part number
  • Marketplace ASIN or item ID
  • Brand plus normalized title plus size

Do not over-match. “iPhone 15 case” and “iPhone 15 Pro case” are different products, even if a fuzzy title match says they are close.

For fuzzy matching, store the confidence score and require review around the boundary:

from rapidfuzz import fuzz

score = fuzz.token_set_ratio(
    "Nike Pegasus 41 Mens Running Shoes Black Size 10",
    "Nike Air Zoom Pegasus 41 Men's Road Running Shoe - Black - 10"
)

if score >= 92:
    match_status = "auto_match"
elif score >= 80:
    match_status = "needs_review"
else:
    match_status = "no_match"
Enter fullscreen mode Exit fullscreen mode

A wrong match can be worse than no data because it creates fake price gaps.

Put guardrails around alerts

Real-time competitor data tempts teams to automate every reaction. Be careful. Web data is noisy, and e-commerce pages contain short-lived promotions, personalization, and regional differences.

Before triggering an alert or repricing action, require multiple signals:

  • Same price observed in two consecutive runs
  • Price change exceeds a minimum absolute and percentage threshold
  • Currency and region match your baseline
  • Product match confidence is high
  • Page was not blocked or partially parsed

For scheduled competitor monitoring, Wire can return repeated price and availability snapshots, which still need the same validation rules before they drive alerts or pricing changes.

Public web data works best when you treat it like production telemetry from someone else's system: useful, incomplete, and occasionally wrong.

Start by taking one product category you already track manually. Define the fields, failure states, validation rules, and matching keys before scaling the crawler. If the small pipeline can explain its own bad data, the larger one has a chance.

Top comments (0)