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_mj: 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) et lbm (5decd97), hébergeur SiteGround identique (9e connecteur touché). Les syncs du 2026-09-13 03:03 et 04:44 ont rapporté found=0 ok x2 (médiane 9, 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 dont le body HTML fait lever .json(), 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, 2 090 octets JSON, les 9 posts du CPT a-louer présents. 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 (API wp-json + fiches détail partagent l'IP), erreur franche si le challenge persiste — l'appel API (liste unique, pas de pagination) lève désormais au lieu de retourner [] (fini le zéro silencieux, ingest journalise ok=0), idem si la réponse wp-json n'est plus une liste ; _fetch_detail() passe aussi par _get_html() pour ne pas empoisonner detail_cache avec un payload vide (clé figée jamais invalidée) ; les posts gardent le continue (résultats partiels acceptables). Le chemin direct reste tenté d'abord (gratuit si SiteGround déflague l'IP). Testé : fetch() venv → 9 annonces (studios 1 400-1 600 $, 4½ 1 650 $, 5½ 2 199 $ — 9155 Meilleur, 9820 Verville, 9050 av. du Parc, 4 photos chacune — médiane 9 restaurée), run.py sync gestion_mj → found=9 ok, pm2 restart lou-ka-sync, site :8095 → 200.

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

1 changed file +50 −6

modified louka/connectors/gestion_mj.py +50 −6
@@ -12,10 +12,20 @@
12 12 # chambre(s) », « 900 pi² », « 02/03/2025 ») → chambres/typologie dérivées
13 13 # du TITRE (« Studio moderne », « Appartement 2/3 chambres »), icônes
14 14 # reléguées en details. Granularité : une annonce par unité (post CPT).
15 +# 2026-09-13 : SiteGround sert son anti-bot sgcaptcha (202 + challenge JS,
16 +# en-tête sg-captcha) aux IP datacenter — invisible du wrapper résilient
17 +# (2xx) ; on bascule alors la session sur un proxy résidentiel Oxylabs CA
18 +# (même pattern que boreal_abitibi/citiluxx/cromwell/deschenes_pepin/
19 +# gestion_habitation/gimcote/appartements_rimouski/lbm, hébergeur
20 +# identique).
15 21 # -----------------------------------------------------------------------------
16 22 from __future__ import annotations
17 23
24 +import json
25 +import os
18 26 import re
27 +import time
28 +from urllib.parse import quote
19 29
20 30 from bs4 import BeautifulSoup
21 31
@@ -26,6 +36,16 @@ BASE = "https://gestionmj.com"
26 36 API_URL = (f"{BASE}/wp-json/wp/v2/a-louer?per_page=100"
27 37 "&_fields=id,slug,link,title,modified")
28 38
39 +# page interstitielle sgcaptcha SiteGround (meta refresh vers /.well-known/…)
40 +_SG_MARKER = "/.well-known/sgcaptcha/"
41 +
42 +
43 +def _sg_challenge(resp) -> bool:
44 + """True si la réponse est le challenge anti-bot SiteGround (202 + JS)."""
45 + if "challenge" in str(resp.headers.get("sg-captcha", "")).lower():
46 + return True
47 + return _SG_MARKER in (resp.text or "")[:600]
48 +
29 49 BED_TITLE_RE = re.compile(r"(\d)\s*chambres?", re.I)
30 50 IMG_RE = re.compile(r"https://gestionmj\.com/wp-content/uploads/"
31 51 r'[^"\s\\)]+\.(?:jpe?g|png|webp)', re.I)
@@ -35,14 +55,36 @@ class GestionMJConnector(BaseConnector):
35 55 source_id = "gestion_mj"
36 56 request_delay = 0.7
37 57
58 + def _enable_residential_proxy(self) -> bool:
59 + """Route toute la session via Oxylabs résidentiel CA (session collante)."""
60 + endpoint = os.environ.get("OXYLABS_PROXY")
61 + user = os.environ.get("OXYLABS_PROXY_USER")
62 + pwd = os.environ.get("OXYLABS_PROXY_PASS")
63 + if not (endpoint and user and pwd):
64 + return False
65 + puser = f"{user}-cc-CA-sessid-gestionmj{int(time.time())}-sesstime-10"
66 + proxy = f"http://{quote(puser, safe='')}:{quote(pwd, safe='')}@{endpoint}"
67 + self.session.proxies = {"http": proxy, "https": proxy}
68 + self.session.verify = False # CA MITM du proxy Oxylabs
69 + return True
70 +
71 + def _get_html(self, url: str) -> str:
72 + """GET avec contournement du challenge sgcaptcha (proxy résidentiel)."""
73 + resp = self.get(url)
74 + if not _sg_challenge(resp):
75 + return resp.text
76 + if not self.session.proxies and self._enable_residential_proxy():
77 + resp = self.get(url)
78 + if not _sg_challenge(resp):
79 + return resp.text
80 + raise RuntimeError(f"challenge sgcaptcha non contourné — {url}")
81 +
38 82 def fetch(self) -> list[Listing]:
39 83 listings: list[Listing] = []
40 − try:
41 − posts = self.get(API_URL).json()
42 − except Exception:
43 − return listings
84 + # échec franc sur challenge/panne (fini le « 0 trouvé ok » silencieux)
85 + posts = json.loads(self._get_html(API_URL))
44 86 if not isinstance(posts, list):
45 − return listings
87 + raise RuntimeError("réponse wp-json inattendue (pas une liste)")
46 88
47 89 for post in posts:
48 90 try:
@@ -93,7 +135,9 @@ class GestionMJConnector(BaseConnector):
93 135
94 136 def _fetch_detail(self, url: str) -> dict:
95 137 out: dict = {}
96 − html = self.get(url).text
138 + # _get_html lève sur challenge : pas de {} vide dans detail_cache
139 + # (clé figée jamais invalidée)
140 + html = self._get_html(url)
97 141 soup = BeautifulSoup(html, "html.parser")
98 142
99 143 el = soup.select_one("h2.price")
100 144