[ka6] fix connecteur lassonde: site passé derrière Sucuri Cloudproxy (challenge JS en 307 sans Location → 0 offre) — détection Sucuri dans is_blocked (_resilient) + fetch via get_resilient(render_js=True), 32 offres retrouvées
2 changed files +16 −6
modified
jobka/connectors/_resilient.py
+7 −3
@@ -53,13 +53,16 @@ except Exception: # noqa: BLE001 | |||||
| 53 | # 401 = auth manquante, 403/429 = bot bloqué, 5xx CF = challenge/edge). | 53 | # 401 = auth manquante, 403/429 = bot bloqué, 5xx CF = challenge/edge). | ||
| 54 | BLOCK_STATUS = {403, 429, 503, 520, 521, 522, 523, 524, 526, 1020} | 54 | BLOCK_STATUS = {403, 429, 503, 520, 521, 522, 523, 524, 526, 1020} | ||
| 55 | 55 | ||||
| 56 | −# Marqueurs de page-challenge (Cloudflare/Akamai/Incapsula/PerimeterX/DataDome). | 56 | +# Marqueurs de page-challenge (Cloudflare/Akamai/Incapsula/PerimeterX/DataDome/ | ||
| 57 | +# Sucuri). Sucuri Cloudproxy sert son challenge JS en 307 SANS Location | ||||
| 58 | +# (vu sur lassonde.com 2026-08-31) : injecte un cookie via JS obfusqué. | ||||
| 57 | _CHALLENGE_MARKERS = ( | 59 | _CHALLENGE_MARKERS = ( | ||
| 58 | "just a moment", "cf-browser-verification", "cf-challenge", | 60 | "just a moment", "cf-browser-verification", "cf-challenge", | ||
| 59 | "attention required", "access denied", "request unsuccessful", | 61 | "attention required", "access denied", "request unsuccessful", | ||
| 60 | "px-captcha", "perimeterx", "incapsula", "_incapsula_", "datadome", | 62 | "px-captcha", "perimeterx", "incapsula", "_incapsula_", "datadome", | ||
| 61 | "captcha-delivery", "please enable javascript and cookies", | 63 | "captcha-delivery", "please enable javascript and cookies", | ||
| 62 | "checking your browser", "ddos protection by", | 64 | "checking your browser", "ddos protection by", | ||
| 65 | + "sucuri_cloudproxy", "you are being redirected", | ||||
| 63 | ) | 66 | ) | ||
| 64 | 67 | ||||
| 65 | _UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " | 68 | _UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " | ||
@@ -169,8 +172,9 @@ def is_blocked(resp) -> bool: | |||||
| 169 | code = getattr(resp, "status_code", 0) or 0 | 172 | code = getattr(resp, "status_code", 0) or 0 | ||
| 170 | if code in BLOCK_STATUS: | 173 | if code in BLOCK_STATUS: | ||
| 171 | return True | 174 | return True | ||
| 172 | − # 200 mais page-challenge servie | 175 | + # 200 mais page-challenge servie — ou 307 « sans Location » de Sucuri | ||
| 173 | − if code == 200: | 176 | + # (requests ne peut pas la suivre : le corps EST le challenge JS) | ||
| 177 | + if code in (200, 307): | ||||
| 174 | try: | 178 | try: | ||
| 175 | body = (resp.text or "")[:4000].lower() | 179 | body = (resp.text or "")[:4000].lower() | ||
| 176 | except Exception: # noqa: BLE001 | 180 | except Exception: # noqa: BLE001 | ||
modified
jobka/connectors/lassonde.py
+9 −3
@@ -9,7 +9,11 @@ | |||||
| 9 | # Workforce Now : le centre de carrières ADP de Lassonde ne sert | 9 | # Workforce Now : le centre de carrières ADP de Lassonde ne sert | ||
| 10 | # plus que les postes américains (constaté 2026-08-19, found=0 QC), | 10 | # plus que les postes américains (constaté 2026-08-19, found=0 QC), | ||
| 11 | # les postes québécois sont affichés sur le site maison. | 11 | # les postes québécois sont affichés sur le site maison. | ||
| 12 | −# Créé : 2026-08-18 Modifié : 2026-08-19 | 12 | +# 2026-08-31 : lassonde.com est passé derrière Sucuri Cloudproxy | ||
| 13 | +# (challenge JS servi en 307 sans Location → 0 offre en direct) — | ||||
| 14 | +# fetch via get_resilient(render_js=True) : direct d'abord, | ||||
| 15 | +# escalade Scrapfly ASP+rendu JS si challenge. | ||||
| 16 | +# Créé : 2026-08-18 Modifié : 2026-08-31 | ||||
| 13 | # ============================================================================= | 17 | # ============================================================================= | ||
| 14 | from __future__ import annotations | 18 | from __future__ import annotations | ||
| 15 | 19 | ||||
@@ -45,7 +49,7 @@ class LassondeConnector(BaseConnector): | |||||
| 45 | def _fetch_detail(self, url: str) -> dict: | 49 | def _fetch_detail(self, url: str) -> dict: | ||
| 46 | """Le site n'expose pas de JSON-LD JobPosting (seulement un @graph | 50 | """Le site n'expose pas de JSON-LD JobPosting (seulement un @graph | ||
| 47 | WebPage) : la description vient du corps de la page (après le h1).""" | 51 | WebPage) : la description vient du corps de la page (après le h1).""" | ||
| 48 | − html = self.get(url).text | 52 | + html = self.get_resilient(url, render_js=True).text | ||
| 49 | node = _jsonld.extract_jobposting(html) | 53 | node = _jsonld.extract_jobposting(html) | ||
| 50 | if node: | 54 | if node: | ||
| 51 | return _jsonld.jobposting_fields(node) | 55 | return _jsonld.jobposting_fields(node) | ||
@@ -59,7 +63,9 @@ class LassondeConnector(BaseConnector): | |||||
| 59 | return {"description_html": m.group(1)[:60000]} | 63 | return {"description_html": m.group(1)[:60000]} | ||
| 60 | 64 | ||||
| 61 | def fetch(self) -> list[JobPosting]: | 65 | def fetch(self) -> list[JobPosting]: | ||
| 62 | − page = self.get(self.LIST_URL).text | 66 | + # Sucuri Cloudproxy depuis 2026-08 : rendu JS obligatoire (challenge | ||
| 67 | + # cookie), le direct est retenté d'abord à chaque cycle (gratuit) | ||||
| 68 | + page = self.get_resilient(self.LIST_URL, render_js=True).text | ||||
| 63 | out: list[JobPosting] = [] | 69 | out: list[JobPosting] = [] | ||
| 64 | details_used = 0 | 70 | details_used = 0 | ||
| 65 | seen: set[str] = set() | 71 | seen: set[str] = set() | ||
| 66 | 72 | ||||