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 boreal_abitibi: contournement du challenge sgcaptcha SiteGround via proxy résidentiel Oxylabs. Le sync du 2026-09-13 01:07 échouait honnêtement (« ajax_object introuvable sur /locations/ », ok=0) : SiteGround sert désormais son anti-bot sgcaptcha (HTTP 202 + meta refresh vers /.well-known/sgcaptcha/, en-tête sg-captcha: challenge) à l'IP datacenter du nœud — un 2xx qui ne lève pas d'HTTPError, donc invisible du wrapper résilient (même angle mort que brio/Sucuri 195f19a). Vérifié live : le challenge frappe tout UA depuis notre IP (76.70.60.50 flaguée « ipr/ipc »), mais une IP résidentielle Oxylabs CA passe (200, 52 Ko, ajax_object présent) et le POST admin-ajax get_rents fonctionne sur la même session collante (nonce af45b697e0 → 1 rent). Fix minimal dans le connecteur : _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 (nonce + POST admin-ajax + fiches détail partagent l'IP, pattern fb_marketplace/_resilient), erreur franche si le challenge persiste ; _fetch_detail() lève aussi sur challenge pour ne pas empoisonner detail_cache avec un payload vide (clé figée jamais invalidée). Le chemin direct reste tenté d'abord (gratuit si SiteGround déflague l'IP). Testé : fetch() venv → 1 annonce (App. 205 Esplanade Val-d'Or, 3½, 1 465 $, dispo octobre, 11 photos — médiane 1.0), pytest fixtures 2 passed, run.py sync boreal_abitibi → found=1 ok, pm2 restart lou-ka-sync, site :8095 → 200.

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

1 changed file +42 −2

modified louka/connectors/boreal_abitibi.py +42 −2
@@ -13,13 +13,19 @@
13 13 # parking, inclusions, description et galerie ; la mention « logements
14 14 # non-fumeur / aucun animal » du site alimente `pets`.
15 15 # robots.txt WP standard (admin-ajax.php explicitement permis), 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 +# en session collante (le nonce et les POST admin-ajax partagent l'IP).
16 20 # -----------------------------------------------------------------------------
17 21 from __future__ import annotations
18 22
19 23 import hashlib
20 24 import json
25 +import os
21 26 import re
22 27 import time
28 +from urllib.parse import quote
23 29
24 30 from bs4 import BeautifulSoup
25 31
@@ -35,6 +41,15 @@ _SIZE_SUFFIX = re.compile(r"-\d{2,4}x\d{2,4}(?=\.(?:jpg|jpeg|png|webp)$)", re.I)
35 41 _AJAX_OBJECT_RE = re.compile(r"var ajax_object\s*=\s*(\{.*?\})\s*;?", re.S)
36 42 # « 4 et demi » (fiche) -> « 4 1/2 » pour la normalisation commune
37 43 _ET_DEMI_RE = re.compile(r"^(\d)\s*et\s*demi$", re.I)
44 +# page interstitielle sgcaptcha SiteGround (meta refresh vers /.well-known/…)
45 +_SG_MARKER = "/.well-known/sgcaptcha/"
46 +
47 +
48 +def _sg_challenge(resp) -> bool:
49 + """True si la réponse est le challenge anti-bot SiteGround (202 + JS)."""
50 + if "challenge" in str(resp.headers.get("sg-captcha", "")).lower():
51 + return True
52 + return _SG_MARKER in (resp.text or "")[:600]
38 53
39 54
40 55 class BorealAbitibiConnector(BaseConnector):
@@ -53,9 +68,33 @@ class BorealAbitibiConnector(BaseConnector):
53 68 resp.raise_for_status()
54 69 return resp
55 70
71 + def _enable_residential_proxy(self) -> bool:
72 + """Route toute la session via Oxylabs résidentiel CA (session collante)."""
73 + endpoint = os.environ.get("OXYLABS_PROXY")
74 + user = os.environ.get("OXYLABS_PROXY_USER")
75 + pwd = os.environ.get("OXYLABS_PROXY_PASS")
76 + if not (endpoint and user and pwd):
77 + return False
78 + puser = f"{user}-cc-CA-sessid-boreal{int(time.time())}-sesstime-10"
79 + proxy = f"http://{quote(puser, safe='')}:{quote(pwd, safe='')}@{endpoint}"
80 + self.session.proxies = {"http": proxy, "https": proxy}
81 + self.session.verify = False # CA MITM du proxy Oxylabs (cf. _resilient)
82 + return True
83 +
84 + def _get_html(self, url: str) -> str:
85 + """GET avec contournement du challenge sgcaptcha (proxy résidentiel)."""
86 + resp = self.get(url)
87 + if not _sg_challenge(resp):
88 + return resp.text
89 + if not self.session.proxies and self._enable_residential_proxy():
90 + resp = self.get(url)
91 + if not _sg_challenge(resp):
92 + return resp.text
93 + raise RuntimeError(f"challenge sgcaptcha non contourné — {url}")
94 +
56 95 def fetch(self) -> list[Listing]:
57 96 # 1) page /locations/ : récupérer le nonce AJAX (grille vide côté serveur)
58 − html = self.get(LIST_URL).text
97 + html = self._get_html(LIST_URL)
59 98 m = _AJAX_OBJECT_RE.search(html)
60 99 if not m:
61 100 raise RuntimeError("ajax_object introuvable sur /locations/")
@@ -173,7 +212,8 @@ class BorealAbitibiConnector(BaseConnector):
173 212 if self._fetched >= self.max_details:
174 213 raise RuntimeError("budget de fiches détail atteint")
175 214 self._fetched += 1
176 − html = self.get(url).text
215 + # lever sur challenge : sinon detail() mettrait en cache un payload vide
216 + html = self._get_html(url)
177 217 soup = BeautifulSoup(html, "html.parser")
178 218 out: dict = {}
179 219
180 220