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 deschenes_pepin: contournement du challenge sgcaptcha SiteGround via proxy résidentiel Oxylabs — même cause et même fix que boreal_abitibi (8923045), citiluxx (d17f5fe) et cromwell (8351131), hébergeur SiteGround identique. Le sync du 2026-09-13 01:56 a rapporté found=0 ok=1 missed=5 (médiane 5.0, stale) : SiteGround sert son anti-bot sgcaptcha (HTTP 202, 174 octets, en-tête sg-captcha: challenge, meta refresh vers /.well-known/sgcaptcha/) à l'IP datacenter du nœud — un 2xx sans HTTPError, 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, 234 Ko, les 2 liens /projet/ (Le Blüm Longueuil + Sainte-Julie) présents. Fix minimal, pattern cromwell : _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 projet partagent l'IP), erreur franche si le challenge persiste — la page de liste lève désormais au lieu de retourner [] (fini le zéro silencieux, ingest journalise ok=0), les fiches projet gardent le continue (résultats partiels acceptables) ; _fetch_project() passe aussi par _get_html(). Le chemin direct reste tenté d'abord (gratuit si SiteGround déflague l'IP). Testé : fetch() venv → 5 annonces (Le Blüm Sainte-Julie 3½/4½, Le Blüm Longueuil 3½/4½/5½, photos 12-20, adresses complètes — médiane 5.0 restaurée), run.py sync deschenes_pepin → found=5 ok, pm2 restart lou-ka-sync, site :8095 → 200.

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

1 changed file +45 −5

modified louka/connectors/deschenes_pepin.py +45 −5
@@ -9,10 +9,17 @@
9 9 # 5 ½ »), l'adresse du bureau d'information, la description et la galerie.
10 10 # Granularité : une annonce par projet × typologie (pas d'unités ni de prix
11 11 # publiés sur le site).
12 +# 2026-09-13 : SiteGround sert son anti-bot sgcaptcha (202 + challenge JS,
13 +# en-tête sg-captcha) aux IP datacenter — invisible du wrapper résilient
14 +# (2xx) ; on bascule alors la session sur un proxy résidentiel Oxylabs CA
15 +# (même pattern que boreal_abitibi/citiluxx/cromwell, hébergeur identique).
12 16 # -----------------------------------------------------------------------------
13 17 from __future__ import annotations
14 18
19 +import os
15 20 import re
21 +import time
22 +from urllib.parse import quote
16 23
17 24 from bs4 import BeautifulSoup
18 25
@@ -35,6 +42,16 @@ ADDR_RE = re.compile(
35 42 r"mont[ée]e|place)\s+[^,<>]{2,60},?\s*(?:Longueuil|Sainte-Julie|Ste-Julie)"
36 43 r"[^<>]{0,40}?(?:[A-Z]\d[A-Z]\s?\d[A-Z]\d)?", re.I)
37 44
45 +# page interstitielle sgcaptcha SiteGround (meta refresh vers /.well-known/…)
46 +_SG_MARKER = "/.well-known/sgcaptcha/"
47 +
48 +
49 +def _sg_challenge(resp) -> bool:
50 + """True si la réponse est le challenge anti-bot SiteGround (202 + JS)."""
51 + if "challenge" in str(resp.headers.get("sg-captcha", "")).lower():
52 + return True
53 + return _SG_MARKER in (resp.text or "")[:600]
54 +
38 55
39 56 def _city_of(slug: str, text: str) -> str:
40 57 key = f"{slug} {text}".lower()
@@ -48,12 +65,35 @@ class DeschenesPepinConnector(BaseConnector):
48 65 request_delay = 0.8
49 66 max_projects = 12 # garde-fou de crawl
50 67
68 + def _enable_residential_proxy(self) -> bool:
69 + """Route toute la session via Oxylabs résidentiel CA (session collante)."""
70 + endpoint = os.environ.get("OXYLABS_PROXY")
71 + user = os.environ.get("OXYLABS_PROXY_USER")
72 + pwd = os.environ.get("OXYLABS_PROXY_PASS")
73 + if not (endpoint and user and pwd):
74 + return False
75 + puser = f"{user}-cc-CA-sessid-deschenes{int(time.time())}-sesstime-10"
76 + proxy = f"http://{quote(puser, safe='')}:{quote(pwd, safe='')}@{endpoint}"
77 + self.session.proxies = {"http": proxy, "https": proxy}
78 + self.session.verify = False # CA MITM du proxy Oxylabs (cf. _resilient)
79 + return True
80 +
81 + def _get_html(self, url: str) -> str:
82 + """GET avec contournement du challenge sgcaptcha (proxy résidentiel)."""
83 + resp = self.get(url)
84 + if not _sg_challenge(resp):
85 + return resp.text
86 + if not self.session.proxies and self._enable_residential_proxy():
87 + resp = self.get(url)
88 + if not _sg_challenge(resp):
89 + return resp.text
90 + raise RuntimeError(f"challenge sgcaptcha non contourné — {url}")
91 +
51 92 def fetch(self) -> list[Listing]:
52 93 listings: list[Listing] = []
53 − try:
54 − html = self.get(LIST_URL).text
55 − except Exception:
56 − return listings
94 + # la page de liste échoue franchement (fini le « 0 trouvé ok »
95 + # silencieux du challenge sgcaptcha — ingest journalise ok=0)
96 + html = self._get_html(LIST_URL)
57 97
58 98 # Slugs des fiches projet référencées depuis la section « Louer »
59 99 slugs = list(dict.fromkeys(PROJECT_LINK_RE.findall(html)))
@@ -66,7 +106,7 @@ class DeschenesPepinConnector(BaseConnector):
66 106
67 107 def _fetch_project(self, slug: str) -> list[Listing]:
68 108 url = f"{BASE}/projet/{slug}/"
69 − html = self.get(url).text
109 + html = self._get_html(url)
70 110 soup = BeautifulSoup(html, "html.parser")
71 111
72 112 h1 = soup.find("h1")
73 113