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 cromwell: contournement du challenge sgcaptcha SiteGround via proxy résidentiel Oxylabs — même cause et même fix que boreal_abitibi (8923045) et citiluxx (d17f5fe), hébergeur SiteGround identique. Le sync du 2026-09-13 01:16 a rapporté found=0 ok=1 (médiane 11.0, stale, retraits suspendus par le disjoncteur de dérive) : SiteGround sert son anti-bot sgcaptcha (HTTP 202, 197 octets, meta refresh vers /.well-known/sgcaptcha/, en-tête sg-captcha: challenge) à l'IP datacenter du nœud (76.70.60.50 flaguée « ipr ») — un 2xx sans HTTPError, avalé par le `except Exception: break` de fetch() en « 0 trouvé ok », invisible du disjoncteur. Vérifié live : IP directe → 202 challenge ; IP résidentielle Oxylabs CA → 200, 244 Ko, les 11 cartes item-listing-wrap présentes. Fix minimal, pattern citiluxx : _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 1re page de liste lève désormais au lieu de break (fini le zéro silencieux), les pages suivantes gardent le break (résultats partiels acceptables) ; _fetch_detail() passe aussi par _get_html() pour ne pas empoisonner detail_cache avec un payload vide. Le chemin direct reste tenté d'abord (gratuit si SiteGround déflague l'IP). Testé : fetch() venv → 11 annonces (Studio 1 345 $ → 6½ 3 995 $, photos 7-10, dispo + promos), pytest fixtures 2 passed, run.py sync cromwell → found=11 ok (médiane 11.0 restaurée), pm2 restart lou-ka-sync, site :8095 → 200.

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

1 changed file +54 −7

modified louka/connectors/cromwell.py +54 −7
@@ -7,11 +7,18 @@
7 7 # publiées sur le site jumeau cromwellmontreal.ca (WordPress + thème
8 8 # Houzez, rendu serveur) : cartes .item-listing-wrap avec data-listid,
9 9 # fiches /property/<slug>/ pour photos, statut et description.
10 +# 2026-09-13 : SiteGround sert son anti-bot sgcaptcha (202 + challenge JS,
11 +# en-tête sg-captcha) aux IP datacenter — invisible du wrapper résilient
12 +# (2xx) ; on bascule alors la session sur un proxy résidentiel Oxylabs CA
13 +# (même pattern que boreal_abitibi/citiluxx, hébergeur SiteGround identique).
10 14 # -----------------------------------------------------------------------------
11 15 from __future__ import annotations
12 16
13 17 import hashlib
18 +import os
14 19 import re
20 +import time
21 +from urllib.parse import quote
15 22
16 23 from bs4 import BeautifulSoup
17 24
@@ -25,6 +32,17 @@ IMG_RE = re.compile(
25 32 r"https://cromwellmontreal\.ca/wp-content/uploads/"
26 33 r"[^\"\s\\]+?\.(?:jpg|jpeg|png|webp)", re.I)
27 34
35 +# page interstitielle sgcaptcha SiteGround (meta refresh vers /.well-known/…)
36 +_SG_MARKER = "/.well-known/sgcaptcha/"
37 +
38 +
39 +def _sg_challenge(resp) -> bool:
40 + """True si la réponse est le challenge anti-bot SiteGround (202 + JS)."""
41 + if "challenge" in str(resp.headers.get("sg-captcha", "")).lower():
42 + return True
43 + return _SG_MARKER in (resp.text or "")[:600]
44 +
45 +
28 46 # Municipalités de l'île qui ne sont pas des arrondissements de Montréal
29 47 _INDEPENDENT_CITIES = {
30 48 "westmount": "Westmount", "hampstead": "Hampstead",
@@ -69,16 +87,44 @@ class CromwellConnector(BaseConnector):
69 87 max_pages = 8 # garde-fou de pagination
70 88 max_details = 60 # garde-fou de fetch des fiches
71 89
90 + def _enable_residential_proxy(self) -> bool:
91 + """Route toute la session via Oxylabs résidentiel CA (session collante)."""
92 + endpoint = os.environ.get("OXYLABS_PROXY")
93 + user = os.environ.get("OXYLABS_PROXY_USER")
94 + pwd = os.environ.get("OXYLABS_PROXY_PASS")
95 + if not (endpoint and user and pwd):
96 + return False
97 + puser = f"{user}-cc-CA-sessid-cromwell{int(time.time())}-sesstime-10"
98 + proxy = f"http://{quote(puser, safe='')}:{quote(pwd, safe='')}@{endpoint}"
99 + self.session.proxies = {"http": proxy, "https": proxy}
100 + self.session.verify = False # CA MITM du proxy Oxylabs (cf. _resilient)
101 + return True
102 +
103 + def _get_html(self, url: str) -> str:
104 + """GET avec contournement du challenge sgcaptcha (proxy résidentiel)."""
105 + resp = self.get(url)
106 + if not _sg_challenge(resp):
107 + return resp.text
108 + if not self.session.proxies and self._enable_residential_proxy():
109 + resp = self.get(url)
110 + if not _sg_challenge(resp):
111 + return resp.text
112 + raise RuntimeError(f"challenge sgcaptcha non contourné — {url}")
113 +
72 114 def fetch(self) -> list[Listing]:
73 115 listings: dict[str, Listing] = {}
74 116
75 − # 1) Liste paginée des unités disponibles
117 + # 1) Liste paginée des unités disponibles — la 1re page échoue
118 + # franchement (fini le « 0 trouvé ok » silencieux du challenge)
76 119 url = LIST_URL
77 − for _ in range(self.max_pages):
78 − try:
79 − html = self.get(url).text
80 − except Exception:
81 − break
120 + for page in range(self.max_pages):
121 + if page == 0:
122 + html = self._get_html(url)
123 + else:
124 + try:
125 + html = self._get_html(url)
126 + except Exception:
127 + break
82 128 soup = BeautifulSoup(html, "html.parser")
83 129 for it in soup.select("div.item-listing-wrap"):
84 130 try:
@@ -125,7 +171,8 @@ class CromwellConnector(BaseConnector):
125 171
126 172 # -- fiche détaillée (thème Houzez) ------------------------------------------
127 173 def _fetch_detail(self, url: str) -> dict:
128 − detail = self.get(url).text
174 + # _get_html lève sur challenge : pas de payload vide en detail_cache
175 + detail = self._get_html(url)
129 176 payload: dict = {}
130 177 imgs = [u for u in dict.fromkeys(IMG_RE.findall(detail))
131 178 if not re.search(r"logo|favicon|icon|-\d+x\d+\.", u, re.I)]
132 179