This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from urllib.parse import urljoin, urlsplit
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from .base import ExtractedJob, ExtractionResult, FieldEvidence
|
||||
|
||||
|
||||
class CordaCampusAdapter:
|
||||
"""Extract the public job cards intentionally published by Corda Campus."""
|
||||
|
||||
parser_key = "regional-corda-campus"
|
||||
parser_version = "1.0.0"
|
||||
source_hosts = ("cordacampus.com",)
|
||||
|
||||
def _supports_url(self, url: str) -> bool:
|
||||
parts = urlsplit(url)
|
||||
host = (parts.hostname or "").lower()
|
||||
return host.endswith(self.source_hosts) and parts.path.rstrip("/") == "/jobs"
|
||||
|
||||
def extract(self, content: str, *, url: str) -> ExtractionResult:
|
||||
if not self._supports_url(url):
|
||||
return ExtractionResult(
|
||||
[], self.parser_key, self.parser_version, 0.0, ["Onherkenbare regiobron"]
|
||||
)
|
||||
|
||||
source_host = (urlsplit(url).hostname or "").lower()
|
||||
soup = BeautifulSoup(content, "lxml")
|
||||
jobs: list[ExtractedJob] = []
|
||||
seen_urls: set[str] = set()
|
||||
|
||||
for card in soup.select('a.event-item[href*="/job/"]'):
|
||||
job_url = urljoin(url, str(card.get("href") or ""))
|
||||
job_host = (urlsplit(job_url).hostname or "").lower()
|
||||
if job_host != source_host or job_url in seen_urls:
|
||||
continue
|
||||
title_node = card.select_one(".job-title")
|
||||
employer_node = card.select_one(".company-title")
|
||||
title = title_node.get_text(" ", strip=True) if title_node else ""
|
||||
employer = employer_node.get_text(" ", strip=True) if employer_node else ""
|
||||
if not title:
|
||||
continue
|
||||
|
||||
date_node = card.select_one(".bottom-info")
|
||||
date_posted = date_node.get_text(" ", strip=True) if date_node else ""
|
||||
external_id = urlsplit(job_url).path.rstrip("/").rsplit("/", 1)[-1]
|
||||
description = f"Vacature van {employer or 'een Corda-werkgever'} via Corda Campus."
|
||||
jobs.append(
|
||||
ExtractedJob(
|
||||
url=job_url,
|
||||
title=title,
|
||||
employer_name=employer,
|
||||
external_id=external_id,
|
||||
location_text="Hasselt, Limburg",
|
||||
region="Limburg",
|
||||
country="BE",
|
||||
description_text=description,
|
||||
date_posted=date_posted,
|
||||
raw={"regional_listing": "corda-campus"},
|
||||
evidence=[
|
||||
FieldEvidence("title", "corda-job-card", 0.95, title[:240]),
|
||||
FieldEvidence("employer_name", "corda-job-card", 0.92, employer[:240]),
|
||||
FieldEvidence(
|
||||
"location_text",
|
||||
"regional-source-scope",
|
||||
0.72,
|
||||
"Corda Campus, Hasselt",
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
seen_urls.add(job_url)
|
||||
|
||||
warnings = [] if jobs else ["Geen actuele Corda-vacatures gevonden"]
|
||||
return ExtractionResult(
|
||||
jobs,
|
||||
self.parser_key,
|
||||
self.parser_version,
|
||||
0.9 if jobs else 0.0,
|
||||
warnings,
|
||||
)
|
||||
|
||||
|
||||
class LocalEmployerListingAdapter:
|
||||
"""Extract reviewed public listing pages of employers in the Hasselt-Genk area."""
|
||||
|
||||
parser_key = "regional-local-employers"
|
||||
parser_version = "1.0.0"
|
||||
supported_routes = {
|
||||
"acagroup.be": {"/en/jobs"},
|
||||
"www.xploregroup.be": {"/en/jobs"},
|
||||
"www.uhasselt.be": {"/vacatures"},
|
||||
"ses.pxl.be": {"/"},
|
||||
"ziekenhuis-oost-limburg.cvw.io": {"/"},
|
||||
}
|
||||
|
||||
def _supports_url(self, url: str) -> bool:
|
||||
return self._route(url) is not None
|
||||
|
||||
def _route(self, url: str) -> tuple[str, str] | None:
|
||||
parts = urlsplit(url)
|
||||
host = (parts.hostname or "").lower()
|
||||
path = parts.path.rstrip("/") or "/"
|
||||
if parts.scheme != "https" or path not in self.supported_routes.get(host, set()):
|
||||
return None
|
||||
return host, path
|
||||
|
||||
@staticmethod
|
||||
def _job(
|
||||
*,
|
||||
url: str,
|
||||
title: str,
|
||||
employer: str,
|
||||
location: str,
|
||||
description: str,
|
||||
external_id: str,
|
||||
evidence_source: str,
|
||||
) -> ExtractedJob:
|
||||
return ExtractedJob(
|
||||
url=url,
|
||||
title=title,
|
||||
employer_name=employer,
|
||||
external_id=external_id,
|
||||
location_text=location,
|
||||
region="Limburg",
|
||||
country="BE",
|
||||
description_text=description,
|
||||
raw={"regional_listing": evidence_source},
|
||||
evidence=[
|
||||
FieldEvidence("title", evidence_source, 0.94, title[:240]),
|
||||
FieldEvidence("employer_name", "reviewed-source", 0.96, employer),
|
||||
FieldEvidence("location_text", "regional-source-scope", 0.78, location),
|
||||
],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _same_host_url(base_url: str, href: str) -> str:
|
||||
job_url = urljoin(base_url, href)
|
||||
if (urlsplit(job_url).hostname or "").lower() != (
|
||||
urlsplit(base_url).hostname or ""
|
||||
).lower():
|
||||
return ""
|
||||
return job_url
|
||||
|
||||
def _aca(self, soup: BeautifulSoup, url: str) -> list[ExtractedJob]:
|
||||
jobs = []
|
||||
for link in soup.find_all("a", href=True):
|
||||
href = str(link.get("href") or "")
|
||||
if not href.startswith("/en/jobs/"):
|
||||
continue
|
||||
title_node = link.find("h3")
|
||||
title = title_node.get_text(" ", strip=True) if title_node else ""
|
||||
job_url = self._same_host_url(url, href)
|
||||
if not title or not job_url:
|
||||
continue
|
||||
description_node = link.find("p")
|
||||
description = (
|
||||
description_node.get_text(" ", strip=True)
|
||||
if description_node
|
||||
else f"Vacature bij ACA Group: {title}."
|
||||
)
|
||||
jobs.append(
|
||||
self._job(
|
||||
url=job_url,
|
||||
title=title,
|
||||
employer="ACA Group",
|
||||
location="Hasselt (hybride; kantoorselectie per vacature)",
|
||||
description=description,
|
||||
external_id=urlsplit(job_url).path.rstrip("/").rsplit("/", 1)[-1],
|
||||
evidence_source="aca-job-card",
|
||||
)
|
||||
)
|
||||
return jobs
|
||||
|
||||
def _xplore(self, soup: BeautifulSoup, url: str) -> list[ExtractedJob]:
|
||||
jobs = []
|
||||
for heading in soup.find_all("h3"):
|
||||
link = heading.find("a", href=True)
|
||||
if not link or not str(link.get("href") or "").startswith("/en/jobs/"):
|
||||
continue
|
||||
card = heading.parent
|
||||
location_node = next(
|
||||
(
|
||||
node
|
||||
for node in card.find_all("p")
|
||||
if "Hasselt" in node.get_text(" ", strip=True)
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not location_node:
|
||||
continue
|
||||
title = link.get_text(" ", strip=True)
|
||||
job_url = self._same_host_url(url, str(link.get("href") or ""))
|
||||
if not title or not job_url:
|
||||
continue
|
||||
location = location_node.get_text(" ", strip=True)
|
||||
jobs.append(
|
||||
self._job(
|
||||
url=job_url,
|
||||
title=title,
|
||||
employer="Xplore Group",
|
||||
location=location,
|
||||
description=(
|
||||
f"Vacature bij Xplore Group met Hasselt als mogelijke werklocatie: {title}."
|
||||
),
|
||||
external_id=urlsplit(job_url).path.rstrip("/").rsplit("/", 1)[-1],
|
||||
evidence_source="xplore-job-card",
|
||||
)
|
||||
)
|
||||
return jobs
|
||||
|
||||
def _uhasselt(self, soup: BeautifulSoup, url: str) -> list[ExtractedJob]:
|
||||
jobs = []
|
||||
for card in soup.select("section.vacancy-item"):
|
||||
title_node = card.find("h3")
|
||||
link = card.find("a", href=True)
|
||||
title = title_node.get_text(" ", strip=True) if title_node else ""
|
||||
job_url = self._same_host_url(url, str(link.get("href") or "")) if link else ""
|
||||
if not title or not job_url or "/vacatures/detail/" not in urlsplit(job_url).path:
|
||||
continue
|
||||
external_id = urlsplit(job_url).path.split("/detail/", 1)[-1].split("-", 1)[0]
|
||||
jobs.append(
|
||||
self._job(
|
||||
url=job_url,
|
||||
title=title,
|
||||
employer="Universiteit Hasselt",
|
||||
location="Hasselt/Diepenbeek, Limburg",
|
||||
description=card.get_text(" ", strip=True),
|
||||
external_id=external_id,
|
||||
evidence_source="uhasselt-vacancy-card",
|
||||
)
|
||||
)
|
||||
return jobs
|
||||
|
||||
def _pxl(self, soup: BeautifulSoup, url: str) -> list[ExtractedJob]:
|
||||
jobs = []
|
||||
for card in soup.select(".vacature-card[id^='Vacature_']"):
|
||||
external_id = str(card.get("id") or "").removeprefix("Vacature_")
|
||||
title_node = card.select_one(".vacature-card-titel")
|
||||
title = title_node.get_text(" ", strip=True) if title_node else ""
|
||||
if not title or not external_id.isdigit():
|
||||
continue
|
||||
cells = [
|
||||
node.get_text(" ", strip=True) for node in card.select("td.vacature-card-td-text")
|
||||
]
|
||||
campus = next((value for value in cells if value.startswith("Campus ")), "Hasselt")
|
||||
job_url = f"{url.rstrip('/')}?vacature_id={external_id}"
|
||||
jobs.append(
|
||||
self._job(
|
||||
url=job_url,
|
||||
title=title,
|
||||
employer="Hogeschool PXL",
|
||||
location=f"{campus}, Limburg",
|
||||
description=" · ".join(cells),
|
||||
external_id=external_id,
|
||||
evidence_source="pxl-vacancy-card",
|
||||
)
|
||||
)
|
||||
return jobs
|
||||
|
||||
def _zol(self, soup: BeautifulSoup, url: str) -> list[ExtractedJob]:
|
||||
jobs = []
|
||||
for link in soup.select("a[data-item='readmore'][data-jobid][href]"):
|
||||
external_id = str(link.get("data-jobid") or "")
|
||||
title_node = link.select_one(".job-title")
|
||||
title = title_node.get_text(" ", strip=True) if title_node else ""
|
||||
job_url = self._same_host_url(url, str(link.get("href") or ""))
|
||||
if not title or not external_id.isdigit() or not job_url:
|
||||
continue
|
||||
jobs.append(
|
||||
self._job(
|
||||
url=job_url,
|
||||
title=title,
|
||||
employer="Ziekenhuis Oost-Limburg",
|
||||
location="Genk/Lanaken/Maaseik, Limburg",
|
||||
description=f"Publieke vacature van Ziekenhuis Oost-Limburg: {title}.",
|
||||
external_id=external_id,
|
||||
evidence_source="zol-cvwarehouse-card",
|
||||
)
|
||||
)
|
||||
return jobs
|
||||
|
||||
def extract(self, content: str, *, url: str) -> ExtractionResult:
|
||||
route = self._route(url)
|
||||
if route is None:
|
||||
return ExtractionResult(
|
||||
[],
|
||||
self.parser_key,
|
||||
self.parser_version,
|
||||
0.0,
|
||||
["Onherkenbare lokale werkgeversbron"],
|
||||
)
|
||||
host, _ = route
|
||||
soup = BeautifulSoup(content, "lxml")
|
||||
extractors = {
|
||||
"acagroup.be": self._aca,
|
||||
"www.xploregroup.be": self._xplore,
|
||||
"www.uhasselt.be": self._uhasselt,
|
||||
"ses.pxl.be": self._pxl,
|
||||
"ziekenhuis-oost-limburg.cvw.io": self._zol,
|
||||
}
|
||||
jobs = extractors[host](soup, url)
|
||||
unique_jobs = list({job.url: job for job in jobs}.values())
|
||||
warnings = [] if unique_jobs else ["Geen actuele lokale vacatures gevonden"]
|
||||
return ExtractionResult(
|
||||
unique_jobs,
|
||||
self.parser_key,
|
||||
self.parser_version,
|
||||
0.9 if unique_jobs else 0.0,
|
||||
warnings,
|
||||
)
|
||||
Reference in New Issue
Block a user