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
2 days agolast push
HTML 98.9% Python 0.6%

[ka2] fix connecteur lbm: 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) et appartements_rimouski (af2be7c), hébergeur SiteGround identique (8e connecteur touché). Le sync du 2026-09-12 22:19 a rapporté found=0 ok (médiane 8.0, stale) : SiteGround sert son anti-bot sgcaptcha (HTTP 202, 166 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, 161 Ko, les 3 pages projet (le-2922, le-3215, le-3537) présentes. Fix minimal, pattern appartements_rimouski : _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 (accueil + pages projet partagent l'IP), erreur franche si le challenge persiste — la page d'accueil (liste des projets, pas de pagination) lève désormais au lieu de retourner [] (fini le zéro silencieux, ingest journalise ok=0) ; les pages projet gardent le continue (résultats partiels acceptables). Pas de detail_cache dans ce connecteur. Le chemin direct reste tenté d'abord (gratuit si SiteGround déflague l'IP). Testé : fetch() venv → 8 annonces (Le 3537 modèles 1-4 : 3½ 1 370 $ → 5½ 1 895 $, 15 photos ; typologies 4½/5½ Le 3215 et Le 2922 — médiane 8.0 restaurée), run.py sync lbm → found=8 ok, pm2 restart lou-ka-sync, site :8095 → 200.

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

1 changed file +45 −5

modified louka/connectors/lbm.py +45 −5
@@ -10,16 +10,34 @@
10 10 # « À partir de X$/m », superficie, chambres, inclusions), les autres
11 11 # seulement les typologies offertes (« 4 1/2 et 5 1/2 », 1100 pi²).
12 12 # Granularité MODÈLE/TYPOLOGIE par immeuble, prix seulement où publié.
13 +# 2026-09-13 : SiteGround sert son anti-bot sgcaptcha (202 + challenge JS,
14 +# en-tête sg-captcha) aux IP datacenter — invisible du wrapper résilient
15 +# (2xx) ; on bascule alors la session sur un proxy résidentiel Oxylabs CA
16 +# (même pattern que boreal_abitibi/citiluxx/cromwell/deschenes_pepin/
17 +# gestion_habitation/gimcote/appartements_rimouski, hébergeur identique).
13 18 # -----------------------------------------------------------------------------
14 19 from __future__ import annotations
15 20
21 +import os
16 22 import re
23 +import time
24 +from urllib.parse import quote
17 25
18 26 from ..schema import Listing, normalize_unit_type
19 27 from .base import BaseConnector
20 28
21 29 BASE = "https://grouperivenord.ca"
22 30
31 +# page interstitielle sgcaptcha SiteGround (meta refresh vers /.well-known/…)
32 +_SG_MARKER = "/.well-known/sgcaptcha/"
33 +
34 +
35 +def _sg_challenge(resp) -> bool:
36 + """True si la réponse est le challenge anti-bot SiteGround (202 + JS)."""
37 + if "challenge" in str(resp.headers.get("sg-captcha", "")).lower():
38 + return True
39 + return _SG_MARKER in (resp.text or "")[:600]
40 +
23 41 PROJECT_RE = re.compile(r'href="(https://grouperivenord\.ca/(le-[\w-]+)/)"')
24 42 HERO_RE = re.compile(r"Bienvenue au\s*\|?\s*(\d{2,5}[^|]{0,40}?)\|", re.I)
25 43 CITY_RE = re.compile(r"\|\s*((?:Saint|Sainte)e?-[A-Za-zÀ-ü’'-]+(?:-[A-Za-zÀ-ü’'-]+)*)\s*\|")
@@ -57,16 +75,38 @@ class LbmConnector(BaseConnector):
57 75 source_id = "lbm"
58 76 request_delay = 0.6
59 77
78 + def _enable_residential_proxy(self) -> bool:
79 + """Route toute la session via Oxylabs résidentiel CA (session collante)."""
80 + endpoint = os.environ.get("OXYLABS_PROXY")
81 + user = os.environ.get("OXYLABS_PROXY_USER")
82 + pwd = os.environ.get("OXYLABS_PROXY_PASS")
83 + if not (endpoint and user and pwd):
84 + return False
85 + puser = f"{user}-cc-CA-sessid-lbm{int(time.time())}-sesstime-10"
86 + proxy = f"http://{quote(puser, safe='')}:{quote(pwd, safe='')}@{endpoint}"
87 + self.session.proxies = {"http": proxy, "https": proxy}
88 + self.session.verify = False # CA MITM du proxy Oxylabs
89 + return True
90 +
91 + def _get_html(self, url: str) -> str:
92 + """GET avec contournement du challenge sgcaptcha (proxy résidentiel)."""
93 + resp = self.get(url)
94 + if not _sg_challenge(resp):
95 + return resp.text
96 + if not self.session.proxies and self._enable_residential_proxy():
97 + resp = self.get(url)
98 + if not _sg_challenge(resp):
99 + return resp.text
100 + raise RuntimeError(f"challenge sgcaptcha non contourné — {url}")
101 +
60 102 def fetch(self) -> list[Listing]:
61 103 listings: list[Listing] = []
62 − try:
63 − home = self.get(BASE + "/").text
64 − except Exception:
65 − return listings
104 + # échec franc sur challenge/panne (fini le « 0 trouvé ok » silencieux)
105 + home = self._get_html(BASE + "/")
66 106 pages = list(dict.fromkeys(PROJECT_RE.findall(home)))
67 107 for page_url, slug in pages:
68 108 try:
69 − html = self.get(page_url).text
109 + html = self._get_html(page_url)
70 110 except Exception:
71 111 continue
72 112 flat = _flat(html)
73 113