scrapewright
Give it a store URL. It writes the scraper.
Most e-commerce catalog scraping splits into two worlds: sites on a known platform (Shopify, WooCommerce) that expose a clean JSON feed, and everything else — bespoke HTML where you hand-write a parser per site and re-write it every time the markup shifts. scrapewright collapses both into one call:
- Detect the platform behind a URL.
- For known platforms, extract deterministically from their public catalog API — free, stable, no LLM.
- For custom HTML, synthesize a reusable extractor once with an LLM, cache it, and replay it deterministically forever after.
The LLM is a compiler, not a runtime. It runs once per site to produce a recipe of CSS selectors; every page after that is parsed by plain BeautifulSoup at zero marginal cost. That is the whole cost-control story — no per-page model calls, no token bill that scales with your crawl.
┌─────────────┐
store URL ───▶ │ detect │
└──────┬──────┘
┌──────────────────┼──────────────────┐
▼ ▼ ▼
shopify woocommerce generic HTML
products.json wc/store/products (page mode)
│ │ │
│ deterministic │ ▼
│ (free) │ cached recipe? ──yes──▶ replay (free)
└────────┬─────────┘ │ no
▼ ▼
Product{} ◀───── selectors ── JSON-LD? ──yes──▶ Product{} (free)
▲ │ no
│ ▼
└──────── replay ◀── LLM synthesizes recipe ONCE ──▶ cache
Everything normalizes to one Product shape, so downstream code never knows or
cares which path a record came from.
Install
pip install scrapewright # deterministic paths (Shopify, Woo, JSON-LD)
pip install "scrapewright[llm]" # + LLM recipe synthesis for custom HTML
Use it
from scrapewright import Scrapewright
sw = Scrapewright()
# Catalog mode — a whole Shopify/WooCommerce store, deterministically
for product in sw.scrape_catalog("https://shop.example.com", max_items=200):
print(product.brand, product.title, product.price, product.currency)
# Page mode — one custom-HTML product page.
# First call: tries JSON-LD (free); if absent, the LLM writes a recipe once.
# Every later call on that domain: replayed from the cached recipe, no LLM.
item = sw.scrape_page("https://boutique.example.com/products/wool-coat")
print(item.model_dump(exclude={"raw"}))
# Crawl mode — walk a WHOLE custom store from one listing/category URL.
# The frontier discovers product pages (deterministic, free); the first page
# pays the single synthesis cost, every other page replays the recipe.
for product in sw.crawl("https://boutique.example.com/collection", max_items=100):
print(product.title, product.price)
CLI
scrapewright detect https://shop.example.com # what platform is this?
scrapewright run https://shop.example.com --max 50 # scrape a catalog → JSONL
scrapewright crawl https://boutique.example.com/collection -o products.xlsx
scrapewright run https://shop.example.com -o products.csv # Excel-ready CSV
scrapewright add https://boutique.example.com/products/coat # learn a site
scrapewright run https://boutique.example.com/products/coat --no-llm
scrapewright list # cached recipe domains
-o writes .csv (Excel-ready, UTF-8 BOM), .xlsx (pip install scrapewright[excel]),
or .jsonl; without it, products stream to stdout as JSONL.
The Product shape
url: str # canonical product URL
title: str
brand: str | None
price: Decimal | None # parsed from "1,250.00" / "1.250,00" / "€1290" alike
currency: str | None
available: bool | None
images: list[str] # absolute URLs
sizes: list[str]
description: str | None
sku: str | None
source_platform: str # shopify | woocommerce | json-ld | selector
A record is usable when it carries a title, a price, and a URL. The
validator (scrapewright.coverage) reports the usable ratio across a batch —
the number a recipe is trusted on before it's cached.
How the pieces fit
| Module | Role |
|---|---|
detect |
Platform probe: Shopify → WooCommerce → generic |
extract/shopify, extract/woocommerce |
Deterministic catalog extractors |
extract/jsonld |
schema.org/Product from <script type="application/ld+json"> — free, ~common |
extract/llm |
Synthesizes a SelectorRecipe from HTML — the one-time compile step |
extract/selectors |
Replays a recipe with BeautifulSoup — the deterministic runtime |
crawl |
Frontier: turns one listing URL into product URLs (pattern match + card-template fallback + pagination) — deterministic, no LLM |
cache |
Persists recipes keyed by domain, so the compile happens once |
validate |
Field-coverage scoring |
export |
Batch → .csv / .xlsx / .jsonl |
pipeline |
Orchestrates detect → extract → validate → cache → heal |
Design notes
- Deterministic paths run first. Shopify JSON, the WooCommerce Store API, and JSON-LD cover a large share of real stores for free. The LLM is only ever reached for genuinely custom HTML.
- Self-healing. When a cached recipe stops producing usable products — the site changed its DOM — the page falls through to the free JSON-LD path and, failing that, a fresh synthesis replaces the stale recipe. A broken site heals on the next run instead of silently returning empty fields.
- Bounded model spend. Batch and crawl runs cap LLM calls at
max_synth_per_run(default 3) — a site that resists synthesis cannot burn one model call per page. The bill is bounded no matter how large the crawl. - Provider-configurable. The LLM extractor takes a
modeland works with any injected client; the default targets Anthropic's Claude via the official SDK.
Testing
The deterministic paths are fully covered by offline fixtures — no network, no model calls — so CI is green without an API key:
pip install "scrapewright[dev]"
pytest
Status
v0.2 (alpha). Implemented and tested: catalog extraction (Shopify, WooCommerce), page extraction (JSON-LD, LLM-synthesized selectors), recipe caching, self-healing re-synthesis with a bounded per-run model budget, a crawl frontier (one listing URL → the whole store), coverage validation, and CSV / XLSX / JSONL export.
Roadmap: schema-agnostic extraction (bring your own field schema — the same compile-once/replay-free loop for any structured site, not just product pages), BigCommerce / Salesforce Commerce detectors, and JS-rendered-page support via an optional Playwright fetcher.
License
MIT — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file scrapewright-0.2.0.tar.gz.
File metadata
- Download URL: scrapewright-0.2.0.tar.gz
- Upload date:
- Size: 25.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
428728a5d6f19864ef89ccf23083691e5ccd6ec07d90dd159caf3cf86bcdd4b5
|
|
| MD5 |
7f1b207ec22a4a21f7ccc63213250076
|
|
| BLAKE2b-256 |
ef39cde35124b1cfb44263ab1f529a4f31756c6bfcf246644dcde7c3d22c9c0e
|
File details
Details for the file scrapewright-0.2.0-py3-none-any.whl.
File metadata
- Download URL: scrapewright-0.2.0-py3-none-any.whl
- Upload date:
- Size: 27.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
977a14066546ff954ed8917344188edb3b0c2b566d5343da89abbfa47bfd791a
|
|
| MD5 |
8efa4a7574f537689eb4a21647822f21
|
|
| BLAKE2b-256 |
8a0b6018ba4331b744d0f645b8acf011d844b9528ae62bec17127687e56ff331
|