SPB Git forge

spb/house-ka

Public
18commits 1branches 0releases
1.9 MBsize
maindefault branch
19 days agolast push
Python 67% TypeScript 18.2% CSS 14.4%
5.8 KB · 146 lines python
Raw Blame History
1#!/usr/bin/env python32# -----------------------------------------------------------------------------3# House-Ka — scout RealtyPress reste-du-Canada (2026-08-27)4# scripts/scout_canada_rp.py : découverte de sites d'agences RealtyPress/CREA5#   DDF hors Ontario/Québec via Serper (Google), puis validation live par6#   empreinte (`rps-property-result`) et estimation de volume par sondage de7#   pagination. Sortie : /tmp/rp_canada_candidates.json8# Usage : SERPER_API_KEY dans ~/.claude/.env — .venv/bin/python scripts/scout_canada_rp.py9# -----------------------------------------------------------------------------10import json11import os12import re13import time14from pathlib import Path15from urllib.parse import urlparse1617import requests1819# clé Serper (~/.claude/.env du nœud)20env = Path.home() / ".claude" / ".env"21for line in env.read_text().splitlines():22    if line.startswith("SERPER_API_KEY="):23        os.environ.setdefault("SERPER_API_KEY", line.split("=", 1)[1].strip())24KEY = os.environ["SERPER_API_KEY"]2526UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "27      "(KHTML, like Gecko) Chrome/126.0 Safari/537.36")2829PROVINCES = {30    "British Columbia": ["Vancouver", "Victoria", "Kelowna", "Kamloops", "Nanaimo", "Prince George"],31    "Alberta": ["Calgary", "Edmonton", "Red Deer", "Lethbridge", "Medicine Hat", "Grande Prairie"],32    "Saskatchewan": ["Saskatoon", "Regina", "Prince Albert", "Moose Jaw"],33    "Manitoba": ["Winnipeg", "Brandon", "Steinbach"],34    "New Brunswick": ["Moncton", "Fredericton", "Saint John", "Miramichi"],35    "Nova Scotia": ["Halifax", "Sydney", "Truro", "Yarmouth"],36    "Prince Edward Island": ["Charlottetown", "Summerside"],37    "Newfoundland and Labrador": ["St. John's", "Corner Brook", "Gander"],38    "Yukon": ["Whitehorse"],39    "Northwest Territories": ["Yellowknife"],40}4142SKIP_DOMAINS = re.compile(43    r"realtor\.ca|realtypress\.ca|zillow|point2homes|remax\.ca|royallepage\.ca|"44    r"centris|duproprio|kijiji|facebook|youtube|instagram|linkedin|wikipedia|"45    r"realtylink|housesigma|zolo\.ca|honestdoor|wowa\.ca|estately|redfin|"46    r"strata\.ca|rew\.ca|ovlix|homefinder|propertyguys")4748sess = requests.Session()49sess.headers["User-Agent"] = UA50sess.cookies.set("disclaimer", "accepted")515253def serper(q: str) -> list[str]:54    try:55        r = requests.post("https://google.serper.dev/search",56                          headers={"X-API-KEY": KEY, "Content-Type": "application/json"},57                          json={"q": q, "gl": "ca", "hl": "en", "num": 30}, timeout=20)58        r.raise_for_status()59        out = []60        for it in r.json().get("organic", []):61            host = urlparse(it.get("link", "")).netloc.lower().removeprefix("www.")62            if host and not SKIP_DOMAINS.search(host):63                out.append(host)64        return out65    except Exception as e:66        print(f"  serper KO ({q[:50]}…): {e}")67        return []686970def fetch(url: str) -> str:71    try:72        r = sess.get(url, timeout=18, allow_redirects=True)73        return r.text if r.status_code == 200 else ""74    except Exception:75        return ""767778CARD = "rps-property-result"79PROV_RE = re.compile(r",\s*(British Columbia|Alberta|Saskatchewan|Manitoba|"80                     r"New Brunswick|Nova Scotia|Prince Edward Island|"81                     r"Newfoundland and Labrador|Ontario|Qu[ée]bec|Yukon|"82                     r"Northwest Territories|Nunavut)\b", re.I)838485def probe(domain: str) -> dict | None:86    """Valide l'empreinte RealtyPress et estime le volume par sondage de pages."""87    for archive in ("listing", "listings", "all-regional-listings"):88        base = f"https://{domain}/{archive}"89        body = fetch(f"{base}/?posts_per_page=100")90        n = body.count(CARD)91        if n < 3:92            continue93        # provinces observées sur la 1re page94        provs: dict[str, int] = {}95        for m in PROV_RE.finditer(body):96            p = m.group(1).title().replace("Nd", "nd").replace(" And ", " and ")97            provs[p] = provs.get(p, 0) + 198        # volume : dernière page non vide parmi des jalons croissants99        last_ok, per_page = 1, n100        for pg in (5, 20, 50, 100, 200, 400, 800, 1200):101            b = fetch(f"{base}/page/{pg}/?posts_per_page=100")102            c = b.count(CARD)103            if c >= 3:104                last_ok, per_page = pg, max(per_page, c)105            else:106                break107            time.sleep(0.4)108        return {"domain": domain, "archive": archive, "cards_page1": n,109                "provinces": provs, "approx_pages_gte": last_ok,110                "approx_volume_gte": last_ok * per_page}111    return None112113114def main() -> None:115    domains: dict[str, str] = {}116    for prov, cities in PROVINCES.items():117        queries = [118            f'"MLS® Number" "{prov}" inurl:listing',119            f'"Powered by RealtyPress" "{prov}"',120        ] + [f'inurl:/listing/ "MLS®" "{c}" "{prov}"' for c in cities[:3]]121        for q in queries:122            for d in serper(q):123                domains.setdefault(d, prov)124            time.sleep(0.4)125        print(f"[scout] {prov}: {sum(1 for v in domains.values() if v == prov)} candidats cumulés")126127    print(f"[scout] {len(domains)} domaines candidats — validation live…")128    results = []129    for i, (d, prov_hint) in enumerate(sorted(domains.items()), 1):130        r = probe(d)131        if r:132            r["province_hint"] = prov_hint133            results.append(r)134            print(f"  ✅ {d} ({r['archive']}) ~≥{r['approx_volume_gte']}{r['provinces']}")135        if i % 20 == 0:136            print(f"  … {i}/{len(domains)}")137        time.sleep(0.3)138139    out = Path("/tmp/rp_canada_candidates.json")140    out.write_text(json.dumps(results, indent=1, ensure_ascii=False))141    print(f"[scout] {len(results)} sites RealtyPress validés → {out}")142143144if __name__ == "__main__":145    main()146