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 gestion_habitation: 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) et deschenes_pepin (f8aca03), hébergeur SiteGround identique. Le sync du 2026-09-12 21:50 a rapporté found=0 ok (médiane 4.0, stale) : SiteGround sert son anti-bot sgcaptcha (HTTP 202, 176 octets, meta refresh vers /.well-known/sgcaptcha/) à l'IP datacenter du nœud (76.70.60.50 flaguée « ipr ») — un 2xx sans HTTPError, la soupe sans .location-card devenait « 0 trouvé ok », invisible du disjoncteur. Vérifié live : IP directe → 202 challenge ; IP résidentielle Oxylabs CA → 200, 130 Ko, 12 cartes dont 8 « NON DISPONIBLE » (4 disponibles, cohérent avec la médiane). Fix minimal, pattern deschenes_pepin : _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 détail partagent l'IP), erreur franche si le challenge persiste — la page de liste lève désormais au lieu de retourner 0 carte (fini le zéro silencieux, ingest journalise ok=0) ; _fetch_detail() passe aussi par _get_html() et lève sur challenge pour ne pas empoisonner detail_cache avec un payload vide (clé figée jamais invalidée) ; les fiches détail gardent le except autour de detail() (résultats partiels acceptables). Le chemin direct reste tenté d'abord (gratuit si SiteGround déflague l'IP). Testé : fetch() venv → 4 annonces (4 ch. Malartic 3 200 $, 225 1e Av. O. Amos 900 $, 3½ Malartic 1 250 $ juin 2026, 3½ Amos 1 050 $ juillet 2026, photos 8-25 — médiane 4.0 restaurée), pytest fixtures 2 passed, run.py sync gestion_habitation → found=4 ok, pm2 restart lou-ka-sync, site :8095 → 200.

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

1 changed file +48 −2

modified louka/connectors/gestion_habitation.py +48 −2
@@ -13,11 +13,19 @@
13 13 # animaux, fumeur, stationnement, disponibilité), description
14 14 # (`.location-more-infos`) et galerie (carrousel Oxygen, liens pleine taille).
15 15 # robots.txt WP standard (Disallow /wp-admin/), sitemap XML.
16 +# 2026-09-13 : SiteGround sert son anti-bot sgcaptcha (202 + challenge JS,
17 +# en-tête sg-captcha) aux IP datacenter — invisible du wrapper résilient
18 +# (2xx) ; on bascule alors la session sur un proxy résidentiel Oxylabs CA
19 +# (même pattern que boreal_abitibi/citiluxx/cromwell/deschenes_pepin,
20 +# hébergeur identique).
16 21 # -----------------------------------------------------------------------------
17 22 from __future__ import annotations
18 23
19 24 import hashlib
25 +import os
20 26 import re
27 +import time
28 +from urllib.parse import quote
21 29
22 30 from bs4 import BeautifulSoup
23 31
@@ -27,6 +35,16 @@ from .base import BaseConnector
27 35 BASE = "https://gestionhabitation.ca"
28 36 LIST_URL = f"{BASE}/a-louer/"
29 37
38 +# page interstitielle sgcaptcha SiteGround (meta refresh vers /.well-known/…)
39 +_SG_MARKER = "/.well-known/sgcaptcha/"
40 +
41 +
42 +def _sg_challenge(resp) -> bool:
43 + """True si la réponse est le challenge anti-bot SiteGround (202 + JS)."""
44 + if "challenge" in str(resp.headers.get("sg-captcha", "")).lower():
45 + return True
46 + return _SG_MARKER in (resp.text or "")[:600]
47 +
30 48 # suffixe de redimensionnement WordPress (« -224x300.jpg » -> pleine taille)
31 49 _SIZE_SUFFIX = re.compile(r"-\d{2,4}x\d{2,4}(?=\.(?:jpg|jpeg|png|webp)$)", re.I)
32 50 # « 3 1/2 », « 3½ », « 4 et demi » dans le titre de la carte
@@ -53,8 +71,34 @@ class GestionHabitationConnector(BaseConnector):
53 71 request_delay = 0.6
54 72 max_details = 30 # garde-fou fiches détail (vraies requêtes par sync)
55 73
74 + def _enable_residential_proxy(self) -> bool:
75 + """Route toute la session via Oxylabs résidentiel CA (session collante)."""
76 + endpoint = os.environ.get("OXYLABS_PROXY")
77 + user = os.environ.get("OXYLABS_PROXY_USER")
78 + pwd = os.environ.get("OXYLABS_PROXY_PASS")
79 + if not (endpoint and user and pwd):
80 + return False
81 + puser = f"{user}-cc-CA-sessid-gesthab{int(time.time())}-sesstime-10"
82 + proxy = f"http://{quote(puser, safe='')}:{quote(pwd, safe='')}@{endpoint}"
83 + self.session.proxies = {"http": proxy, "https": proxy}
84 + self.session.verify = False # CA MITM du proxy Oxylabs (cf. _resilient)
85 + return True
86 +
87 + def _get_html(self, url: str) -> str:
88 + """GET avec contournement du challenge sgcaptcha (proxy résidentiel)."""
89 + resp = self.get(url)
90 + if not _sg_challenge(resp):
91 + return resp.text
92 + if not self.session.proxies and self._enable_residential_proxy():
93 + resp = self.get(url)
94 + if not _sg_challenge(resp):
95 + return resp.text
96 + raise RuntimeError(f"challenge sgcaptcha non contourné — {url}")
97 +
56 98 def fetch(self) -> list[Listing]:
57 − html = self.get(LIST_URL).text
99 + # la page de liste échoue franchement sur challenge (fini le
100 + # « 0 trouvé ok » silencieux — ingest journalise ok=0)
101 + html = self._get_html(LIST_URL)
58 102 soup = BeautifulSoup(html, "html.parser")
59 103
60 104 self._fetched = 0
@@ -155,7 +199,9 @@ class GestionHabitationConnector(BaseConnector):
155 199 if self._fetched >= self.max_details:
156 200 raise RuntimeError("budget de fiches détail atteint")
157 201 self._fetched += 1
158 − html = self.get(url).text
202 + # _get_html lève sur challenge : pas de {} vide dans detail_cache
203 + # (clé figée jamais invalidée)
204 + html = self._get_html(url)
159 205 soup = BeautifulSoup(html, "html.parser")
160 206 out: dict = {}
161 207
162 208