[ka2] fix connecteur brio: contourner la variante brotli figée du cache Cloudflare qui rendait la page illisible (faux « 0 trouvé ok »). Le sync du 2026-09-07 14:21 a rapporté found=0 ok=1 (missed=3, médiane 3) : reproduit en live — le cache CDN d'immeublesbrio.com sert pour /appartements-a-louer-val-belair/ une variante brotli figée (content-encoding: br, cf-cache-status HIT, age ~11 j, expire 2026-10-08) MÊME quand le client n'annonce que gzip (Vary ignoré) ; requests ne décode pas br → .text binaire → 0 hotspot-info parsé, invisible du disjoncteur d'ingest.py. La source est saine : un paramètre cache-buster force un MISS et l'origine renvoie du gzip valide (49 hotspots). Fix : _html() vérifie un marqueur attendu ('hotspot-info' / 'partir de'), retente avec cache-buster si absent, puis RuntimeError franche ; garde supplémentaire si aucun hotspot-info (une page complète en contient toujours, Disponible ET Loué — même pattern que gwlra/le_fjord/logiluxx). Testé : run.py sync brio → found=3 ok (App. 109 5½ 1 850 $, App. 303 4½ 1 625 $, App. 408 3½ 1 500 $, missed remis à 0), pm2 restart lou-ka-sync, site :8095 → 200.
1 changed file +31 −3
modified
louka/connectors/brio.py
+31 −3
@@ -10,6 +10,7 @@ | ||
| 10 | 10 | from __future__ import annotations |
| 11 | 11 | |
| 12 | 12 | import re |
| 13 | +import time | |
| 13 | 14 | |
| 14 | 15 | from bs4 import BeautifulSoup |
| 15 | 16 | |
@@ -28,6 +29,26 @@ class BrioConnector(BaseConnector): | ||
| 28 | 29 | source_id = "brio" |
| 29 | 30 | request_delay = 0.6 |
| 30 | 31 | |
| 32 | + def _html(self, url: str, marker: str) -> str: | |
| 33 | + """HTML de `url`, avec contournement du cache CDN empoisonné. | |
| 34 | + | |
| 35 | + Vu 2026-09-07 : le cache Cloudflare du site sert une variante | |
| 36 | + brotli figée (content-encoding: br, cf-cache-status HIT) même si | |
| 37 | + le client n'annonce que gzip — requests ne peut pas la décoder et | |
| 38 | + .text devient du binaire (0 annonce parsée, faux « 0 trouvé ok »). | |
| 39 | + Si le marqueur attendu est absent, on force un MISS via un | |
| 40 | + paramètre cache-buster ; s'il manque toujours, échec franc. | |
| 41 | + """ | |
| 42 | + html = self.get(url).text | |
| 43 | + if marker in html: | |
| 44 | + return html | |
| 45 | + html = self.get(url, params={"lkb": int(time.time())}).text | |
| 46 | + if marker not in html: | |
| 47 | + raise RuntimeError( | |
| 48 | + f"brio : contenu inattendu (marqueur {marker!r} absent, " | |
| 49 | + f"cache CDN illisible ?) — {url}") | |
| 50 | + return html | |
| 51 | + | |
| 31 | 52 | def fetch(self) -> list[Listing]: |
| 32 | 53 | # 1) Page d'accueil : prix « à partir de » par type (ex. « 3½ à |
| 33 | 54 | # partir de 1500$ »), services de l'immeuble (blurbs Divi) et |
@@ -36,7 +57,7 @@ class BrioConnector(BaseConnector): | ||
| 36 | 57 | services: list[str] = [] |
| 37 | 58 | contact: dict = {} |
| 38 | 59 | try: |
| 39 | − home = self.get(HOME_URL).text | |
| 60 | + home = self._html(HOME_URL, "partir de") | |
| 40 | 61 | home_txt = re.sub(r"<[^>]+>", " ", home) |
| 41 | 62 | for m in re.finditer(r"(\d)\s*½\s*à\s*partir\s*de\s*([\d\s ]+)\$", |
| 42 | 63 | home_txt): |
@@ -58,9 +79,16 @@ class BrioConnector(BaseConnector): | ||
| 58 | 79 | pass |
| 59 | 80 | |
| 60 | 81 | # 2) Page des appartements : hotspots par étage |
| 61 | − html = self.get(LIST_URL).text | |
| 82 | + html = self._html(LIST_URL, "hotspot-info") | |
| 62 | 83 | soup = BeautifulSoup(html, "html.parser") |
| 63 | 84 | |
| 85 | + infos = soup.select("div.hotspot-info") | |
| 86 | + if not infos: | |
| 87 | + # une page complète contient toujours les hotspots (Disponible | |
| 88 | + # ET Loué) — leur absence = rendu partiel, pas un inventaire vide | |
| 89 | + raise RuntimeError("brio : aucun hotspot-info dans la page des " | |
| 90 | + "appartements (rendu partiel ?)") | |
| 91 | + | |
| 64 | 92 | # Galerie générale de l'immeuble (photos des pièces) |
| 65 | 93 | gallery = [] |
| 66 | 94 | for img in soup.select("img"): |
@@ -71,7 +99,7 @@ class BrioConnector(BaseConnector): | ||
| 71 | 99 | gallery = list(dict.fromkeys(gallery))[:8] |
| 72 | 100 | |
| 73 | 101 | listings: list[Listing] = [] |
| 74 | − for info in soup.select("div.hotspot-info"): | |
| 102 | + for info in infos: | |
| 75 | 103 | try: |
| 76 | 104 | title_el = info.select_one(".hotspot-title") |
| 77 | 105 | if not title_el: |
| 78 | 106 | |