# ============================================================================= # Job·Ka — Groupe KA # Auteur : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Fichier : jobka/connectors/guichet_emplois.py # Rôle : Connecteur portail — Guichet-Emplois / Job Bank Canada (offres # Québec, fenêtre glissante des N derniers jours). AGRÉGATEUR : # moins autoritaire qu'une page carrière (voir dedup.AGGREGATORS). # Créé : 2026-08-18 Modifié : 2026-08-30 # ============================================================================= """Portail Guichet-Emplois (guichetemplois.gc.ca — gouvernement du Canada). Recherche HTML rendue côté serveur (27 offres/page) filtrée province=QC et publication récente (fage). Chaque résultat inclut titre, employeur, ville, salaire et date — la description est lue sur la page de l'offre (cache BD + budget). Chaque fiche renvoie vers la page Guichet-Emplois, qui référence l'offre originale — conformité aux conditions d'utilisation du site. robots.txt impose « Crawl-delay: 5 » : request_delay = 5 s, d'où une fenêtre volontairement bornée (pages et détails limités par synchronisation). """ from __future__ import annotations import html as _html import os import re import time import requests from ..schema import JobPosting, clean_html from .base import BaseConnector BASE = "https://www.guichetemplois.gc.ca" DAYS = os.environ.get("JOBKA_GUICHET_DAYS", "7") MAX_PAGES = int(os.environ.get("JOBKA_GUICHET_PAGES", "20")) MAX_DETAILS = int(os.environ.get("JOBKA_GUICHET_DETAIL_LIMIT", "50")) # Page « Erreur HTTP 500 » servie avec un statut 200 (vu 2026-08-30, aléatoire # requête par requête) : sans détection, 0 item → fin de pagination prématurée. _SOFT500_RE = re.compile(r"
m = re.search(r"
]*>([^<]+)
", html) if m: lang = _txt(m.group(1)).lower() d["work_language"] = ("bilingue" if "biling" in lang else "fr" if "fran" in lang else "en" if "angl" in lang else "") return d def fetch(self) -> list[JobPosting]: out: list[JobPosting] = [] details_used = 0 seen: set[str] = set() for page in range(1, MAX_PAGES + 1): html = "" for attempt in (1, 2, 3): try: html = self.get(f"{BASE}/jobsearch/rechercheemplois", params={"fprov": "QC", "sort": "D", "fage": DAYS, "page": str(page)}).text except (requests.ConnectionError, requests.Timeout): # le WAF du Guichet coupe parfois la connexion : une # reprise après pause, sinon on garde l'acquis partiel if attempt == 3: if out: return out raise time.sleep(25) continue if _SOFT500_RE.search(html): # « Erreur HTTP 500 » servie en 200 : retenter la page html = "" if attempt < 3: time.sleep(15) continue break if not html: continue # erreur persistante sur CETTE page : suivante items = _ITEM_RE.findall(html) if not items: break for jid, block in items: if jid in seen: continue seen.add(jid) title_m = _TITLE_RE.search(block) fields = {} for k, rx in _LI_RE.items(): m = rx.search(block) fields[k] = _txt(m.group(1)) if m else "" location = re.sub(r"\bEmplacement\b", "", fields["location"]) location = location.replace("(QC)", "").strip(" ,") salary = re.sub(r"^.*?Salaire\s*:?", "", fields["salary"]).strip() job = JobPosting( source=self.source_id, external_id=jid, url=f"{BASE}/rechercheemplois/offredemploi/{jid}", employer=fields["business"], title=_txt(title_m.group(1)) if title_m else "", city=location.split(";")[0].split(",")[0], region="Québec", location_label=location, salary_label=salary, date_posted=fields["date"] or None, ats=self.ats, ) if not job.title or not job.employer: continue # clé « v2| » : re-visite progressive (avantages + langue) key = f"v2|{fields['date']}|{job.title[:40]}" if details_used < MAX_DETAILS: fresh = [False] def _fn(j=jid, fresh=fresh): fresh[0] = True return self._fetch_detail(j) try: d = self.detail(jid, key, _fn) except (requests.ConnectionError, requests.Timeout): d = self.stale_detail(jid) # repris au prochain cycle if fresh[0]: details_used += 1 else: # budget épuisé : détail périmé plutôt que fiche vidée d = self.stale_detail(jid) job.description = d.get("description", "") job.date_deadline = d.get("date_deadline") if d.get("benefits"): job.benefits = d["benefits"] if d.get("work_language"): job.language = d["work_language"] out.append(job) return out