SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
3 days agolast push
HTML 98.9% Python 0.6%

[ka2] fix connecteur mondev: contournement du challenge sgcaptcha SiteGround via proxy résidentiel Oxylabs — même cause et même fix que boreal_abitibi (8923045), citiluxx (d17f5fe), cromwell (8351131), deschenes_pepin (f8aca03), gestion_habitation (8b0660b), gimcote (12df764), appartements_rimouski (af2be7c), lbm (5decd97), gestion_mj (15d2bbb) et mercini (5125859), hébergeur SiteGround identique (11e connecteur touché). Le sync du 2026-09-12 22:42 a rapporté found=0 ok (médiane 20.0, stale) : SiteGround sert son anti-bot sgcaptcha (HTTP 202, 199 octets, en-tête sg-captcha: challenge, meta refresh vers /.well-known/sgcaptcha/) à l'IP datacenter du nœud — un 2xx sans HTTPError, avalé par le `except Exception: return listings` de fetch() en « 0 trouvé ok », invisible du disjoncteur. Vérifié live : IP directe → 202 challenge ; IP résidentielle Oxylabs CA → 200, 557 Ko, 30 immeubles présents. Fix minimal, pattern lbm (liste unique + fiches immeubles re-fetchées à chaque sync, pas de detail_cache) : _get_html() détecte le challenge (en-tête sg-captcha ou marqueur /.well-known/sgcaptcha/) et bascule la session sur pr.oxylabs.io:7777 en session collante -cc-CA (liste + fiches immeubles partagent l'IP), erreur franche si le challenge persiste — la page de liste (unique, pas de pagination) lève désormais au lieu de retourner [] (fini le zéro silencieux, ingest journalise ok=0) ; _parse_building() passe aussi par _get_html() et garde le continue autour (résultats partiels acceptables). Le chemin direct reste tenté d'abord (gratuit si SiteGround déflague l'IP). Testé : fetch() venv → 40 annonces (Florence 3½ 1 500 $ → 5½ 2 600 $ rue Florian, Jacques Studio 1 630 $ rue Fullum — adresses complètes, 16-40 photos, médiane 20.0 largement couverte, dernier sync sain à 43), pytest fixtures 2 passed, run.py sync mondev → found=40 ok (94 s), pm2 restart lou-ka-sync, site :8095 → 200.

Simon-Pierre Boucher committed 13 days ago (Sep 13, 2026) parent 5125859

1 changed file +45 −5

modified louka/connectors/mondev.py +45 −5
@@ -9,10 +9,17 @@
9 9 # contient un « PLAN SELECTOR » avec, par typologie, « Starting at $X » ou
10 10 # « not available ». Une annonce par (immeuble, typologie) disponible.
11 11 # Prix « à partir de », adresse, description, commodités et galerie photos.
12 +# 2026-09-13 : SiteGround sert son anti-bot sgcaptcha (202 + challenge JS,
13 +# en-tête sg-captcha) aux IP datacenter — invisible du wrapper résilient
14 +# (2xx) ; on bascule alors la session sur un proxy résidentiel Oxylabs CA
15 +# (même pattern que boreal_abitibi/…/mercini, hébergeur identique).
12 16 # -----------------------------------------------------------------------------
13 17 from __future__ import annotations
14 18
19 +import os
15 20 import re
21 +import time
22 +from urllib.parse import quote
16 23
17 24 from bs4 import BeautifulSoup
18 25
@@ -22,6 +29,16 @@ from .base import BaseConnector
22 29 BASE = "https://mondev.ca"
23 30 LIST_URL = f"{BASE}/apartments-and-condos-for-rent/"
24 31
32 +# page interstitielle sgcaptcha SiteGround (meta refresh vers /.well-known/…)
33 +_SG_MARKER = "/.well-known/sgcaptcha/"
34 +
35 +
36 +def _sg_challenge(resp) -> bool:
37 + """True si la réponse est le challenge anti-bot SiteGround (202 + JS)."""
38 + if "challenge" in str(resp.headers.get("sg-captcha", "")).lower():
39 + return True
40 + return _SG_MARKER in (resp.text or "")[:600]
41 +
25 42 _BUILDING_RE = re.compile(
26 43 r'href="(https://mondev\.ca/apartments-and-condos-for-rent/'
27 44 r'([a-z0-9-]+)/([a-z0-9-]+)/)"')
@@ -78,12 +95,35 @@ class MondevConnector(BaseConnector):
78 95 request_delay = 0.6
79 96 max_buildings = 45 # garde-fou de crawl
80 97
98 + def _enable_residential_proxy(self) -> bool:
99 + """Route toute la session via Oxylabs résidentiel CA (session collante)."""
100 + endpoint = os.environ.get("OXYLABS_PROXY")
101 + user = os.environ.get("OXYLABS_PROXY_USER")
102 + pwd = os.environ.get("OXYLABS_PROXY_PASS")
103 + if not (endpoint and user and pwd):
104 + return False
105 + puser = f"{user}-cc-CA-sessid-mondev{int(time.time())}-sesstime-10"
106 + proxy = f"http://{quote(puser, safe='')}:{quote(pwd, safe='')}@{endpoint}"
107 + self.session.proxies = {"http": proxy, "https": proxy}
108 + self.session.verify = False # CA MITM du proxy Oxylabs (cf. _resilient)
109 + return True
110 +
111 + def _get_html(self, url: str) -> str:
112 + """GET avec contournement du challenge sgcaptcha (proxy résidentiel)."""
113 + resp = self.get(url)
114 + if not _sg_challenge(resp):
115 + return resp.text
116 + if not self.session.proxies and self._enable_residential_proxy():
117 + resp = self.get(url)
118 + if not _sg_challenge(resp):
119 + return resp.text
120 + raise RuntimeError(f"challenge sgcaptcha non contourné — {url}")
121 +
81 122 def fetch(self) -> list[Listing]:
82 123 listings: list[Listing] = []
83 − try:
84 − index = self.get(LIST_URL).text
85 − except Exception:
86 − return listings
124 + # la liste échoue franchement sur challenge (fini le « 0 trouvé ok »
125 + # silencieux — ingest journalise ok=0)
126 + index = self._get_html(LIST_URL)
87 127
88 128 buildings: dict[str, tuple[str, str]] = {}
89 129 for url, zone, slug in _BUILDING_RE.findall(index):
@@ -99,7 +139,7 @@ class MondevConnector(BaseConnector):
99 139 return listings
100 140
101 141 def _parse_building(self, url: str, zone: str, slug: str) -> list[Listing]:
102 − html = self.get(url).text
142 + html = self._get_html(url)
103 143 soup = BeautifulSoup(html, "html.parser")
104 144
105 145 h1 = soup.find("h1")
106 146