Add 8 site adapters, early stop, Telegram notifications, and list command
New site adapters: - usajobs: multi-keyword sweep (dentist/orthodontics/orthodontist), URL pagination - docshealth, southernortho: Paylocity platform, keyword in URL - pdshealth, saltdental: iCIMS Angular platform, URL pagination - hospitaljobsonline: Load More button, relative URL fix - aaoinfo: AAO Career Center, click-based AJAX pagination - aroragroup: Load More + post-scrape keyword filter Core improvements: - Early stop pagination: stops when all fingerprints on a page are already in DB - multi_keyword_mode: separate — one scrape per keyword, cross-sweep dedup - gimme-job list command to view collected postings - Stored column in status command - Telegram notification (replacing KakaoTalk) - DB repo: count_by_site(), get_existing_fingerprints() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>main
parent
8028d5e566
commit
83fb8542a9
@ -0,0 +1,59 @@
|
||||
"""AAO Career Center adapter — click-based AJAX pagination."""
|
||||
from __future__ import annotations
|
||||
|
||||
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
|
||||
|
||||
|
||||
@register("aaoinfo")
|
||||
class AAOInfoAdapter(ManifestDrivenAdapter):
|
||||
_BASE_URL = "https://careers.aaoinfo.org"
|
||||
|
||||
def _wait_for_results(self, page) -> None:
|
||||
try:
|
||||
page.wait_for_selector("div.job-tile", timeout=15000, state="attached")
|
||||
except Exception:
|
||||
logger.debug("[aaoinfo] Results wait timed out — may be zero results")
|
||||
|
||||
def collect_cards(self, page, manifest):
|
||||
self._wait_for_results(page)
|
||||
return super().collect_cards(page, manifest)
|
||||
|
||||
def paginate(self, page, page_index: int, config) -> bool:
|
||||
"""Click the next page number button in the AJAX pagination."""
|
||||
if page_index >= config.pagination.max_pages - 1:
|
||||
return False
|
||||
|
||||
next_page = page_index + 2
|
||||
|
||||
# Try clicking the specific page number button
|
||||
next_btn = page.query_selector(f"#page-item-{next_page} a")
|
||||
if not next_btn:
|
||||
# Fall back to the last non-active/non-disabled pagination link
|
||||
next_btn = page.query_selector(
|
||||
"ul.pagination li.page-item:not(.active):not(.disabled):last-child a"
|
||||
)
|
||||
|
||||
if not next_btn:
|
||||
logger.debug(f"[aaoinfo] No next page button found — stopping at page {page_index + 1}")
|
||||
return False
|
||||
|
||||
logger.debug(f"[aaoinfo] Paginating to page {next_page}")
|
||||
next_btn.click()
|
||||
|
||||
# Wait for the new page's active indicator to confirm AJAX completed
|
||||
try:
|
||||
page.wait_for_selector(f"#page-item-{next_page}.active", timeout=12000)
|
||||
except Exception:
|
||||
page.wait_for_timeout(3000)
|
||||
|
||||
return True
|
||||
|
||||
def normalize(self, raw: RawJobCard) -> JobPostingCandidate:
|
||||
candidate = super().normalize(raw)
|
||||
if candidate.job_url and candidate.job_url.startswith("/"):
|
||||
candidate.job_url = self._BASE_URL + candidate.job_url
|
||||
return candidate
|
||||
@ -0,0 +1,68 @@
|
||||
"""Arora Group Jobs adapter — Load More button, keyword post-filter."""
|
||||
from __future__ import annotations
|
||||
|
||||
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
|
||||
|
||||
_KEYWORDS = ["dentist", "orthodontic", "orthodontist"]
|
||||
_MAX_LOAD_MORE = 3 # clicks in addition to the initial load
|
||||
|
||||
|
||||
@register("aroragroup")
|
||||
class AroraGroupAdapter(ManifestDrivenAdapter):
|
||||
_BASE_URL = "https://jobs.aroragroup.com"
|
||||
|
||||
def collect_cards(self, page, manifest):
|
||||
"""Load initial results, click 'Load More' up to N times, then filter by keywords."""
|
||||
try:
|
||||
page.wait_for_selector(".job-post-row", timeout=15000, state="attached")
|
||||
except Exception:
|
||||
logger.debug("[aroragroup] Initial results wait timed out")
|
||||
|
||||
for i in range(_MAX_LOAD_MORE):
|
||||
btn = page.query_selector("button#loadMore")
|
||||
if not btn or not btn.is_visible():
|
||||
logger.debug(f"[aroragroup] Load More button gone after {i} extra loads")
|
||||
break
|
||||
|
||||
current_count = len(page.query_selector_all(".job-post-row[onclick]"))
|
||||
btn.click()
|
||||
|
||||
try:
|
||||
page.wait_for_function(
|
||||
f"document.querySelectorAll('.job-post-row[onclick]').length > {current_count}",
|
||||
timeout=15000,
|
||||
)
|
||||
logger.debug(f"[aroragroup] Load More {i + 1}/{_MAX_LOAD_MORE}: loaded more cards")
|
||||
except Exception:
|
||||
logger.debug(f"[aroragroup] Load More {i + 1}: no new cards appeared — stopping")
|
||||
break
|
||||
|
||||
all_cards = super().collect_cards(page, manifest)
|
||||
|
||||
# Filter by keywords in title
|
||||
filtered = [c for c in all_cards if self._matches_keywords(c.title)]
|
||||
logger.debug(
|
||||
f"[aroragroup] Keyword filter: {len(filtered)}/{len(all_cards)} cards matched"
|
||||
)
|
||||
return filtered
|
||||
|
||||
def paginate(self, page, page_index: int, config) -> bool:
|
||||
"""Pagination is handled inside collect_cards — always return False."""
|
||||
return False
|
||||
|
||||
def normalize(self, raw: RawJobCard) -> JobPostingCandidate:
|
||||
candidate = super().normalize(raw)
|
||||
if candidate.job_url and candidate.job_url.startswith("/"):
|
||||
candidate.job_url = self._BASE_URL + candidate.job_url
|
||||
return candidate
|
||||
|
||||
@staticmethod
|
||||
def _matches_keywords(title: str | None) -> bool:
|
||||
if not title:
|
||||
return False
|
||||
title_lower = title.lower()
|
||||
return any(kw in title_lower for kw in _KEYWORDS)
|
||||
@ -0,0 +1,34 @@
|
||||
"""DOCS Health adapter — Paylocity-hosted job board, single-page results."""
|
||||
from __future__ import annotations
|
||||
|
||||
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
|
||||
|
||||
|
||||
@register("docshealth")
|
||||
class DocsHealthAdapter(ManifestDrivenAdapter):
|
||||
_BASE_URL = "https://recruiting.paylocity.com"
|
||||
_COMPANY = "DOCS Health"
|
||||
|
||||
def collect_cards(self, page, manifest):
|
||||
# Wait for job listings to render
|
||||
try:
|
||||
page.wait_for_selector(".job-listing-job-item", timeout=12000, state="attached")
|
||||
except Exception:
|
||||
logger.debug("[docshealth] Results wait timed out — may be zero results")
|
||||
return super().collect_cards(page, manifest)
|
||||
|
||||
def normalize(self, raw: RawJobCard) -> JobPostingCandidate:
|
||||
candidate = super().normalize(raw)
|
||||
# Fix relative URL → absolute
|
||||
if candidate.job_url and candidate.job_url.startswith("/"):
|
||||
candidate.job_url = self._BASE_URL + candidate.job_url
|
||||
# Company is always DOCS Health (not in per-card HTML)
|
||||
candidate.company = self._COMPANY
|
||||
# Clean posted_text: "03/27/2026 - " → "03/27/2026"
|
||||
if candidate.posted_text:
|
||||
candidate.posted_text = candidate.posted_text.replace(" - ", "").strip()
|
||||
return candidate
|
||||
@ -0,0 +1,65 @@
|
||||
"""Hospital Jobs Online adapter — URL-based pagination with page= parameter."""
|
||||
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
|
||||
|
||||
|
||||
@register("hospitaljobsonline")
|
||||
class HospitalJobsOnlineAdapter(ManifestDrivenAdapter):
|
||||
_BASE_URL = "https://www.hospitaljobsonline.com"
|
||||
|
||||
def _wait_for_results(self, page) -> None:
|
||||
try:
|
||||
page.wait_for_selector("div.jobresult", timeout=15000, state="attached")
|
||||
except Exception:
|
||||
logger.debug("[hospitaljobsonline] Results wait timed out — may be zero results")
|
||||
|
||||
def collect_cards(self, page, manifest):
|
||||
self._wait_for_results(page)
|
||||
return super().collect_cards(page, manifest)
|
||||
|
||||
def paginate(self, page, page_index: int, config) -> bool:
|
||||
"""Navigate to next page by incrementing the ?page= URL parameter."""
|
||||
if page_index >= config.pagination.max_pages - 1:
|
||||
return False
|
||||
|
||||
current_url = page.url
|
||||
next_page = page_index + 2
|
||||
|
||||
if re.search(r"[?&]page=\d+", current_url):
|
||||
next_url = re.sub(r"(page=)\d+", f"page={next_page}", current_url)
|
||||
else:
|
||||
sep = "&" if "?" in current_url else "?"
|
||||
next_url = f"{current_url}{sep}page={next_page}"
|
||||
|
||||
logger.debug(f"[hospitaljobsonline] Paginating to page {next_page}: {next_url}")
|
||||
page.goto(next_url, timeout=30000)
|
||||
self._wait_for_results(page)
|
||||
return True
|
||||
|
||||
def normalize(self, raw: RawJobCard) -> JobPostingCandidate:
|
||||
candidate = super().normalize(raw)
|
||||
|
||||
if candidate.job_url and candidate.job_url.startswith("/"):
|
||||
candidate.job_url = self._BASE_URL + candidate.job_url
|
||||
|
||||
# Parse location text: "1 week ago - Company Name - Location: City, ST"
|
||||
if candidate.location:
|
||||
loc = candidate.location
|
||||
# Extract part after "Location:" if present
|
||||
m = re.search(r"Location:\s*(.+)$", loc, re.IGNORECASE)
|
||||
if m:
|
||||
candidate.location = m.group(1).strip()
|
||||
else:
|
||||
# Fallback: take last segment after " - "
|
||||
parts = [p.strip() for p in loc.split(" - ")]
|
||||
if len(parts) >= 2:
|
||||
candidate.location = parts[-1]
|
||||
|
||||
return candidate
|
||||
@ -0,0 +1,48 @@
|
||||
"""PDS Health adapter — Angular Material job board, URL-based pagination."""
|
||||
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
|
||||
|
||||
|
||||
@register("pdshealth")
|
||||
class PdsHealthAdapter(ManifestDrivenAdapter):
|
||||
|
||||
def _wait_for_results(self, page) -> None:
|
||||
try:
|
||||
page.wait_for_selector(
|
||||
"mat-expansion-panel.search-result-item",
|
||||
timeout=15000,
|
||||
state="attached",
|
||||
)
|
||||
except Exception:
|
||||
logger.debug("[pdshealth] Results wait timed out — may be zero results")
|
||||
|
||||
def collect_cards(self, page, manifest):
|
||||
self._wait_for_results(page)
|
||||
return super().collect_cards(page, manifest)
|
||||
|
||||
def paginate(self, page, page_index: int, config) -> bool:
|
||||
"""Navigate to next page by incrementing the &page= URL parameter."""
|
||||
max_pages = config.pagination.max_pages
|
||||
if page_index >= max_pages - 1:
|
||||
return False
|
||||
|
||||
current_url = page.url
|
||||
next_page = page_index + 2 # 0-based index → 1-based page param
|
||||
|
||||
if re.search(r"[?&]page=\d+", current_url):
|
||||
next_url = re.sub(r"(page=)\d+", f"page={next_page}", current_url)
|
||||
else:
|
||||
sep = "&" if "?" in current_url else "?"
|
||||
next_url = f"{current_url}{sep}page={next_page}"
|
||||
|
||||
logger.debug(f"[pdshealth] Paginating to page {next_page}: {next_url}")
|
||||
page.goto(next_url, timeout=30000)
|
||||
self._wait_for_results(page)
|
||||
return True
|
||||
@ -0,0 +1,46 @@
|
||||
"""Salt Dental Partners adapter — iCIMS Angular platform, URL-based pagination."""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from gimme_job.adapters.base import ManifestDrivenAdapter
|
||||
from gimme_job.adapters.registry import register
|
||||
|
||||
|
||||
@register("saltdental")
|
||||
class SaltDentalAdapter(ManifestDrivenAdapter):
|
||||
|
||||
def _wait_for_results(self, page) -> None:
|
||||
try:
|
||||
page.wait_for_selector(
|
||||
"mat-expansion-panel.search-result-item",
|
||||
timeout=15000,
|
||||
state="attached",
|
||||
)
|
||||
except Exception:
|
||||
logger.debug("[saltdental] Results wait timed out — may be zero results")
|
||||
|
||||
def collect_cards(self, page, manifest):
|
||||
self._wait_for_results(page)
|
||||
return super().collect_cards(page, manifest)
|
||||
|
||||
def paginate(self, page, page_index: int, config) -> bool:
|
||||
"""Navigate to next page by incrementing the &page= URL parameter."""
|
||||
if page_index >= config.pagination.max_pages - 1:
|
||||
return False
|
||||
|
||||
current_url = page.url
|
||||
next_page = page_index + 2
|
||||
|
||||
if re.search(r"[?&]page=\d+", current_url):
|
||||
next_url = re.sub(r"(page=)\d+", f"page={next_page}", current_url)
|
||||
else:
|
||||
sep = "&" if "?" in current_url else "?"
|
||||
next_url = f"{current_url}{sep}page={next_page}"
|
||||
|
||||
logger.debug(f"[saltdental] Paginating to page {next_page}: {next_url}")
|
||||
page.goto(next_url, timeout=30000)
|
||||
self._wait_for_results(page)
|
||||
return True
|
||||
@ -0,0 +1,28 @@
|
||||
"""Southern Orthodontic Partners adapter — Paylocity platform, single page."""
|
||||
from __future__ import annotations
|
||||
|
||||
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
|
||||
|
||||
|
||||
@register("southernortho")
|
||||
class SouthernOrthoAdapter(ManifestDrivenAdapter):
|
||||
_BASE_URL = "https://recruiting.paylocity.com"
|
||||
|
||||
def collect_cards(self, page, manifest):
|
||||
try:
|
||||
page.wait_for_selector(".job-listing-job-item", timeout=12000, state="attached")
|
||||
except Exception:
|
||||
logger.debug("[southernortho] Results wait timed out — may be zero results")
|
||||
return super().collect_cards(page, manifest)
|
||||
|
||||
def normalize(self, raw: RawJobCard) -> JobPostingCandidate:
|
||||
candidate = super().normalize(raw)
|
||||
if candidate.job_url and candidate.job_url.startswith("/"):
|
||||
candidate.job_url = self._BASE_URL + candidate.job_url
|
||||
if candidate.posted_text:
|
||||
candidate.posted_text = candidate.posted_text.replace(" - ", "").strip()
|
||||
return candidate
|
||||
@ -0,0 +1,58 @@
|
||||
"""USAJOBS adapter — waits for JS render, handles relative URLs, URL-based pagination."""
|
||||
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
|
||||
|
||||
|
||||
@register("usajobs")
|
||||
class UsaJobsAdapter(ManifestDrivenAdapter):
|
||||
_BASE_URL = "https://www.usajobs.gov"
|
||||
|
||||
def _wait_for_results(self, page) -> None:
|
||||
"""Wait for JS-rendered results or zero-results heading."""
|
||||
try:
|
||||
page.wait_for_selector(
|
||||
"#search-results .border, h2.font-normal",
|
||||
timeout=12000,
|
||||
state="attached",
|
||||
)
|
||||
except Exception:
|
||||
logger.debug("[usajobs] Results wait timed out — may be zero results")
|
||||
|
||||
def collect_cards(self, page, manifest):
|
||||
self._wait_for_results(page)
|
||||
return super().collect_cards(page, manifest)
|
||||
|
||||
def paginate(self, page, page_index: int, config) -> bool:
|
||||
"""Navigate to next page by incrementing the &p= URL parameter."""
|
||||
max_pages = config.pagination.max_pages
|
||||
if page_index >= max_pages - 1:
|
||||
return False
|
||||
|
||||
current_url = page.url
|
||||
next_page = page_index + 2 # page_index is 0-based; URL param is 1-based
|
||||
|
||||
# Replace &p=N or add &p=N
|
||||
if re.search(r"[?&]p=\d+", current_url):
|
||||
next_url = re.sub(r"(p=)\d+", f"p={next_page}", current_url)
|
||||
else:
|
||||
sep = "&" if "?" in current_url else "?"
|
||||
next_url = f"{current_url}{sep}p={next_page}"
|
||||
|
||||
logger.debug(f"[usajobs] Paginating to page {next_page}: {next_url}")
|
||||
page.goto(next_url, timeout=30000)
|
||||
self._wait_for_results(page)
|
||||
return True
|
||||
|
||||
def normalize(self, raw: RawJobCard) -> JobPostingCandidate:
|
||||
candidate = super().normalize(raw)
|
||||
# USAJOBS job links are relative: /job/123456 → absolute URL
|
||||
if candidate.job_url and candidate.job_url.startswith("/"):
|
||||
candidate.job_url = self._BASE_URL + candidate.job_url
|
||||
return candidate
|
||||
@ -0,0 +1,53 @@
|
||||
"""Telegram notification client — sends messages via Bot API."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
TELEGRAM_API_URL = "https://api.telegram.org/bot{token}/sendMessage"
|
||||
MAX_TEXT_LENGTH = 4096 # Telegram message limit
|
||||
|
||||
|
||||
class TelegramClient:
|
||||
def __init__(
|
||||
self,
|
||||
bot_token: Optional[str] = None,
|
||||
chat_id: Optional[str] = None,
|
||||
):
|
||||
self.bot_token = bot_token or os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
||||
self.chat_id = chat_id or os.environ.get("TELEGRAM_CHAT_ID", "")
|
||||
|
||||
def is_configured(self) -> bool:
|
||||
return bool(self.bot_token and self.chat_id)
|
||||
|
||||
def send_message(self, text: str) -> bool:
|
||||
"""Send a message to the configured chat. Returns True on success."""
|
||||
if not self.is_configured():
|
||||
logger.warning("Telegram not configured (missing BOT_TOKEN or CHAT_ID)")
|
||||
return False
|
||||
|
||||
if len(text) > MAX_TEXT_LENGTH:
|
||||
text = text[: MAX_TEXT_LENGTH - 20] + "\n\n[...truncated]"
|
||||
|
||||
try:
|
||||
import httpx
|
||||
response = httpx.post(
|
||||
TELEGRAM_API_URL.format(token=self.bot_token),
|
||||
json={
|
||||
"chat_id": self.chat_id,
|
||||
"text": text,
|
||||
"parse_mode": "HTML",
|
||||
},
|
||||
timeout=15.0,
|
||||
)
|
||||
if response.status_code == 200 and response.json().get("ok"):
|
||||
logger.info("Telegram message sent successfully")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Telegram API error: {response.text[:200]}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Telegram send failed: {e}")
|
||||
return False
|
||||
@ -0,0 +1,60 @@
|
||||
site_id: aaoinfo
|
||||
enabled: true
|
||||
repair_needed: false
|
||||
|
||||
identity:
|
||||
label: AAO Career Center
|
||||
category: aggregator
|
||||
base_url: https://careers.aaoinfo.org
|
||||
start_url_template: "https://careers.aaoinfo.org/jobs/"
|
||||
|
||||
browser:
|
||||
profile_mode: persistent_chrome_profile
|
||||
profile_name: JobAgent
|
||||
headed_on_learn: true
|
||||
headless_on_run: true
|
||||
|
||||
search:
|
||||
keyword_mode: none
|
||||
location_mode: none
|
||||
sort_mode: none
|
||||
date_mode: none
|
||||
result_list_wait_selector: "div.job-tile"
|
||||
|
||||
pagination:
|
||||
mode: none
|
||||
max_pages: 10
|
||||
|
||||
extract:
|
||||
container_selectors:
|
||||
- "div.job-tile:not(.job-mo)"
|
||||
fields:
|
||||
title:
|
||||
text:
|
||||
- ".job-title a"
|
||||
company:
|
||||
text:
|
||||
- ".job-company-row"
|
||||
location:
|
||||
text:
|
||||
- ".job-location"
|
||||
posted_text:
|
||||
text:
|
||||
- ".job-posted-date"
|
||||
url:
|
||||
attr:
|
||||
selector: ".job-title a"
|
||||
name: href
|
||||
|
||||
post_filters:
|
||||
include_posted_text: []
|
||||
|
||||
healthcheck:
|
||||
min_items_expected: 0
|
||||
required_fields:
|
||||
- title
|
||||
fail_if_zero_when_known_active: false
|
||||
|
||||
learn:
|
||||
last_learned_at: null
|
||||
source_url: null
|
||||
@ -0,0 +1,56 @@
|
||||
site_id: aroragroup
|
||||
enabled: true
|
||||
repair_needed: false
|
||||
|
||||
identity:
|
||||
label: Arora Group Jobs
|
||||
category: employer
|
||||
base_url: https://jobs.aroragroup.com
|
||||
start_url_template: "https://jobs.aroragroup.com/"
|
||||
|
||||
browser:
|
||||
profile_mode: persistent_chrome_profile
|
||||
profile_name: JobAgent
|
||||
headed_on_learn: true
|
||||
headless_on_run: true
|
||||
|
||||
search:
|
||||
keyword_mode: none
|
||||
location_mode: none
|
||||
sort_mode: none
|
||||
date_mode: none
|
||||
result_list_wait_selector: ".job-post-row"
|
||||
|
||||
pagination:
|
||||
mode: none
|
||||
max_pages: 1
|
||||
|
||||
extract:
|
||||
container_selectors:
|
||||
- ".job-post-row[onclick]"
|
||||
fields:
|
||||
title:
|
||||
text:
|
||||
- ".POST_TITLE"
|
||||
- "h3.job-post-title a"
|
||||
location:
|
||||
text:
|
||||
- ".POST_LOCATION"
|
||||
- ".job-post-location"
|
||||
url:
|
||||
attr:
|
||||
selector: "a.job-post-href"
|
||||
name: href
|
||||
|
||||
post_filters:
|
||||
include_posted_text: []
|
||||
|
||||
healthcheck:
|
||||
min_items_expected: 0
|
||||
required_fields:
|
||||
- title
|
||||
fail_if_zero_when_known_active: false
|
||||
|
||||
learn:
|
||||
last_learned_at: null
|
||||
source_url: null
|
||||
@ -0,0 +1,64 @@
|
||||
site_id: docshealth
|
||||
enabled: true
|
||||
repair_needed: false
|
||||
|
||||
identity:
|
||||
label: DOCS Health
|
||||
category: employer
|
||||
base_url: https://recruiting.paylocity.com
|
||||
start_url_template: "https://recruiting.paylocity.com/recruiting/jobs/All/4f5b87cb-fda9-4243-976b-499bbf0ce9fc/DOCS-Health?search={keywords_urlencoded}&location=All%20Locations&department=All%20Departments"
|
||||
|
||||
browser:
|
||||
profile_mode: persistent_chrome_profile
|
||||
profile_name: JobAgent
|
||||
headed_on_learn: true
|
||||
headless_on_run: true
|
||||
|
||||
search:
|
||||
keyword_mode: url_param
|
||||
location_mode: url_param
|
||||
sort_mode: none
|
||||
date_mode: none
|
||||
result_list_wait_selector: ".job-listing-job-item"
|
||||
|
||||
search_override:
|
||||
keywords:
|
||||
- dentist
|
||||
- orthodontics
|
||||
- orthodontist
|
||||
multi_keyword_mode: separate
|
||||
|
||||
pagination:
|
||||
mode: none
|
||||
max_pages: 1
|
||||
|
||||
extract:
|
||||
container_selectors:
|
||||
- "div.job-listing-job-item"
|
||||
fields:
|
||||
title:
|
||||
text:
|
||||
- ".job-item-title a"
|
||||
location:
|
||||
text:
|
||||
- ".location-column .job-item-normal"
|
||||
posted_text:
|
||||
text:
|
||||
- ".job-title-column > span:not(.job-item-title):not(.job-item-normal)"
|
||||
url:
|
||||
attr:
|
||||
selector: ".job-item-title a"
|
||||
name: href
|
||||
|
||||
post_filters:
|
||||
include_posted_text: []
|
||||
|
||||
healthcheck:
|
||||
min_items_expected: 0
|
||||
required_fields:
|
||||
- title
|
||||
fail_if_zero_when_known_active: false
|
||||
|
||||
learn:
|
||||
last_learned_at: null
|
||||
source_url: null
|
||||
@ -0,0 +1,60 @@
|
||||
site_id: hospitaljobsonline
|
||||
enabled: true
|
||||
repair_needed: false
|
||||
|
||||
identity:
|
||||
label: Hospital Jobs Online
|
||||
category: aggregator
|
||||
base_url: https://www.hospitaljobsonline.com
|
||||
start_url_template: "https://www.hospitaljobsonline.com/jobs/q-korea-jobs//?page=1&limit=100"
|
||||
|
||||
browser:
|
||||
profile_mode: persistent_chrome_profile
|
||||
profile_name: JobAgent
|
||||
headed_on_learn: true
|
||||
headless_on_run: true
|
||||
|
||||
search:
|
||||
keyword_mode: none
|
||||
location_mode: none
|
||||
sort_mode: none
|
||||
date_mode: none
|
||||
result_list_wait_selector: "div.jobresult"
|
||||
|
||||
pagination:
|
||||
mode: url_increment
|
||||
max_pages: 10
|
||||
|
||||
extract:
|
||||
container_selectors:
|
||||
- "div.jobresult"
|
||||
fields:
|
||||
title:
|
||||
text:
|
||||
- ".jobtitle a"
|
||||
company:
|
||||
text:
|
||||
- ".location a"
|
||||
location:
|
||||
text:
|
||||
- ".location"
|
||||
posted_text:
|
||||
text:
|
||||
- ".location .date"
|
||||
url:
|
||||
attr:
|
||||
selector: ".jobtitle a"
|
||||
name: href
|
||||
|
||||
post_filters:
|
||||
include_posted_text: []
|
||||
|
||||
healthcheck:
|
||||
min_items_expected: 0
|
||||
required_fields:
|
||||
- title
|
||||
fail_if_zero_when_known_active: false
|
||||
|
||||
learn:
|
||||
last_learned_at: null
|
||||
source_url: null
|
||||
@ -0,0 +1,74 @@
|
||||
site_id: linkedin
|
||||
enabled: true
|
||||
repair_needed: false
|
||||
|
||||
identity:
|
||||
label: LinkedIn Jobs
|
||||
category: aggregator
|
||||
base_url: https://www.linkedin.com
|
||||
start_url_template: "https://www.linkedin.com/jobs/search/?keywords={keywords_urlencoded}&f_TPR=r86400&sortBy=DD"
|
||||
|
||||
browser:
|
||||
profile_mode: persistent_chrome_profile
|
||||
profile_name: JobAgent
|
||||
headed_on_learn: true
|
||||
headless_on_run: false
|
||||
|
||||
search:
|
||||
keyword_mode: url_param
|
||||
location_mode: url_param
|
||||
sort_mode: url_param
|
||||
date_mode: url_param
|
||||
result_list_wait_selector: ".jobs-search__results-list li, .scaffold-layout__list-container li"
|
||||
|
||||
pagination:
|
||||
mode: next_button
|
||||
next_button_selectors:
|
||||
- "button[aria-label='View next page']"
|
||||
- "button[aria-label*='next']"
|
||||
- "li.artdeco-pagination__indicator--number.selected + li button"
|
||||
max_pages: 3
|
||||
|
||||
extract:
|
||||
container_selectors:
|
||||
- ".jobs-search__results-list li"
|
||||
- ".scaffold-layout__list-container li"
|
||||
- "li:has(.base-card)"
|
||||
fields:
|
||||
title:
|
||||
text:
|
||||
- "h3.base-search-card__title"
|
||||
- ".job-card-list__title"
|
||||
- "h3"
|
||||
- "a[data-control-name='job_card_title']"
|
||||
company:
|
||||
text:
|
||||
- "h4.base-search-card__subtitle"
|
||||
- ".job-card-container__primary-description"
|
||||
- "a.job-card-container__company-name"
|
||||
location:
|
||||
text:
|
||||
- "span.job-search-card__location"
|
||||
- ".job-card-container__metadata-item"
|
||||
posted_text:
|
||||
text:
|
||||
- "time"
|
||||
- ".job-search-card__listdate"
|
||||
- "span[class*='listdate']"
|
||||
url:
|
||||
attr:
|
||||
selector: "a.base-card__full-link, a[data-control-name='job_card_title'], a[href*='/jobs/view/']"
|
||||
name: href
|
||||
|
||||
post_filters:
|
||||
include_posted_text: []
|
||||
|
||||
healthcheck:
|
||||
min_items_expected: 0
|
||||
required_fields:
|
||||
- title
|
||||
fail_if_zero_when_known_active: false
|
||||
|
||||
learn:
|
||||
last_learned_at: null
|
||||
source_url: null
|
||||
@ -0,0 +1,57 @@
|
||||
site_id: pdshealth
|
||||
enabled: true
|
||||
repair_needed: false
|
||||
|
||||
identity:
|
||||
label: PDS Health
|
||||
category: employer
|
||||
base_url: https://jobs.pdshealth.com
|
||||
start_url_template: "https://jobs.pdshealth.com/jobs?keywords=orthodontist&sortBy=relevance&page=1&limit=100"
|
||||
|
||||
browser:
|
||||
profile_mode: persistent_chrome_profile
|
||||
profile_name: JobAgent
|
||||
headed_on_learn: true
|
||||
headless_on_run: true
|
||||
|
||||
search:
|
||||
keyword_mode: none
|
||||
location_mode: none
|
||||
sort_mode: none
|
||||
date_mode: none
|
||||
result_list_wait_selector: "mat-expansion-panel.search-result-item"
|
||||
|
||||
pagination:
|
||||
mode: url_increment
|
||||
max_pages: 20
|
||||
|
||||
extract:
|
||||
container_selectors:
|
||||
- "mat-expansion-panel.search-result-item"
|
||||
fields:
|
||||
title:
|
||||
text:
|
||||
- "a.job-title-link"
|
||||
company:
|
||||
text:
|
||||
- ".label-value.tags1"
|
||||
location:
|
||||
text:
|
||||
- ".label-value.location"
|
||||
url:
|
||||
attr:
|
||||
selector: "a.job-title-link"
|
||||
name: href
|
||||
|
||||
post_filters:
|
||||
include_posted_text: []
|
||||
|
||||
healthcheck:
|
||||
min_items_expected: 0
|
||||
required_fields:
|
||||
- title
|
||||
fail_if_zero_when_known_active: false
|
||||
|
||||
learn:
|
||||
last_learned_at: null
|
||||
source_url: null
|
||||
@ -0,0 +1,57 @@
|
||||
site_id: saltdental
|
||||
enabled: true
|
||||
repair_needed: false
|
||||
|
||||
identity:
|
||||
label: Salt Dental Partners
|
||||
category: employer
|
||||
base_url: https://jobs.saltdentalpartners.com
|
||||
start_url_template: "https://jobs.saltdentalpartners.com/jobs?keywords=orthodontist&sortBy=relevance&page=1"
|
||||
|
||||
browser:
|
||||
profile_mode: persistent_chrome_profile
|
||||
profile_name: JobAgent
|
||||
headed_on_learn: true
|
||||
headless_on_run: true
|
||||
|
||||
search:
|
||||
keyword_mode: none
|
||||
location_mode: none
|
||||
sort_mode: none
|
||||
date_mode: none
|
||||
result_list_wait_selector: "mat-expansion-panel.search-result-item"
|
||||
|
||||
pagination:
|
||||
mode: url_increment
|
||||
max_pages: 5
|
||||
|
||||
extract:
|
||||
container_selectors:
|
||||
- "mat-expansion-panel.search-result-item"
|
||||
fields:
|
||||
title:
|
||||
text:
|
||||
- "a.job-title-link"
|
||||
company:
|
||||
text:
|
||||
- ".label-value.tags1"
|
||||
location:
|
||||
text:
|
||||
- ".label-value.location"
|
||||
url:
|
||||
attr:
|
||||
selector: "a.job-title-link"
|
||||
name: href
|
||||
|
||||
post_filters:
|
||||
include_posted_text: []
|
||||
|
||||
healthcheck:
|
||||
min_items_expected: 0
|
||||
required_fields:
|
||||
- title
|
||||
fail_if_zero_when_known_active: false
|
||||
|
||||
learn:
|
||||
last_learned_at: null
|
||||
source_url: null
|
||||
@ -0,0 +1,57 @@
|
||||
site_id: southernortho
|
||||
enabled: true
|
||||
repair_needed: false
|
||||
|
||||
identity:
|
||||
label: Southern Orthodontic Partners
|
||||
category: employer
|
||||
base_url: https://recruiting.paylocity.com
|
||||
start_url_template: "https://recruiting.paylocity.com/recruiting/jobs/All/06144481-7908-457c-9312-51cf7b58cf01/Southern-Orthodontic-Partners-Mgmt-LLC?search=orthodontist&location=All%20Locations&department=All%20Departments"
|
||||
|
||||
browser:
|
||||
profile_mode: persistent_chrome_profile
|
||||
profile_name: JobAgent
|
||||
headed_on_learn: true
|
||||
headless_on_run: true
|
||||
|
||||
search:
|
||||
keyword_mode: none
|
||||
location_mode: none
|
||||
sort_mode: none
|
||||
date_mode: none
|
||||
result_list_wait_selector: ".job-listing-job-item"
|
||||
|
||||
pagination:
|
||||
mode: none
|
||||
max_pages: 1
|
||||
|
||||
extract:
|
||||
container_selectors:
|
||||
- "div.job-listing-job-item"
|
||||
fields:
|
||||
title:
|
||||
text:
|
||||
- ".job-item-title a"
|
||||
company:
|
||||
text:
|
||||
- ".location-column .job-item-normal"
|
||||
posted_text:
|
||||
text:
|
||||
- ".job-title-column > span:not(.job-item-title):not(.job-item-normal)"
|
||||
url:
|
||||
attr:
|
||||
selector: ".job-item-title a"
|
||||
name: href
|
||||
|
||||
post_filters:
|
||||
include_posted_text: []
|
||||
|
||||
healthcheck:
|
||||
min_items_expected: 0
|
||||
required_fields:
|
||||
- title
|
||||
fail_if_zero_when_known_active: false
|
||||
|
||||
learn:
|
||||
last_learned_at: null
|
||||
source_url: null
|
||||
@ -0,0 +1,70 @@
|
||||
site_id: usajobs
|
||||
enabled: true
|
||||
repair_needed: false
|
||||
|
||||
identity:
|
||||
label: USAJOBS
|
||||
category: government
|
||||
base_url: https://www.usajobs.gov
|
||||
start_url_template: "https://www.usajobs.gov/Search/Results?k={keywords_urlencoded}&p=1"
|
||||
|
||||
browser:
|
||||
profile_mode: persistent_chrome_profile
|
||||
profile_name: JobAgent
|
||||
headed_on_learn: true
|
||||
headless_on_run: true
|
||||
|
||||
search:
|
||||
keyword_mode: url_param
|
||||
location_mode: url_param
|
||||
sort_mode: url_param
|
||||
date_mode: url_param
|
||||
result_list_wait_selector: ""
|
||||
|
||||
pagination:
|
||||
mode: next_button
|
||||
next_button_selectors:
|
||||
- "button#page-m-next"
|
||||
- "button[title='Go to next page']"
|
||||
max_pages: 10
|
||||
|
||||
extract:
|
||||
container_selectors:
|
||||
- "#search-results .border.border-gray-lighter"
|
||||
fields:
|
||||
title:
|
||||
text:
|
||||
- "h2 a"
|
||||
company:
|
||||
text:
|
||||
- "p strong"
|
||||
location:
|
||||
text:
|
||||
- "div:nth-child(2) div:first-child p:nth-child(3)"
|
||||
posted_text:
|
||||
text:
|
||||
- ".italic"
|
||||
url:
|
||||
attr:
|
||||
selector: "h2 a"
|
||||
name: href
|
||||
|
||||
search_override:
|
||||
keywords:
|
||||
- dentist
|
||||
- orthodontics
|
||||
- orthodontist
|
||||
multi_keyword_mode: separate
|
||||
|
||||
post_filters:
|
||||
include_posted_text: []
|
||||
|
||||
healthcheck:
|
||||
min_items_expected: 0
|
||||
required_fields:
|
||||
- title
|
||||
fail_if_zero_when_known_active: false
|
||||
|
||||
learn:
|
||||
last_learned_at: null
|
||||
source_url: null
|
||||
Loading…
Reference in New Issue