SPB Git

spb/job-ka

Public

[ka6] fix connecteur sante_montreal: Radware étendu au domaine personnalisé post-maintenance Njoyn — bascule Scrapfly + sessions Scrapfly pour la pagination

Depuis la maintenance Njoyn du 2026-09-05→07, Radware Bot Manager couvre aussi
emplois.santemontreal.qc.ca (avant : seulement *.njoyn.com). Chronologie des
syncs : 731 offres (08/09 14h32) → 50 (POST de pagination rejeté) → 0 (page
liste bloquée). La page « Radware Block Page » embarque l'URL Xweb.asp dans ses
paramètres et passait le garde-fou de _fetch_listing → faux « 0 offre, ok ».

- sante_montreal.py : USE_SCRAPFLY = True (pattern des 10 autres tenants Njoyn)
- njoyn.py : détection _RADWARE_RE (Radware Block Page/shieldsquare/perfdrive)
  dans _html avec bascule Scrapfly pour tout le reste du sync ; sessions
  Scrapfly (_sf_session) partagées entre GET liste / POST pagination / détails
  — sans session le tbtoken (lié aux cookies ASP du GET) est rejeté en
  400 Bad Request au POST ; casse Xweb.asp forcée sur l'action du formulaire
  (gotcha 2026-09-02)
- base.py : kwarg optionnel session sur scrapfly()/get_scrapfly()
  (session + session_sticky_proxy), rétrocompatible

Sync rejoué : 726 offres (médiane historique ~706), removed=16 offres
dépubliées pendant la panne. pm2 restart job-ka-sync OK, site 200.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Sep 9, 2026) parent 4b92938 Browse files

3 changed files +52 −12

modified jobka/connectors/base.py +14 −4
@@ -5,7 +5,7 @@
5 # Fichier : jobka/connectors/base.py 5 # Fichier : jobka/connectors/base.py
6 # Rôle : Classe de base des connecteurs + backends de fetch (requests 6 # Rôle : Classe de base des connecteurs + backends de fetch (requests
7 # direct, Scrapfly pour les pages carrières JS / derrière anti-bot) 7 # direct, Scrapfly pour les pages carrières JS / derrière anti-bot)
8−# Créé : 2026-08-17 Modifié : 2026-08-27 8+# Créé : 2026-08-17 Modifié : 2026-09-09
9 # ============================================================================= 9 # =============================================================================
10 from __future__ import annotations 10 from __future__ import annotations
11 11
@@ -69,13 +69,18 @@ class BaseConnector:
69 wait_for_selector: str | None = None, 69 wait_for_selector: str | None = None,
70 js_scenario: list | str | None = None, 70 js_scenario: list | str | None = None,
71 proxy_pool: str | None = None, headers: dict | None = None, 71 proxy_pool: str | None = None, headers: dict | None = None,
72− method: str = "GET", body: str | None = None) -> dict: 72+ method: str = "GET", body: str | None = None,
73+ session: str | None = None) -> dict:
73 """Appel Scrapfly complet — retourne le dict `result` (content, status_code…). 74 """Appel Scrapfly complet — retourne le dict `result` (content, status_code…).
74 75
75 - `js_scenario` : liste d'étapes [{"scroll_y":…},{"wait":…}] (base64) 76 - `js_scenario` : liste d'étapes [{"scroll_y":…},{"wait":…}] (base64)
76 pour charger les listes d'offres virtualisées. 77 pour charger les listes d'offres virtualisées.
77 - `proxy_pool` : ex. "public_residential_pool" (WAF/anti-bot agressif). 78 - `proxy_pool` : ex. "public_residential_pool" (WAF/anti-bot agressif).
78 - `headers`/`method`/`body` : pour rejouer une API JSON interne via ASP. 79 - `headers`/`method`/`body` : pour rejouer une API JSON interne via ASP.
80+ - `session` : nom de session Scrapfly — cookies + IP (proxy collant)
81+ partagés entre les requêtes du même sync ; requis quand un POST doit
82+ rejouer un jeton lié à la session du GET (ex. tbtoken Njoyn, sinon
83+ 400 Bad Request).
79 """ 84 """
80 import base64 85 import base64
81 key = os.environ.get("SCRAPFLY_API_KEY") 86 key = os.environ.get("SCRAPFLY_API_KEY")
@@ -95,6 +100,9 @@ class BaseConnector:
95 if js_scenario is not None: 100 if js_scenario is not None:
96 js = js_scenario if isinstance(js_scenario, str) else json.dumps(js_scenario) 101 js = js_scenario if isinstance(js_scenario, str) else json.dumps(js_scenario)
97 params["js_scenario"] = base64.urlsafe_b64encode(js.encode()).decode() 102 params["js_scenario"] = base64.urlsafe_b64encode(js.encode()).decode()
103+ if session:
104+ params["session"] = session
105+ params["session_sticky_proxy"] = "true"
98 if headers: 106 if headers:
99 for k, v in headers.items(): 107 for k, v in headers.items():
100 params[f"headers[{k}]"] = v 108 params[f"headers[{k}]"] = v
@@ -114,13 +122,15 @@ class BaseConnector:
114 rendering_wait: int = 0, country: str = "ca", 122 rendering_wait: int = 0, country: str = "ca",
115 wait_for_selector: str | None = None, 123 wait_for_selector: str | None = None,
116 js_scenario: list | str | None = None, 124 js_scenario: list | str | None = None,
117− proxy_pool: str | None = None) -> str: 125+ proxy_pool: str | None = None,
126+ session: str | None = None) -> str:
118 """HTML rendu via Scrapfly (ASP = bypass anti-bot + rendu JS). Retourne 127 """HTML rendu via Scrapfly (ASP = bypass anti-bot + rendu JS). Retourne
119 le HTML (result.content) ou "" en cas d'échec ASP.""" 128 le HTML (result.content) ou "" en cas d'échec ASP."""
120 return self.scrapfly(url, render_js=render_js, asp=asp, 129 return self.scrapfly(url, render_js=render_js, asp=asp,
121 rendering_wait=rendering_wait, country=country, 130 rendering_wait=rendering_wait, country=country,
122 wait_for_selector=wait_for_selector, 131 wait_for_selector=wait_for_selector,
123− js_scenario=js_scenario, proxy_pool=proxy_pool 132+ js_scenario=js_scenario, proxy_pool=proxy_pool,
133+ session=session
124 ).get("content") or "" 134 ).get("content") or ""
125 135
126 def detail(self, external_id: str, key: str, fetch_fn) -> dict: 136 def detail(self, external_id: str, key: str, fetch_fn) -> dict:
modified jobka/connectors/njoyn.py +32 −7
@@ -5,7 +5,7 @@
5 # Fichier : jobka/connectors/njoyn.py 5 # Fichier : jobka/connectors/njoyn.py
6 # Rôle : Classe de plateforme Njoyn (CGI) — très répandu au secteur public 6 # Rôle : Classe de plateforme Njoyn (CGI) — très répandu au secteur public
7 # québécois (santé, villes, sociétés d'État). HTML serveur + jeton. 7 # québécois (santé, villes, sociétés d'État). HTML serveur + jeton.
8−# Créé : 2026-08-18 Modifié : 2026-09-07 8+# Créé : 2026-08-18 Modifié : 2026-09-09
9 # ============================================================================= 9 # =============================================================================
10 """Plateforme Njoyn (CGI). 10 """Plateforme Njoyn (CGI).
11 11
@@ -17,9 +17,10 @@ une ligne de tableau (habillage classique) pointant vers
17 ``page=jobdetails&jobid=J####-####``. 17 ``page=jobdetails&jobid=J####-####``.
18 18
19 ⚠ Les domaines *.njoyn.com sont derrière Radware Bot Manager : pour ces 19 ⚠ Les domaines *.njoyn.com sont derrière Radware Bot Manager : pour ces
20−tenants, mettre ``USE_SCRAPFLY = True`` (contournement ASP). Les babillards 20+tenants, mettre ``USE_SCRAPFLY = True`` (contournement ASP). Depuis la
21−servis sur un domaine personnalisé (ex. emplois.santemontreal.qc.ca) se 21+maintenance plateforme du 2026-09-05→07, Radware couvre aussi des domaines
22−consultent directement. 22+personnalisés (ex. emplois.santemontreal.qc.ca depuis le 2026-09-08) ; le
23+fallback de ``_html`` bascule alors tout le sync sur Scrapfly.
23 """ 24 """
24 from __future__ import annotations 25 from __future__ import annotations
25 26
@@ -63,6 +64,12 @@ _MAINT_RE = re.compile(
63 # pour un vrai navigateur (vérifié Chromium/Playwright depuis IP résidentielle). 64 # pour un vrai navigateur (vérifié Chromium/Playwright depuis IP résidentielle).
64 # Ce n'est PAS de l'anti-bot : échec immédiat, sans retry ni 2e crédit Scrapfly. 65 # Ce n'est PAS de l'anti-bot : échec immédiat, sans retry ni 2e crédit Scrapfly.
65 _INVALID_RE = re.compile(r"Invalid request XWP\d+", re.I) 66 _INVALID_RE = re.compile(r"Invalid request XWP\d+", re.I)
67+# Mur Radware Bot Manager (« Radware Block Page », CSS shieldsquare) : depuis
68+# la maintenance du 2026-09-05→07 il couvre AUSSI des domaines personnalisés
69+# (constaté 2026-09-08 sur emplois.santemontreal.qc.ca). ⚠ La page challenge
70+# embarque l'URL Xweb.asp dans ses paramètres : sans détection dédiée elle
71+# passait le garde-fou de _fetch_listing et donnait un faux « 0 offre, ok ».
72+_RADWARE_RE = re.compile(r"Radware Block Page|shieldsquare|perfdrive", re.I)
66 73
67 74
68 def _txt(s: str) -> str: 75 def _txt(s: str) -> str:
@@ -97,12 +104,26 @@ class NjoynConnector(BaseConnector):
97 u += f"&jobid={jobid}" 104 u += f"&jobid={jobid}"
98 return u 105 return u
99 106
107+ def _sf_session(self) -> str:
108+ """Nom de session Scrapfly du sync courant : cookies + IP (proxy
109+ collant) partagés entre le GET liste, les POST de pagination et les
110+ pages détail. Sans session, le tbtoken du formulaire (lié aux cookies
111+ ASP de la session du GET) est rejeté en 400 Bad Request au POST."""
112+ if not getattr(self, "_sf_session_name", None):
113+ self._sf_session_name = f"njoyn-{self.source_id}-{int(time.time())}"
114+ return self._sf_session_name
115+
100 def _html(self, url: str) -> str: 116 def _html(self, url: str) -> str:
101 if self.USE_SCRAPFLY: 117 if self.USE_SCRAPFLY:
102− return self.get_scrapfly(url, render_js=False) 118+ return self.get_scrapfly(url, render_js=False,
119+ session=self._sf_session())
103 text = self.get(url).text 120 text = self.get(url).text
104− if "perfdrive" in text[:4000]: # mur anti-bot : bascule Scrapfly 121+ if _RADWARE_RE.search(text[:4000]): # mur anti-bot : bascule Scrapfly
105− return self.get_scrapfly(url, render_js=False) 122+ # … pour TOUTE la suite du sync (pagination POST et pages détail
123+ # passent par le même mur), pas seulement cette requête.
124+ self.USE_SCRAPFLY = True
125+ return self.get_scrapfly(url, render_js=False,
126+ session=self._sf_session())
106 return text 127 return text
107 128
108 def _fetch_detail(self, jobid: str) -> dict: 129 def _fetch_detail(self, jobid: str) -> dict:
@@ -195,6 +216,7 @@ class NjoynConnector(BaseConnector):
195 action, render_js=False, method="POST", 216 action, render_js=False, method="POST",
196 body=urlencode(data), 217 body=urlencode(data),
197 headers={"Content-Type": "application/x-www-form-urlencoded"}, 218 headers={"Content-Type": "application/x-www-form-urlencoded"},
219+ session=self._sf_session(),
198 ).get("content") or "" 220 ).get("content") or ""
199 return self.post(action, data=data).text 221 return self.post(action, data=data).text
200 222
@@ -216,6 +238,9 @@ class NjoynConnector(BaseConnector):
216 if not m: 238 if not m:
217 return items 239 return items
218 action = _html.unescape(m.group(1)) 240 action = _html.unescape(m.group(1))
241+ # Même gotcha de casse que _url : le formulaire annonce « xweb.asp »
242+ # minuscule, que le couple Njoyn/Radware rejette depuis le 2026-09-02.
243+ action = re.sub(r"xweb\.asp", "Xweb.asp", action)
219 if not action.startswith("http"): 244 if not action.startswith("http"):
220 action = f"{self.BASE}/{self.CL}/xweb/{action}" 245 action = f"{self.BASE}/{self.CL}/xweb/{action}"
221 m_sort = re.search( 246 m_sort = re.search(
modified jobka/connectors/sante_montreal.py +6 −1
@@ -5,7 +5,7 @@
5 # Fichier : jobka/connectors/sante_montreal.py 5 # Fichier : jobka/connectors/sante_montreal.py
6 # Rôle : Connecteur Santé Québec — Montréal — njoyn (base='https://emplois.santemontreal.qc.ca', cl='CL3') 6 # Rôle : Connecteur Santé Québec — Montréal — njoyn (base='https://emplois.santemontreal.qc.ca', cl='CL3')
7 # [généré par scripts/gen_connectors.py, flux vérifié le 2026-08-18] 7 # [généré par scripts/gen_connectors.py, flux vérifié le 2026-08-18]
8−# Créé : 2026-08-18 Modifié : 2026-08-18 8+# Créé : 2026-08-18 Modifié : 2026-09-09
9 # ============================================================================= 9 # =============================================================================
10 from .njoyn import NjoynConnector 10 from .njoyn import NjoynConnector
11 11
@@ -16,3 +16,8 @@ class SanteMontrealConnector(NjoynConnector):
16 BASE = 'https://emplois.santemontreal.qc.ca' 16 BASE = 'https://emplois.santemontreal.qc.ca'
17 CL = 'CL3' 17 CL = 'CL3'
18 CLID = '54327' 18 CLID = '54327'
19+ # Depuis la maintenance Njoyn du 2026-09-05→07, Radware Bot Manager couvre
20+ # aussi ce domaine personnalisé (avant : seulement *.njoyn.com). L'accès
21+ # direct renvoie « Radware Block Page » — passage par Scrapfly (ASP) comme
22+ # tous les autres tenants Njoyn du repo.
23+ USE_SCRAPFLY = True
19 24
Menu

Theme