[ka6] fix connecteur lassonde: garde anti faux-zéro sur la page liste — le site (derrière Sucuri depuis 2026-08) sert parfois un 200 sans la section emplois ni marqueur de challenge connu (constaté 2026-09-05 21h09, sync à 0 alors que 32 offres réelles, is_blocked muet), le conteneur 'job-result-default' est maintenant exigé avec retry+backoff sinon erreur explicite dans sync_log; sync rejoué: 32 offres (médiane 32.0)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 changed file +31 −2
modified
jobka/connectors/lassonde.py
+31 −2
@@ -13,13 +13,19 @@ | ||
| 13 | 13 | # (challenge JS servi en 307 sans Location → 0 offre en direct) — |
| 14 | 14 | # fetch via get_resilient(render_js=True) : direct d'abord, |
| 15 | 15 | # escalade Scrapfly ASP+rendu JS si challenge. |
| 16 | −# Créé : 2026-08-18 Modifié : 2026-08-31 | |
| 16 | +# 2026-09-05 : garde anti faux-zéro sur la page liste — le site sert | |
| 17 | +# parfois un 200 sans la section emplois ni marqueur Sucuri connu | |
| 18 | +# (sync à 0 le 2026-09-05 21h09 alors que 32 offres réelles) ; le | |
| 19 | +# conteneur `job-result-default` est maintenant exigé, retry+backoff | |
| 20 | +# sinon erreur explicite dans sync_log. | |
| 21 | +# Créé : 2026-08-18 Modifié : 2026-09-05 | |
| 17 | 22 | # ============================================================================= |
| 18 | 23 | from __future__ import annotations |
| 19 | 24 | |
| 20 | 25 | import html as _html |
| 21 | 26 | import os |
| 22 | 27 | import re |
| 28 | +import time | |
| 23 | 29 | |
| 24 | 30 | from ..schema import JobPosting |
| 25 | 31 | from . import _jsonld |
@@ -62,10 +68,33 @@ class LassondeConnector(BaseConnector): | ||
| 62 | 68 | return {} |
| 63 | 69 | return {"description_html": m.group(1)[:60000]} |
| 64 | 70 | |
| 71 | + def _fetch_list(self) -> str: | |
| 72 | + # Le site sert parfois un 200 sans la section emplois et sans aucun | |
| 73 | + # marqueur Sucuri connu (constaté le 2026-09-05 21h09 : sync à 0 offre | |
| 74 | + # alors que la page réelle en listait 32). Une vraie page liste porte | |
| 75 | + # toujours le conteneur `job-result-default` (avec le compteur | |
| 76 | + # filters-list-count), même si l'employeur n'affichait aucune offre : | |
| 77 | + # son absence = challenge/page tronquée, pas une liste vide — on | |
| 78 | + # retente avec backoff puis on lève une erreur explicite plutôt que | |
| 79 | + # de rapporter un faux « 0 offre ». | |
| 80 | + last = "" | |
| 81 | + for attempt in range(3): | |
| 82 | + resp = self.get_resilient(self.LIST_URL, render_js=True) | |
| 83 | + html = resp.text | |
| 84 | + if "job-result-default" in html: | |
| 85 | + return html | |
| 86 | + last = (f"HTTP {resp.status_code} via " | |
| 87 | + f"{getattr(resp, 'via', 'direct')}, {len(html)} octets") | |
| 88 | + if attempt < 2: | |
| 89 | + time.sleep(10 * (attempt + 1)) | |
| 90 | + raise RuntimeError( | |
| 91 | + "lassonde: page liste sans la section emplois " | |
| 92 | + f"(job-result-default) après 3 essais ({last})") | |
| 93 | + | |
| 65 | 94 | def fetch(self) -> list[JobPosting]: |
| 66 | 95 | # Sucuri Cloudproxy depuis 2026-08 : rendu JS obligatoire (challenge |
| 67 | 96 | # cookie), le direct est retenté d'abord à chaque cycle (gratuit) |
| 68 | − page = self.get_resilient(self.LIST_URL, render_js=True).text | |
| 97 | + page = self._fetch_list() | |
| 69 | 98 | out: list[JobPosting] = [] |
| 70 | 99 | details_used = 0 |
| 71 | 100 | seen: set[str] = set() |
| 72 | 101 | |