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.
91 lines
4.4 KiB
TypeScript
91 lines
4.4 KiB
TypeScript
// Zürich sweep via job-room.ch (canton ZH), scored against cv/polo.md
|
|
import { NON_ENGLISH_RE, JUNIOR_TITLE_RE, JAVA_TITLE_RE, SOFTWARE_TITLE_RE } from "../fetch.ts";
|
|
|
|
const MS_STACK_RE = /\.net\b|\bc#|sharepoint|dynamics 365|power platform|\bsccm\b|intune|successfactors|\bsap\b|\bm365\b|servicenow|salesforce|avaloq|abap/i;
|
|
const PHARMA_RE = /\b(clinical|pharmaceutical|drug\s+(substance|product)|ha?ematology|oncology|formulation|laborator|chemist|chemistry|biolog|preclinical|regulatory\s+affairs|medical\s+affairs|veterinary|pathologist)\b/i;
|
|
const DE_FR_RE = /\b(und|oder|wir|Sie|Ihre|Ihrer|Kenntnisse|Erfahrung|Aufgaben|Anforderungen|Stelle|Unternehmen|Mitarbeiter|Bereich|nous|vous|votre|pour|avec|des|les)\b/g;
|
|
const EMBEDDED_RE = /\bembedded\b|\bfirmware\b|\bplc\b|\bsps\b|\bqt\b|\bc\+\+\b|automation\s?engineer/i;
|
|
const SUPPORT_RE = /service\s?desk|1st|2nd\s?level|helpdesk|support\s?engineer|technician|operator/i;
|
|
|
|
const SENIOR_RE = /\b(senior|staff|principal|lead|head|director|architect|vp)\b/i;
|
|
const STACK_RE = /\b(python|typescript|javascript|ruby|rails|react|node\.?js|golang|\bgo\b|postgres|kafka)\b/i;
|
|
const DOMAIN_RE = /\b(platform|kubernetes|docker|terraform|ci\/cd|devops|aws|data\s+(pipeline|platform|engineer)|llm|genai|microservice|sre)\b/i;
|
|
|
|
const strip = (s: string) => s.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
|
|
const API = "https://www.job-room.ch/jobadservice/api/jobAdvertisements/_search";
|
|
const UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
|
|
const KWS = ["software", "engineer", "developer", "devops", "data", "python", "cloud",
|
|
"platform", "typescript", "backend", "full stack", "ai", "kubernetes", "react"];
|
|
|
|
const ads = new Map<string, any>();
|
|
for (const kw of KWS) {
|
|
process.stdout.write(`${kw} `);
|
|
for (let page = 0; page < 8; page++) {
|
|
const res = await fetch(`${API}?page=${page}&size=100`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", Accept: "application/json", "User-Agent": UA },
|
|
body: JSON.stringify({
|
|
onlineSince: 30, displayRestricted: false, keywords: [kw],
|
|
cantonCodes: ["ZH"], workloadPercentageMin: 0, workloadPercentageMax: 100,
|
|
}),
|
|
});
|
|
if (!res.ok) break;
|
|
const hits = (await res.json()) as any[];
|
|
for (const h of hits) if (h.jobAdvertisement?.id) ads.set(h.jobAdvertisement.id, h.jobAdvertisement);
|
|
if (hits.length < 100) break;
|
|
await Bun.sleep(200);
|
|
}
|
|
await Bun.sleep(200);
|
|
}
|
|
console.log(`\n${ads.size} unique ads in ZH\n`);
|
|
|
|
const kept: any[] = [];
|
|
for (const ad of ads.values()) {
|
|
const c = ad.jobContent;
|
|
const d = c?.jobDescriptions?.[0];
|
|
if (!d) continue;
|
|
const title = strip(d.title);
|
|
if (NON_ENGLISH_RE.test(title) || JUNIOR_TITLE_RE.test(title) || JAVA_TITLE_RE.test(title)) continue;
|
|
if (!SOFTWARE_TITLE_RE.test(title)) continue;
|
|
if (MS_STACK_RE.test(title) || PHARMA_RE.test(title)) continue;
|
|
if (EMBEDDED_RE.test(title) || SUPPORT_RE.test(title)) continue;
|
|
|
|
const body = strip(d.description ?? "");
|
|
if ((body.match(DE_FR_RE) ?? []).length >= 6) continue;
|
|
if (MS_STACK_RE.test(body)) continue;
|
|
|
|
const hay = `${title} ${body}`;
|
|
let score = 0;
|
|
if (SENIOR_RE.test(title)) score += 3;
|
|
score += (hay.match(STACK_RE) ?? []).length;
|
|
score += (hay.match(DOMAIN_RE) ?? []).length;
|
|
|
|
const langs = (c.languageSkills ?? [])
|
|
.map((l: any) => `${l.languageIsoCode}:${l.spokenLevel ?? "?"}`).join(" ");
|
|
// A declared German requirement above B1 is disqualifying
|
|
const hardGerman = /de(-ch)?:(PROFICIENT|ADVANCED)/.test(langs);
|
|
|
|
kept.push({
|
|
score, title,
|
|
company: c.company?.name ?? "",
|
|
city: c.location?.city ?? "",
|
|
workload: `${c.employment?.workloadPercentageMin ?? "?"}-${c.employment?.workloadPercentageMax ?? "?"}%`,
|
|
posted: ad.createdTime?.slice(0, 10) ?? "",
|
|
langs, hardGerman,
|
|
url: c.externalUrl ?? `https://www.job-room.ch/job-search/${ad.id}`,
|
|
snippet: body.slice(0, 230),
|
|
});
|
|
}
|
|
|
|
kept.sort((a, b) => b.score - a.score || b.posted.localeCompare(a.posted));
|
|
const good = kept.filter((k) => !k.hardGerman);
|
|
console.log(`${kept.length} survivors (${kept.length - good.length} dropped for German requirement)\n`);
|
|
|
|
for (const j of good.slice(0, 30)) {
|
|
console.log(`### [${j.score}] ${j.title} — ${j.company}`);
|
|
console.log(` ${j.city} · ${j.workload} · ${j.posted}${j.langs ? ` · ${j.langs}` : ""}`);
|
|
console.log(` ${j.url}`);
|
|
console.log(` ${j.snippet}\n`);
|
|
}
|