// jobs.ch — public search API sweep, Switzerland-wide. // Endpoint: /api/v1/public/search?query=…&rows=…&page=… import { NON_ENGLISH_RE, JUNIOR_TITLE_RE, JAVA_TITLE_RE, SOFTWARE_TITLE_RE } from "../fetch.ts"; import { readFileSync } from "fs"; const csv = readFileSync("./results/jobs.csv", "utf8"); const existing = new Set([...csv.matchAll(/"(https?:\/\/[^"]+)"/g)].map((m) => m[1])); const QUERIES = [ "software engineer", "full stack engineer", "backend engineer", "platform engineer", "devops engineer", "data engineer", "AI engineer", "machine learning engineer", "python developer", "typescript developer", "ruby on rails", "node.js developer", "tech lead", "engineering manager", "staff engineer", "principal engineer", "solution architect", "data scientist", ]; const MS_RE = /\b(\.net|c#|azure|sharepoint|dynamics|power\s?(bi|platform|apps)|sccm|intune|windows|sap|abap)\b/i; // Entry-level roles whose *title* looks neutral — caught only in the description. const JUNIOR_TEXT_RE = /\b(0\s*[-–to]+\s*[1-5]\s*years|entry.level|junior|graduate program|no experience required)\b/i; const DE_FR_WORDS_RE = /\b(und|oder|wir|Sie|Ihre|Ihrer|Kenntnisse|Erfahrung|Aufgaben|Anforderungen|Stelle|Unternehmen|Mitarbeiter|Bereich|für|mit|nous|vous|votre|pour|avec|des|les|une|dans)\b/g; interface Doc { job_id: string; title: string; preview?: string; company_name?: string; place?: string; publication_date?: string; language_skills?: Array<{ language: string; level: number }>; _links?: { detail_en?: { href: string } }; } const seen = new Map(); for (const q of QUERIES) { // rows is capped at 20 by the API (422 above that); sort=date puts fresh postings first. for (let page = 1; page <= 6; page++) { const url = `https://www.jobs.ch/api/v1/public/search?query=${encodeURIComponent(q)}&rows=20&page=${page}&sort=date`; const res = await fetch(url, { headers: { "User-Agent": "Mozilla/5.0", Accept: "application/json" }, }); if (!res.ok) break; const data = (await res.json()) as { documents: Doc[]; num_pages: number }; for (const d of data.documents ?? []) if (!seen.has(d.job_id)) seen.set(d.job_id, d); if (page >= (data.num_pages ?? 1)) break; await Bun.sleep(250); } console.log(`${q}: ${seen.size} unique so far`); } console.log(`\n${seen.size} raw postings. Filtering…\n`); const out: Array = []; let dropped = { title: 0, ms: 0, lang: 0, junior: 0, dup: 0 }; for (const d of seen.values()) { const t = d.title; if (NON_ENGLISH_RE.test(t) || JUNIOR_TITLE_RE.test(t) || JAVA_TITLE_RE.test(t) || !SOFTWARE_TITLE_RE.test(t)) { dropped.title++; continue; } if (MS_RE.test(t)) { dropped.ms++; continue; } // German/French required at a working level → drop const needsDeFr = (d.language_skills ?? []).some( (l) => (l.language === "de" || l.language === "fr") && l.level >= 3 ); const preview = (d.preview ?? "").replace(/<[^>]+>/g, " "); const deWords = (preview.match(DE_FR_WORDS_RE) ?? []).length; if (needsDeFr || deWords >= 4) { dropped.lang++; continue; } if (JUNIOR_TEXT_RE.test(preview)) { dropped.junior++; continue; } const link = d._links?.detail_en?.href ?? `https://www.jobs.ch/en/vacancies/detail/${d.job_id}/`; if (existing.has(link)) { dropped.dup++; continue; } out.push({ ...d, link }); } out.sort((a, b) => (b.publication_date ?? "").localeCompare(a.publication_date ?? "")); console.log(`dropped: ${dropped.title} title, ${dropped.ms} MS-stack, ${dropped.lang} DE/FR, ${dropped.junior} entry-level, ${dropped.dup} already in CSV`); console.log(`\n=== ${out.length} jobs.ch CANDIDATES ===\n`); for (const j of out) { console.log(`${j.title} | ${j.company_name} | ${j.place} | ${(j.publication_date ?? "").slice(0, 10)}`); console.log(` ${j.link}`); }