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 appartements_rimouski: 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) et gimcote (12df764), hébergeur SiteGround identique (7e connecteur touché). Les syncs du 2026-09-13 02:13→02:40 ont rapporté found=0 ok x5 (médiane 16-17, disjoncteur de dérive déclenché puis médiane érodée à 0) : SiteGround sert son anti-bot sgcaptcha (HTTP 202, 186 octets, en-tête sg-captcha: challenge, 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 lien /logements/ devenait « 0 trouvé ok », invisible du disjoncteur. Vérifié live : IP directe → 202 challenge ; IP résidentielle Oxylabs CA → 200, 229 Ko, les 16 cartes /logements/ présentes. Fix minimal, pattern gimcote : _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 (unique, pas de pagination) 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() pour ne pas empoisonner detail_cache avec un payload vide (clé figée jamais invalidée) ; les fiches 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 → 16 annonces (studio 675 $ → 6½ 2 300 $, photos 4-13, Rimouski/Le Bic — médiane 16 restaurée), pytest fixtures 2 passed, run.py sync appartements_rimouski → found=16 ok, pm2 restart lou-ka-sync, site :8095 → 200.

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

1 changed file +47 −2

modified louka/connectors/appartements_rimouski.py +47 −2
@@ -12,13 +12,21 @@
12 12 # BD) : description complète, quartier (« Quartier : Rimouski-Est »),
13 13 # galerie et GPS via le JSON-LD schema.org/House (lat/lng inversés à la
14 14 # source : corrigés). robots.txt : Disallow /pro/* seulement, sitemap XML.
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, hébergeur identique).
15 20 # -----------------------------------------------------------------------------
16 21 from __future__ import annotations
17 22
18 23 import hashlib
19 24 import html as htmllib
20 25 import json
26 +import os
21 27 import re
28 +import time
29 +from urllib.parse import quote
22 30
23 31 from bs4 import BeautifulSoup
24 32
@@ -28,6 +36,16 @@ from .base import BaseConnector
28 36 BASE = "https://www.appartementsrimouski.com"
29 37 LIST_URL = f"{BASE}/choisir-mon-logement"
30 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 +
31 49 _PRICE_RE = re.compile(r"\d[\d\s,.]*\$\s*/\s*mois", re.I)
32 50 # type explicite dans le titre (« 3 1/2 », « Studio », « Chambre », « Loft »)
33 51 _HALF_RE = re.compile(r"(\d+)\s*(?:½|1/2)")
@@ -62,8 +80,33 @@ class AppartementsRimouskiConnector(BaseConnector):
62 80 request_delay = 0.6
63 81 max_details = 30 # garde-fou fiches détail (vraies requêtes par sync)
64 82
83 + def _enable_residential_proxy(self) -> bool:
84 + """Route toute la session via Oxylabs résidentiel CA (session collante)."""
85 + endpoint = os.environ.get("OXYLABS_PROXY")
86 + user = os.environ.get("OXYLABS_PROXY_USER")
87 + pwd = os.environ.get("OXYLABS_PROXY_PASS")
88 + if not (endpoint and user and pwd):
89 + return False
90 + puser = f"{user}-cc-CA-sessid-rimouski{int(time.time())}-sesstime-10"
91 + proxy = f"http://{quote(puser, safe='')}:{quote(pwd, safe='')}@{endpoint}"
92 + self.session.proxies = {"http": proxy, "https": proxy}
93 + self.session.verify = False # CA MITM du proxy Oxylabs
94 + return True
95 +
96 + def _get_html(self, url: str) -> str:
97 + """GET avec contournement du challenge sgcaptcha (proxy résidentiel)."""
98 + resp = self.get(url)
99 + if not _sg_challenge(resp):
100 + return resp.text
101 + if not self.session.proxies and self._enable_residential_proxy():
102 + resp = self.get(url)
103 + if not _sg_challenge(resp):
104 + return resp.text
105 + raise RuntimeError(f"challenge sgcaptcha non contourné — {url}")
106 +
65 107 def fetch(self) -> list[Listing]:
66 − html = self.get(LIST_URL).text
108 + # échec franc sur challenge (fini le « 0 trouvé ok » silencieux)
109 + html = self._get_html(LIST_URL)
67 110 soup = BeautifulSoup(html, "html.parser")
68 111
69 112 self._fetched = 0
@@ -159,7 +202,9 @@ class AppartementsRimouskiConnector(BaseConnector):
159 202 if self._fetched >= self.max_details:
160 203 raise RuntimeError("budget de fiches détail atteint")
161 204 self._fetched += 1
162 − html = self.get(url).text
205 + # _get_html lève sur challenge : pas de {} vide dans detail_cache
206 + # (clé figée jamais invalidée)
207 + html = self._get_html(url)
163 208 soup = BeautifulSoup(html, "html.parser")
164 209 out: dict = {}
165 210
166 211