""" We Work Remotely (weworkremotely.com) scraper — public RSS feed. One of the largest remote job boards. Good for WFH PM roles worldwide. RSS endpoint requires no authentication. """ import logging import xml.etree.ElementTree as ET from .base import BaseScraper, Job log = logging.getLogger("scraper.wwr") _RSS_NAMESPACE = { "content": "http://purl.org/rss/1.0/modules/content/", } class WeWorkRemotelyScraper(BaseScraper): """Scrapes We Work Remotely via their public RSS search feed.""" RSS_URL = "https://weworkremotely.com/remote-jobs/search.rss" def __init__(self): super().__init__("WeWorkRemotely") def search(self, role: str, location: str, max_results: int = 25) -> list: """Fetch PM jobs from the WWR RSS search feed.""" params = {"term": role or "product manager"} resp = self._get(self.RSS_URL, params=params) if not resp: log.warning("WWR RSS returned no response") return [] jobs = [] try: root = ET.fromstring(resp.content) channel = root.find("channel") if channel is None: return [] for item in channel.findall("item"): title_raw = _text(item, "title") url = _text(item, "link") or _text(item, "guid") pub_date = _text(item, "pubDate") or "" region = _text(item, "region") or "Worldwide" # WWR title format: "Company Name: Job Title" if ": " in title_raw: company_raw, job_title = title_raw.split(": ", 1) elif " at " in title_raw: job_title, company_raw = title_raw.rsplit(" at ", 1) else: job_title = title_raw company_raw = "" job_title = job_title.strip() company_raw = company_raw.strip() if not self.is_pm_role(job_title): continue # Location filter: skip jobs explicitly for US/Europe only region_l = region.lower() loc_lower = location.lower() if loc_lower not in ("remote", "worldwide"): blocked = ("usa", "us only", "europe", "uk", "canada", "australia") if any(b in region_l for b in blocked) and "worldwide" not in region_l: continue # Parse description from (may contain HTML) raw_desc = _text(item, "description") or "" if raw_desc and "<" in raw_desc: try: from bs4 import BeautifulSoup raw_desc = BeautifulSoup(raw_desc, "lxml").get_text(separator="\n", strip=True) except Exception: pass raw_desc = raw_desc[:3000] # Normalise the URL (WWR uses for the actual job URL) if not url or "weworkremotely.com" not in url: guid = _text(item, "guid") if guid and "weworkremotely" in guid: url = guid jobs.append(Job( title=job_title, company=company_raw, location=region or "Remote", url=url or "", platform="WeWorkRemotely", description=raw_desc, salary="Not specified", posted_date=pub_date[:16] if pub_date else "", job_id=(url or "").split("/")[-1], )) if len(jobs) >= max_results: break except ET.ParseError as e: log.warning(f"WWR XML parse error: {e}") except Exception as e: log.warning(f"WWR scrape error: {e}", exc_info=True) log.info(f"WWR: {len(jobs)} PM jobs for role={role!r}") return jobs def get_details_bulk(self, jobs: list, progress_cb=None) -> None: """Descriptions already parsed from RSS — nothing to fetch.""" if progress_cb: progress_cb(len(jobs), len(jobs)) def _text(el, tag: str) -> str: child = el.find(tag) return (child.text or "").strip() if child is not None else ""