SPB Git

spb/job-ka

Public

[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

Simon-Pierre Boucher committed (Aug 31, 2026) parent 3ea118a Browse files

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
Menu

Theme