ok
This commit is contained in:
parent
c2070693fb
commit
909e241907
55 changed files with 594 additions and 223 deletions
|
|
@ -36,7 +36,7 @@ _MAX_INDEX = 1008
|
|||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# The search API (_paginate) only returns an outcode-level `displayAddress`
|
||||
# (e.g. "Akerman Road, Brixton, London, SW9") — never the full postcode. Each
|
||||
# (e.g. "Akerman Road, Brixton, London, SW9"), never the full postcode. Each
|
||||
# listing's detail page, however, embeds the property's OWN full postcode in a
|
||||
# `window.__PAGE_MODEL` script as `propertyData.address.{outcode, incode}`
|
||||
# (e.g. outcode "SW9" + incode "0HD" → "SW9 0HD"), independently corroborated by
|
||||
|
|
@ -51,8 +51,8 @@ _MAX_INDEX = 1008
|
|||
# __PAGE_MODEL is a "devalue"-style flattened object graph: its `data` field is
|
||||
# a JSON STRING holding a flat array where every integer inside a container is
|
||||
# an index reference into that same array (so the graph can dedupe). We
|
||||
# brace-match the (large, deeply-nested) object literal — a non-greedy regex
|
||||
# cannot — then rehydrate the reference graph before reading the address.
|
||||
# brace-match the (large, deeply-nested) object literal (a non-greedy regex
|
||||
# cannot), then rehydrate the reference graph before reading the address.
|
||||
|
||||
_PAGE_MODEL_RE = re.compile(r"window\.__PAGE_MODEL\s*=\s*")
|
||||
|
||||
|
|
@ -128,7 +128,7 @@ def parse_detail_postcode(html: str) -> str | None:
|
|||
Pure and network-free so it is unit-testable: callers pass the page HTML.
|
||||
Reads ``propertyData.address.outcode`` + ``.incode`` from window.__PAGE_MODEL
|
||||
and returns a normalised full postcode (e.g. "SW9 0HD"), or None when the
|
||||
page has no parseable address (the property location wrapper can be empty —
|
||||
page has no parseable address (the property location wrapper can be empty;
|
||||
the caller then keeps the coordinate fallback). The returned outcode is
|
||||
re-validated against the joined postcode so a malformed incode is dropped.
|
||||
"""
|
||||
|
|
@ -193,7 +193,7 @@ def _fetch_detail_postcode(client: httpx.Client, property_id: str) -> str | None
|
|||
"""GET a listing detail page and return its true full postcode (or None).
|
||||
|
||||
Results (including failures) are cached by listing id. The detail page is a
|
||||
plain HTML GET — no Cloudflare, unlike Zoopla — so a single httpx call
|
||||
plain HTML GET (no Cloudflare, unlike Zoopla), so a single httpx call
|
||||
suffices; any error degrades gracefully to the coordinate fallback. Safe to
|
||||
call concurrently: distinct listing ids write distinct cache keys, and the
|
||||
shared RATE_LIMITER spaces the GETs."""
|
||||
|
|
@ -245,7 +245,7 @@ def _needs_detail_fetch(prop: dict) -> bool:
|
|||
Skips listings the search already pins precisely: an "ACCURATE_POINT"
|
||||
``pinType`` means rooftop-exact coordinates, so the coordinate-nearest
|
||||
postcode is trustworthy and the detail page would only confirm it. Listings
|
||||
with an approximate pin — or no ``pinType`` field at all — still get fetched,
|
||||
with an approximate pin (or no ``pinType`` field at all) still get fetched,
|
||||
so this degrades safely to the previous behaviour when the search payload
|
||||
omits ``pinType``."""
|
||||
if not RIGHTMOVE_SKIP_DETAILS_FOR_ACCURATE_PINS:
|
||||
|
|
@ -262,8 +262,8 @@ def _prime_detail_postcodes(
|
|||
) -> None:
|
||||
"""Fill ``_detail_postcode_cache`` for the listings that need a detail page.
|
||||
|
||||
Picks the fresh (uncached, not-skipped) listings — up to ``detail_cap`` per
|
||||
outcode — then fetches their detail pages CONCURRENTLY, bounded by
|
||||
Picks the fresh (uncached, not-skipped) listings, up to ``detail_cap`` per
|
||||
outcode, then fetches their detail pages CONCURRENTLY, bounded by
|
||||
``DETAIL_FETCH_CONCURRENCY`` (the shared RATE_LIMITER keeps the combined
|
||||
request rate polite). Cached listings cost neither a slot nor a GET. The
|
||||
worklist is deduplicated, so distinct ids write distinct cache keys and the
|
||||
|
|
@ -305,8 +305,8 @@ def _collect_search_props(
|
|||
) -> tuple[list[dict], int]:
|
||||
"""Paginate the search API for one outcode+channel, collecting raw results.
|
||||
|
||||
Returns ``(raw_props, result_count)``. Pagination stays serial — each page
|
||||
reveals the next — but is cheap relative to detail fetching, and the
|
||||
Returns ``(raw_props, result_count)``. Pagination stays serial (each page
|
||||
reveals the next) but is cheap relative to detail fetching, and the
|
||||
RATE_LIMITER spaces the page GETs. Collection stops at ``max_properties`` raw
|
||||
listings, the end of results, or Rightmove's ``_MAX_INDEX`` page cap."""
|
||||
raw_props: list[dict] = []
|
||||
|
|
@ -324,6 +324,8 @@ def _collect_search_props(
|
|||
"channel": channel_cfg["channel"],
|
||||
"transactionType": channel_cfg["transactionType"],
|
||||
}
|
||||
# Optional per-channel filters, e.g. `mustHave=newHome` for the new-homes pass.
|
||||
params.update(channel_cfg.get("extra_params", {}))
|
||||
data = fetch_with_retry(client, SEARCH_URL, params)
|
||||
if not data:
|
||||
log.warning(
|
||||
|
|
@ -372,7 +374,7 @@ def _paginate(
|
|||
) -> tuple[list[dict], int]:
|
||||
"""Collect search results, recover true postcodes, and transform them.
|
||||
|
||||
Search pages are paginated serially; then — when ``fetch_details`` is set —
|
||||
Search pages are paginated serially; then, when ``fetch_details`` is set,
|
||||
up to ``detail_cap`` listings per outcode have their detail page fetched
|
||||
CONCURRENTLY for the property's TRUE full postcode (see
|
||||
``parse_detail_postcode``), with listings the search already pins precisely
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue