Releases: 2scraper/woolworths-scraper
Release list
v0.1.1 — category mode fixed on the Playwright engine
--mode categorywas broken in v0.1.0 on the Playwright engine, the one
the README recommends. It died on its first API call with
AttributeError: 'Page' object has no attribute 'page'. Search mode was
unaffected. Upgrade if you used v0.1.0 for anything under/shop/browse/.
Fixed
_resolve_target passed a page where _fetch_api expects a session. The
retry work in v0.1.0 changed that signature and landed in two engines out of
three; the verification afterwards used a search URL, which never reaches the
line, so it shipped.
Added
A check comparing the argument spelling of same-named helpers across the
three engines. Nothing existing could catch this one: arity was identical so
the call bound fine, compileall and the undefined-name walk passed, and the
shared-module signature check does not cover a module's own helpers — where
inspect.signature cannot help anyway, because session and session.page
are the same arity.
It immediately found a second divergence: _read_tiles took a page on
Playwright and a session on its twins. All three now take the session.
Verified
Every engine against both modes, which is what should have been run after
the original change:
| engine | search | category |
|---|---|---|
| playwright | 76 rows | 81 rows |
| selenium | 80 rows | 81 rows |
| puppeteer | 76 rows | 81 rows |
All complete, all exit 0. 75 offline checks.
v0.1.0 — rebuilt on the family architecture
First release on the 2scraper family architecture. The previous contents of
this repository were three standalone single-file scripts sharing no row
schema, no exit codes and no tests with the rest of the family; nothing of
that generation remains.
If you used the previous scripts, read this. They advertised
rating,
rating_count,review_countandcountry_of_origin. All four are gone,
because all four were null on every row — Woolworths ships aRating
object on every product and it is empty on every product (0 across 660
listing rows and 10 detail rows), andcountryoforiginis present on all
660 rows and null on all 660. In exchange,health_star_rating,
ingredients,allergy_statement,dietary_claimsand
storage_instructionsare new and real: they live in
AdditionalAttributesrather than at the top level, which is why the
previous generation promised them and could not fill them.
What this site turned out to be
Woolworths puts no product data in its HTML. A served category page is
872 KB whose visible text is 3,735 characters of navigation chrome; "Banana"
appears zero times on /shop/browse/fruit-veg, there are zero
/shop/productdetails/ anchors, and the only JSON-LD on a category page is a
BreadcrumbList. The catalogue arrives as JSON over the site's own API and
renders into open shadow roots that page.content() does not serialise.
So the engines navigate once — which is what makes Akamai issue a session
— and then ask /apis/ui/... for each page from inside that loaded page.
Three traps that would each have shipped a silently wrong answer
| measured 2026-09-16 | |
|---|---|
| headful vs headless | served 4/4 vs refused 4/4 (Akamai 403), one datacentre address |
navigator.webdriver |
without the automation flag 3/3 runs bounced to /unauthorisederror; with it 3/3 stayed and rendered 36–39 tiles |
WasPrice == Price |
537 of 660 rows — reading it straight puts a 0% discount on four products in five |
The middle one is the nastiest: the document and the API both keep
answering 200 with real products, so an unfixed run returns the right rows
and reports success while the grid is gone.
Two markers that looked obviously right were caught by counting them on a
page known to be good: akamai appears once on every page the site serves
and zero times on its denial page, and couldn't find any appears four times
on every page — it made a 2,205-result search report itself empty before it
was removed.
Verified live
- All three engines agree: 79 rows each on one 2-page search, same columns in
the same order, every non-volatile value identical. - A 17-page run returned 575 rows — exactly the total the site states —
and stopped when page 17 came back as ads only. - Scraping Browser (
--cdp-endpoint, US and AU exits) and--fingerprint
exercised with a real key. An Australian exit is not required. --concurrencyfetched pages out of order and merged them in page order.- The container scraped a live search and wrote 75 products.
Not included, with the measurement
- No Scraper API engine. The path works (HTTP 200, 619 KB, not blocked)
and returns zero products, because it returns served HTML and this
site's served HTML has no catalogue in it. That is the site, not the
product. - No in-store price column: identical to
Priceon 656 of 656 rows. - No product-detail mode: the endpoint returns the object already on
every listing row.
Captcha
No captcha has been observed on this site — sitekey, recaptcha,
hcaptcha and turnstile are each 0 on every served page and on both denial
pages — and Akamai's refusal carries no widget. No solve is attempted and
nothing is charged. That is a statement about the page, not about the
product: captcha_solver.py implements reCAPTCHA v2/v3, enterprise reCAPTCHA
(RecaptchaV2EnterpriseTaskProxyless) and Cloudflare Turnstile
(TurnstileTaskProxyless).
Checks
74 offline checks, passing with no engine library installed and in each
engine's own virtualenv. CI runs the offline suite on Python 3.9 and 3.12,
each engine in its own venv, and builds and runs the Docker image. The whole
history was scanned before release: 50 blobs across 82 objects, nothing
credential-shaped.