[ka2] fix connecteur gwlra: retry (3 essais, backoff 5/10 s) du sitemap et des pages floorplans — un raté transitoire Scrapfly (vu 2026-08-30 ~04:20) produisait un faux « 0 trouvé ok » (médiane 1) car get_scrapfly retourne "" en silence, _building retourne [] sur non-200/contenu vide et fetch() avalait les exceptions par immeuble; désormais erreur franche si le sitemap ne donne aucune page QC après 3 essais, si une page floorplans échoue 3 fois (seul un 404/410 vaut « immeuble retiré ») ou si tous les immeubles sont en erreur. Testé: run.py sync gwlra → 1 annonce (= médiane 1)
1 changed file +49 −14
modified
louka/connectors/gwlra.py
+49 −14
@@ -16,6 +16,7 @@ from __future__ import annotations | ||
| 16 | 16 | |
| 17 | 17 | import json |
| 18 | 18 | import re |
| 19 | +import time | |
| 19 | 20 | |
| 20 | 21 | from bs4 import BeautifulSoup |
| 21 | 22 | |
@@ -46,28 +47,62 @@ class GwlraConnector(BaseConnector): | ||
| 46 | 47 | use_detail_cache = False # tout passe par Scrapfly, pages fraîches |
| 47 | 48 | |
| 48 | 49 | def fetch(self) -> list[Listing]: |
| 49 | − sitemap = self.get_scrapfly(SITEMAP_URL, render_js=False) | |
| 50 | − pages = {} # slug -> (ville, chemin floorplans) | |
| 51 | − for city, slug in FLOORPLAN_PATH_RE.findall(sitemap): | |
| 52 | − pages.setdefault(slug, (city, f"apartments/qc/{city}/{slug}" | |
| 53 | − "/floorplans")) | |
| 50 | + # Un raté transitoire (Scrapfly vide, vu 2026-08-30 ~04:20) produisait | |
| 51 | + # un faux « 0 trouvé ok » (médiane 1) : retry du sitemap (3 essais, | |
| 52 | + # backoff 5/10 s) puis erreur franche plutôt qu'un zéro silencieux. | |
| 53 | + pages: dict[str, tuple[str, str]] = {} # slug -> (ville, chemin) | |
| 54 | + for attempt in range(3): | |
| 55 | + try: | |
| 56 | + sitemap = self.get_scrapfly(SITEMAP_URL, render_js=False) | |
| 57 | + except Exception: | |
| 58 | + if attempt == 2: | |
| 59 | + raise | |
| 60 | + sitemap = "" | |
| 61 | + for city, slug in FLOORPLAN_PATH_RE.findall(sitemap): | |
| 62 | + pages.setdefault(slug, (city, f"apartments/qc/{city}/{slug}" | |
| 63 | + "/floorplans")) | |
| 64 | + if pages: | |
| 65 | + break | |
| 66 | + if attempt < 2: | |
| 67 | + time.sleep(5 * (attempt + 1)) | |
| 68 | + if not pages: | |
| 69 | + raise RuntimeError( | |
| 70 | + "sitemap sans page floorplans QC après 3 essais") | |
| 71 | + | |
| 54 | 72 | listings: list[Listing] = [] |
| 73 | + errors = 0 | |
| 74 | + last_exc: Exception | None = None | |
| 55 | 75 | for slug, (city_slug, path) in sorted(pages.items()): |
| 56 | 76 | try: |
| 57 | 77 | listings.extend(self._building(slug, city_slug, |
| 58 | 78 | f"{BASE}/{path}")) |
| 59 | − except Exception: | |
| 60 | − continue | |
| 79 | + except Exception as exc: | |
| 80 | + errors += 1 | |
| 81 | + last_exc = exc | |
| 82 | + # tous les immeubles en erreur et rien récolté : erreur franche | |
| 83 | + if not listings and last_exc is not None and errors == len(pages): | |
| 84 | + raise last_exc | |
| 61 | 85 | return listings |
| 62 | 86 | |
| 63 | 87 | def _building(self, slug: str, city_slug: str, url: str) -> list[Listing]: |
| 64 | − # le sitemap contient des immeubles retirés (404) : vérifier le statut | |
| 65 | − result = self.scrapfly(url, render_js=True) | |
| 66 | − if result.get("status_code") != 200: | |
| 67 | − return [] | |
| 68 | − html = result.get("content") or "" | |
| 69 | − if not html: | |
| 70 | − return [] | |
| 88 | + # le sitemap contient des immeubles retirés (404) : vérifier le statut. | |
| 89 | + # Les ratés transitoires (5xx, contenu vide) sont retentés 3 fois puis | |
| 90 | + # remontés en erreur franche — seul un 404/410 vaut « immeuble retiré ». | |
| 91 | + html = "" | |
| 92 | + for attempt in range(3): | |
| 93 | + result = self.scrapfly(url, render_js=True) | |
| 94 | + status = result.get("status_code") | |
| 95 | + if status in (404, 410): | |
| 96 | + return [] | |
| 97 | + html = result.get("content") or "" | |
| 98 | + if status == 200 and html: | |
| 99 | + break | |
| 100 | + if attempt < 2: | |
| 101 | + time.sleep(5 * (attempt + 1)) | |
| 102 | + else: | |
| 103 | + raise RuntimeError( | |
| 104 | + f"page floorplans {url} en échec après 3 essais " | |
| 105 | + f"(statut {status!r}, contenu {'vide' if not html else 'ok'})") | |
| 71 | 106 | soup = BeautifulSoup(html, "html.parser") |
| 72 | 107 | |
| 73 | 108 | # JSON-LD ApartmentComplex : nom, adresse, animaux, détail des plans |
| 74 | 109 | |