Scrape UK property listings from Zoopla (for-sale or to-rent) — price, address, bedrooms, agent phone, floor plan, EPC, photos, full description. Export to JSON or CSV. We handle the blocks so your dataset stays clean.
Fix: first cloud run (Sfgpktp0siTm1O1Lk, build 0.0.1) reached the
search page and produced zero rows. Diagnosis + fix:
Pinned the proxy to RESIDENTIAL + apifyProxyCountry: "GB"
(.actor/input_schema.json, src/models.py default, and
src/browser.py::PROXY_COUNTRY_CODE, hardcoded — never read from
user input, mirrors the 99acres/funda single-market pattern). The
account is now on the STARTER plan; the old FREE-tier
RESIDENTIAL availableCount: 0 reasoning that justified an
unpinned default no longer applies, and a geo-random exit was the
likely cause of the empty/mismatched search page.
Added a zero-rows diagnostic: the last-fetched search page's raw
HTML is now captured in a debug_sink threaded through
scraper.scrape() → _try_once() → _fetch_search_hits(), and
dumped to the KVS key EMPTY_SEARCH_PAGE_HTML by
main._dump_empty_search_debug() before the run fails loud —
mirrors funda-netherlands-real-estate-scraper's
EMPTY_SEARCH_DEBUG_KEY pattern so a block vs. a parse-miss is
diagnosable from the KVS instead of guessed at.
Kept the existing no-geoip=True / static-locale Camoufox launch
pattern (proven fix, see src/browser.py module docstring and the
ai-overview-citations/vrbo-vacation-rentals-scraper precedent) —
pinning the proxy's own exit country is the safe way to fix the
locale/exit-geo mismatch the geoip=True warning flags, without
reintroducing the diagnosed pre-launch IP-echo-sweep crash risk.
Root cause of the zero rows, confirmed via the diagnostic above (cloud
run YmaJV5GMdj75QRo8O, real search-page HTML pulled from KVS): the
proxy fix cleared Cloudflare fine (a genuine 628 KB Zoopla page came
back, no challenge markers), but the search-card parser was wrong.
Zoopla now serves the Next.js App Router — there is no
<script id="__NEXT_DATA__"> blob (RSC streaming instead), and the
DOM shape the original hand-guessed selectors targeted
([data-testid="search-result"], data-listing-id, etc.) does not
exist on the live site. Rewrote src/parsers/search.py::parse_search_dom
against the real shape: cards live in
[data-testid="regular-listings"] div[id^="listing_"], price/beds/
baths/sqft/tenure are extracted from the row's own text via regex
(CSS module class names are hashed per build and not selected on),
address comes from the semantic <address> tag, and listing date
from <time datetime>. parse_search_json stays as a defensive
first try for a hypothetical legacy page. tests/fixtures/search_page.html
replaced with a fixture mirroring the confirmed real shape.
Added a second, unconditional diagnostic
(main._dump_detail_parse_miss_debug, KVS key DETAIL_PARSE_MISS_HTML)
that captures the first unparseable detail page in a run — detail-page
DOM has not been live-recon'd yet (still hand-built/spec-guessed), so
enrichDetails=True rows may currently degrade to search-card-only
fields (tracked gap, not a blocker: fault-isolated per
reference-fleet-fault-isolation-pattern, never fails the run).
0.0 — 2026-08-13
Scaffolded skeleton (T01). Not yet a working scraper — see
docs/specs/zoopla-uk-property-scraper/tasks.md for the build order
(T03 models, T04 Camoufox browser infra, T05 parser, T06 scraper
orchestration, T07 real main.py are the remaining hard gates).