[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 | # (challenge JS servi en 307 sans Location → 0 offre en direct) — | 13 | # (challenge JS servi en 307 sans Location → 0 offre en direct) — | ||
| 14 | # fetch via get_resilient(render_js=True) : direct d'abord, | 14 | # fetch via get_resilient(render_js=True) : direct d'abord, | ||
| 15 | # escalade Scrapfly ASP+rendu JS si challenge. | 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 | from __future__ import annotations | 23 | from __future__ import annotations | ||
| 19 | 24 | ||||
| 20 | import html as _html | 25 | import html as _html | ||
| 21 | import os | 26 | import os | ||
| 22 | import re | 27 | import re | ||
| 28 | +import time | ||||
| 23 | 29 | ||||
| 24 | from ..schema import JobPosting | 30 | from ..schema import JobPosting | ||
| 25 | from . import _jsonld | 31 | from . import _jsonld | ||
@@ -62,10 +68,33 @@ class LassondeConnector(BaseConnector): | |||||
| 62 | return {} | 68 | return {} | ||
| 63 | return {"description_html": m.group(1)[:60000]} | 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 | def fetch(self) -> list[JobPosting]: | 94 | def fetch(self) -> list[JobPosting]: | ||
| 66 | # Sucuri Cloudproxy depuis 2026-08 : rendu JS obligatoire (challenge | 95 | # Sucuri Cloudproxy depuis 2026-08 : rendu JS obligatoire (challenge | ||
| 67 | # cookie), le direct est retenté d'abord à chaque cycle (gratuit) | 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 | out: list[JobPosting] = [] | 98 | out: list[JobPosting] = [] | ||
| 70 | details_used = 0 | 99 | details_used = 0 | ||
| 71 | seen: set[str] = set() | 100 | seen: set[str] = set() | ||
| 72 | 101 | ||||