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.
This commit is contained in:
2026-08-27 09:03:05 +02:00
parent 3f5360f44c
commit b4957025b1
31 changed files with 2008 additions and 283 deletions
+83
View File
@@ -0,0 +1,83 @@
// 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}`);
}