SPB Git

Repository Public

Commit

[ka2] fix connecteur solaris: variante ANGLAISE servie à l'URL française par le cache Cloudflare → parsing bilingue + rotation d'IP au retry. Au sync de 09:37 le garde-fou levait « aucune unité parsée alors que data-count=8 » sur les 3 tentatives (session proxy Oxylabs collante du fix 99c1a4b) : la page /appartements/ a été servie en ANGLAIS (gabarit identique, data-count=8, mais « 2 bedroom · 4½ 875 sq ft $2,140 a month Free now Photos and plan Visit ») → 0/8 pour ROW_RE. Constats live : le site a une version EN (/en/apartments/, hreflang) exactement à cette signature ; Cloudflare cache les deux variantes SANS Vary et sa clé de cache IGNORE la query string (cf-cache-status: HIT même sur ?lkb=<unique>) → le cache-buster du retry 8eff094 ne buste RIEN, et la session collante (même sortie/colo) revoyait la même variante empoisonnée aux 3 tentatives ; Accept-Language et géo de l'IP (testé Toronto vs Montréal) ne changent pas la réponse de façon déterministe. Fix 1 : parsing bilingue — ROW_EN_RE (ordre des champs inversé : chambres avant typologie, prix $2,140 avec virgule, sq ft, « a month ») en repli par ligne quand ROW_RE ne matche pas, avec renormalisation de la dispo en français canonique (_avail_fr : « Free now » → « Libre maintenant », « Free on Nov 1, 2026 » → « Libre le 1er novembre 2026 ») — parité vérifiée 8/8 FR↔EN sur les deux pages live (prix/typologie/chambres/pi²/dispo strictement identiques, zéro churn). Fix 2 : au retry, si la session est sur proxy, NOUVELLE session collante Oxylabs (_enable_residential_proxy re-appelé → autre IP/colo) — le vrai levier contre une variante en cache, le ?lkb= étant inopérant ; aide aussi contre les sorties résidentielles tarpitées (000 constatés ~1 essai sur 3 en test curl). Fixtures inchangées (page FR), pytest solaris 2 passed. Testé : fetch() venv → 8 unités (1 630–2 730 $, dispos FR), run.py sync solaris → found=8 ok (14,5 s), pm2 restart lou-ka-sync, site :8095 → 200.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Simon-Pierre Boucher committed · Oct 25, 2026 parent 99c1a4b
1 changed file +46 −6 Browse files
modified louka/connectors/solaris.py +46 −6
@@ -39,8 +39,38 @@ ROW_RE = re.compile(
39 39 r"\s*([\d\s]*?)\s*pi²"
40 40 r"\s*([\d\s]+?)\s*\$\s*/\s*mois"
41 41 r"\s*(Libre.*?)\s*(?:Photos\s+et\s+plan|Plan)\b", re.S)
42 +# 2026-10-25 : Cloudflare sert PARFOIS la variante ANGLAISE à l'URL française
43 +# (cache sans Vary, clé de cache qui IGNORE la query string — le cache-buster
44 +# ?lkb= ne buste donc rien). Même data-count=8 mais « 2 bedroom · 4½ 875 sq ft
45 +# $2,140 a month Free now Photos and plan » → 0/8 pour ROW_RE. On parse les
46 +# deux langues (données identiques) et on renormalise la dispo en français.
47 +ROW_EN_RE = re.compile(
48 + r"(\d)\s*bedrooms?\s*·\s*(\d)\s*½"
49 + r"\s*([\d,\s]*?)\s*sq\s*ft"
50 + r"\s*\$\s*([\d,\s]+?)\s*(?:a|per)\s+month"
51 + r"\s*(Free.*?)\s*(?:Photos\s+and\s+plan|Plan)\b", re.S)
42 52 IMG_RE = re.compile(r'<img[^>]+src="([^"]+\.webp)"', re.I)
43 53
54 +_EN_MONTHS = {"jan": "janvier", "feb": "février", "mar": "mars",
55 + "apr": "avril", "may": "mai", "jun": "juin", "jul": "juillet",
56 + "aug": "août", "sep": "septembre", "oct": "octobre",
57 + "nov": "novembre", "dec": "décembre"}
58 +
59 +
60 +def _avail_fr(avail: str) -> str:
61 + """« Free now » / « Free on Nov 1, 2026 » → texte français canonique."""
62 + if re.match(r"free\s+now", avail, re.I):
63 + return "Libre maintenant"
64 + m = re.match(r"free\s+on\s+([A-Za-z]+)\.?\s+(\d{1,2}),?\s+(\d{4})",
65 + avail, re.I)
66 + if m:
67 + month = _EN_MONTHS.get(m.group(1)[:3].lower())
68 + if month:
69 + day = int(m.group(2))
70 + jour = "1er" if day == 1 else str(day)
71 + return f"Libre le {jour} {month} {m.group(3)}"
72 + return avail
73 +
44 74
45 75 def _flat(fragment: str) -> str:
46 76 text = _html.unescape(fragment)
@@ -96,6 +126,10 @@ class SolarisConnector(BaseConnector):
96 126 for attempt in range(3):
97 127 if attempt:
98 128 time.sleep(4 * attempt)
129 + if self.session.proxies:
130 + # le cache CF ignore la query string → le vrai levier de
131 + # retry est une NOUVELLE IP collante (autre sortie/colo).
132 + self._enable_residential_proxy()
99 133 url = PLANS_URL if attempt == 0 else \
100 134 f"{PLANS_URL}?lkb={int(time.time())}"
101 135 try:
@@ -131,19 +165,25 @@ class SolarisConnector(BaseConnector):
131 165
132 166 listings: list[Listing] = []
133 167 for num, body in UNIT_LI_RE.findall(section):
134 − row = ROW_RE.search(_flat(body))
135 − if not row:
136 − continue
137 − n, beds, sqft, amount, avail = row.groups()
168 + text = _flat(body)
169 + row = ROW_RE.search(text)
170 + if row:
171 + n, beds, sqft, amount, avail = row.groups()
172 + else:
173 + row = ROW_EN_RE.search(text)
174 + if not row:
175 + continue
176 + beds, n, sqft, amount, avail = row.groups()
177 + avail = _avail_fr(avail)
138 178 try:
139 − price = float(re.sub(r"\s", "", amount))
179 + price = float(re.sub(r"[\s,]", "", amount))
140 180 except ValueError:
141 181 continue
142 182 if not 300 <= price <= 20000:
143 183 continue
144 184 unit_type = f"{n}½"
145 185 area = None
146 − sqft = re.sub(r"\s", "", sqft)
186 + sqft = re.sub(r"[\s,]", "", sqft)
147 187 if sqft.isdigit():
148 188 area = float(sqft)
149 189 imgs = [u for u in plan_imgs
150 190
Menu

Theme