HTML 82.1%
Python 14.6%
TypeScript 1.9%
CSS 1%
JavaScript 0.5%
1# =============================================================================2# Job·Ka — Groupe KA3# Auteur : Simon-Pierre Boucher4# Contact : contact@spboucher.ai5# Fichier : jobka/connectors/digitalrecruiters.py6# Rôle : Classe de plateforme Cegid Digital Recruiters (sites carrières) —7# sitemap + JSON-LD des pages annonce (ex. Santé Québec)8# Créé : 2026-08-18 Modifié : 2026-08-269# =============================================================================10"""Plateforme Cegid Digital Recruiters.1112Les sites carrières Digital Recruiters (ex. emplois.sante.quebec) sont rendus13côté serveur :14- liste : GET <BASE>/<locale>/sitemap.xml -> URLs /annonce/<id>-<slug>15- détail : chaque page annonce embarque un JSON-LD schema.org/JobPosting16 complet (description, lieu, salaire, dates) — cache BD + budget.17"""18from __future__ import annotations1920import os21import re22import time2324from ..schema import JobPosting, is_quebec_location25from . import _jsonld26from .base import BaseConnector2728MAX_DETAILS = int(os.environ.get("JOBKA_DR_DETAIL_LIMIT", "120"))2930_AD_RE = re.compile(r"<loc>([^<]*/annonce/(\d+)[^<]*)</loc>", re.I)313233class DigitalRecruitersConnector(BaseConnector):34 """Base Digital Recruiters — sous-classes : définir source_id, EMPLOYER,35 BASE (ex. https://emplois.sante.quebec) et LOCALE."""3637 ats = "digitalrecruiters"38 request_delay = 0.83940 EMPLOYER = ""41 BASE = ""42 LOCALE = "fr_CA"43 quebec_only = True4445 def _fetch_detail(self, url: str) -> dict:46 html = self.get(url).text47 node = _jsonld.extract_jobposting(html)48 return _jsonld.jobposting_fields(node) if node else {}4950 def _fetch_sitemap(self) -> str:51 """Sitemap de la liste, avec garde-fou : une réponse vide ou anti-bot52 (challenge en 200, escalade en échec) ne doit JAMAIS passer pour un53 babillard à 0 offre — cause des syncs « 0 trouvé » intermittents de54 cssdd. Tout sitemap DR légitime, même sans annonce, contient <urlset>55 — sinon on retente une fois puis on lève (sync en échec, pas 0)."""56 url = f"{self.BASE}/{self.LOCALE}/sitemap.xml"57 for attempt in range(2):58 xml = self.get(url).text59 if _AD_RE.search(xml) or re.search(r"<urlset", xml, re.I):60 return xml61 if attempt == 0:62 time.sleep(10)63 raise RuntimeError(64 f"digitalrecruiters {self.source_id}: sitemap illisible "65 f"(vide ou anti-bot, {len(xml)} octets) — sync avorté")6667 def fetch(self) -> list[JobPosting]:68 xml = self._fetch_sitemap()69 out: list[JobPosting] = []70 details_used = 071 seen: set[str] = set()72 for url, eid in _AD_RE.findall(xml):73 if eid in seen:74 continue75 seen.add(eid)76 job = JobPosting(source=self.source_id, external_id=eid, url=url,77 employer=self.EMPLOYER, title="", ats=self.ats)78 fields: dict = {}79 if details_used < MAX_DETAILS:80 fresh = [False]8182 def _fn(u=url, fresh=fresh):83 fresh[0] = True84 return self._fetch_detail(u)8586 fields = self.detail(eid, eid, _fn)87 if fresh[0]:88 details_used += 189 if not fields:90 continue91 _jsonld.apply_fields(job, fields)92 if self.quebec_only and job.city and not is_quebec_location(93 f"{job.city} {fields.get('region_code', '')}"):94 if (fields.get("region_code") or "").upper() != "QC":95 continue96 if not job.title:97 continue98 out.append(job)99 return out100