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
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
|