mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-26 23:51:14 +02:00
feat: indeed url classifier and search url builder
This commit is contained in:
parent
01c3f097e6
commit
d87c86dcc4
1 changed files with 124 additions and 0 deletions
|
|
@ -0,0 +1,124 @@
|
|||
"""Classify Indeed URLs and build search URLs.
|
||||
|
||||
Recognizes search pages (``/jobs?q=&l=``), company pages (``/cmp/<slug>/jobs``),
|
||||
and single jobs (``/viewjob?jk=``); other hosts resolve to ``None``. Also owns
|
||||
the country->domain map so classification and URL building share one source.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Literal
|
||||
from urllib.parse import parse_qs, urlencode, urlparse
|
||||
|
||||
ResolvedKind = Literal["search", "company", "job"]
|
||||
|
||||
# Locale subdomains that deviate from the ISO code; others map to <cc>.indeed.com.
|
||||
_DOMAIN_OVERRIDES = {"us": "www", "gb": "uk"}
|
||||
|
||||
_JT_VALUES = frozenset(
|
||||
{
|
||||
"fulltime",
|
||||
"parttime",
|
||||
"contract",
|
||||
"internship",
|
||||
"temporary",
|
||||
"permanent",
|
||||
"seasonal",
|
||||
"freelance",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ResolvedUrl:
|
||||
kind: ResolvedKind
|
||||
value: str # search query, company slug, or job key
|
||||
url: str
|
||||
domain: str
|
||||
location: str | None = None
|
||||
params: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
|
||||
def _is_indeed_host(hostname: str | None) -> bool:
|
||||
if not hostname:
|
||||
return False
|
||||
h = hostname.lower()
|
||||
return h == "indeed.com" or h.endswith(".indeed.com")
|
||||
|
||||
|
||||
def country_domain(country: str) -> str:
|
||||
"""Country code -> Indeed host, e.g. ``us`` -> ``www.indeed.com``."""
|
||||
cc = (country or "us").strip().lower()
|
||||
return f"{_DOMAIN_OVERRIDES.get(cc, cc)}.indeed.com"
|
||||
|
||||
|
||||
def resolve_url(url: str) -> ResolvedUrl | None:
|
||||
"""Classify an Indeed URL into a scrape job, or ``None`` if unrecognized."""
|
||||
parsed = urlparse(url)
|
||||
if not _is_indeed_host(parsed.hostname):
|
||||
return None
|
||||
domain = parsed.hostname or "www.indeed.com"
|
||||
path = (parsed.path or "").rstrip("/")
|
||||
query = parse_qs(parsed.query)
|
||||
segments = [s for s in path.split("/") if s]
|
||||
|
||||
# /viewjob?jk=<key>
|
||||
if path.endswith("/viewjob") or segments[:1] == ["viewjob"]:
|
||||
jk = query.get("jk", [None])[0]
|
||||
return ResolvedUrl("job", jk, url, domain) if jk else None
|
||||
|
||||
# /cmp/<slug>/jobs
|
||||
if segments[:1] == ["cmp"] and "jobs" in segments and len(segments) >= 2:
|
||||
return ResolvedUrl("company", segments[1], url, domain)
|
||||
|
||||
# /jobs?q=&l=
|
||||
if path.endswith("/jobs") or segments[-1:] == ["jobs"]:
|
||||
q = query.get("q", [""])[0]
|
||||
loc = query.get("l", [None])[0]
|
||||
extra = {
|
||||
k: v[0]
|
||||
for k, v in query.items()
|
||||
if k in ("radius", "sort", "fromage", "jt", "explvl") and v
|
||||
}
|
||||
return ResolvedUrl("search", q, url, domain, location=loc, params=extra)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def build_search_url(
|
||||
query: str,
|
||||
*,
|
||||
country: str = "us",
|
||||
location: str | None = None,
|
||||
radius: int | None = None,
|
||||
job_type: str | None = None,
|
||||
level: str | None = None,
|
||||
remote: str | None = None,
|
||||
from_days: int | None = None,
|
||||
sort: str = "relevance",
|
||||
start: int = 0,
|
||||
) -> str:
|
||||
"""Build an Indeed ``/jobs`` search URL.
|
||||
|
||||
Remote/hybrid is passed as a query keyword; Indeed's structured ``sc``
|
||||
attribute codes rotate and aren't stable to hardcode.
|
||||
"""
|
||||
domain = country_domain(country)
|
||||
q = f"{query} {remote}".strip() if remote else query
|
||||
params: dict[str, str] = {"q": q}
|
||||
if location:
|
||||
params["l"] = location
|
||||
if radius is not None:
|
||||
params["radius"] = str(radius)
|
||||
if job_type in _JT_VALUES:
|
||||
params["jt"] = job_type # type: ignore[assignment]
|
||||
if level:
|
||||
params["explvl"] = level
|
||||
if from_days is not None:
|
||||
params["fromage"] = str(from_days)
|
||||
if sort == "date":
|
||||
params["sort"] = "date"
|
||||
if start:
|
||||
params["start"] = str(start)
|
||||
return f"https://{domain}/jobs?{urlencode(params)}"
|
||||
Loading…
Add table
Add a link
Reference in a new issue