You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

78 lines
3.0 KiB
Python

"""HospitalRecruiting adapter — JS extraction from job cards."""
from __future__ import annotations
import re
from loguru import logger
from gimme_job.adapters.base import ManifestDrivenAdapter
from gimme_job.adapters.registry import register
from gimme_job.models.dto import JobPostingCandidate, RawJobCard
_BASE_URL = "https://www.hospitalrecruiting.com"
_EXTRACT_JS = """() => {
const cards = document.querySelectorAll('div:has(> a[href^="/job/"][aria-label])');
return Array.from(cards).map(card => {
const link = card.querySelector('a[href^="/job/"][aria-label]');
const h2 = card.querySelector('h2');
const divs = Array.from(card.querySelectorAll(':scope > div'));
const companyDiv = divs.find(d => d.textContent.includes('Company:'));
const locationDiv = divs.find(d => !d.textContent.includes('Company:') && d.textContent.trim());
const href = link ? link.getAttribute('href') : '';
const idMatch = href.match(/\\/job\\/(\\d+)\\//);
return {
title: h2?.textContent?.trim() || link?.getAttribute('aria-label') || '',
url: href,
company: companyDiv ? companyDiv.textContent.replace('Company:', '').trim() : '',
location: locationDiv?.textContent?.trim() || '',
external_job_id: idMatch ? idMatch[1] : '',
};
});
}"""
@register("hospitalrecruiting")
class HospitalRecruitingAdapter(ManifestDrivenAdapter):
def collect_cards(self, page, manifest) -> list[RawJobCard]:
try:
page.wait_for_selector('a[href^="/job/"][aria-label]', timeout=10000)
except Exception:
logger.debug("[hospitalrecruiting] job link wait timed out")
return self._extract_cards(page)
def paginate(self, page, page_index: int, config) -> bool:
return False
def normalize(self, raw: RawJobCard) -> JobPostingCandidate:
# Make URL absolute
if raw.url and raw.url.startswith("/"):
raw = raw.model_copy(update={"url": _BASE_URL + raw.url})
# Strip "Specialty - " prefix from location
# e.g. "Orthodontist - Roanoke, Virginia" → "Roanoke, Virginia"
if raw.location:
m = re.match(r'^[^,]+ - (.+)$', raw.location)
if m:
raw = raw.model_copy(update={"location": m.group(1)})
return super().normalize(raw)
def _extract_cards(self, page) -> list[RawJobCard]:
data = page.evaluate(_EXTRACT_JS)
cards = []
for item in data:
title = (item.get("title") or "").strip()
if not title:
continue
cards.append(RawJobCard(
title=title,
company=(item.get("company") or "").strip() or None,
location=(item.get("location") or "").strip() or None,
url=item.get("url") or None,
external_job_id=(item.get("external_job_id") or "").strip() or None,
))
logger.debug(f"[hospitalrecruiting] Extracted {len(cards)} cards")
return cards