Files
rodado b4957025b1 chore: update search scripts, extra sources and post drafts
Adds the one-off scripts accumulated across recent sessions, removes
temp scripts whose searches now live in config.ts as role-family
sweeps, and updates fetch-other.ts plus the LinkedIn post drafts.
2026-08-27 09:03:05 +02:00

84 lines
3.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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<string, Doc>();
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<Doc & { link: string }> = [];
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}`);
}