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:
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env bun
|
||||
// Ruby/Rails sweep: Switzerland-wide (onsite+hybrid) + Switzerland remote + EU remote.
|
||||
// Title-keyword search is too narrow for Ruby — most Rails roles are titled
|
||||
// "Backend Engineer" — so this also runs a description-level Ruby check over
|
||||
// generic backend/fullstack titles.
|
||||
import { fetchJobs, isEnglishContent, NON_ENGLISH_RE, JUNIOR_TITLE_RE } from "../fetch.ts";
|
||||
import type { SearchConfig } from "../config.ts";
|
||||
|
||||
const RUBY_KEYWORDS = [
|
||||
"Ruby on Rails", "Rails Engineer", "Ruby Engineer", "Rails Developer",
|
||||
"Ruby Developer", "Ruby", "Rails",
|
||||
].join(" OR ");
|
||||
|
||||
const BACKEND_KEYWORDS = [
|
||||
"Backend Engineer", "Full Stack Engineer", "Fullstack Developer",
|
||||
"Senior Software Engineer", "Tech Lead", "Staff Engineer",
|
||||
].join(" OR ");
|
||||
|
||||
const RUBY_RE = /\bruby\b|\brails\b|\bsidekiq\b|\bhotwire\b|\bactiverecord\b|\bactive record\b/i;
|
||||
const MS_RE = /\b(\.net|c#|sharepoint|dynamics|sccm|intune)\b/i;
|
||||
|
||||
const csv = await Bun.file("./results/jobs.csv").text();
|
||||
const existing = new Set([...csv.matchAll(/"(https?:\/\/[^"]+)"/g)].map((m) => m[1]));
|
||||
|
||||
const searches: Array<SearchConfig & { descCheck?: boolean }> = [
|
||||
{ label: "CH — Ruby titles", keywords: RUBY_KEYWORDS, location: "Switzerland", workType: ["onsite", "hybrid"], timePosted: "month" },
|
||||
{ label: "CH remote — Ruby titles", keywords: RUBY_KEYWORDS, location: "Switzerland", workType: ["remote"], timePosted: "month" },
|
||||
{ label: "Basel — Ruby titles", keywords: RUBY_KEYWORDS, geoId: "90009885", location: "Basel Metropolitan Area", workType: ["onsite", "hybrid"], timePosted: "month" },
|
||||
{ label: "Zürich — Ruby titles", keywords: RUBY_KEYWORDS, geoId: "90009888", location: "Zürich Area", workType: ["onsite", "hybrid"], timePosted: "month" },
|
||||
{ label: "CH — backend (desc check)", keywords: BACKEND_KEYWORDS, location: "Switzerland", workType: ["onsite", "hybrid", "remote"], timePosted: "month", descCheck: true },
|
||||
];
|
||||
|
||||
type Hit = { title: string; company: string; location: string; link: string; label: string; via: string };
|
||||
const hits: Hit[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const config of searches) {
|
||||
console.log(`\n[${config.label}]`);
|
||||
let jobs;
|
||||
try {
|
||||
jobs = await fetchJobs(config, { raw: true });
|
||||
} catch (e) {
|
||||
console.log(` error: ${e}`);
|
||||
continue;
|
||||
}
|
||||
console.log(` ${jobs.length} raw cards`);
|
||||
|
||||
for (const j of jobs) {
|
||||
if (seen.has(j.link)) continue;
|
||||
if (NON_ENGLISH_RE.test(j.title) || JUNIOR_TITLE_RE.test(j.title)) continue;
|
||||
if (MS_RE.test(j.title)) continue;
|
||||
|
||||
const titleRuby = RUBY_RE.test(j.title);
|
||||
if (!titleRuby && !config.descCheck) continue;
|
||||
|
||||
// Confirm Ruby in the description (and that the posting is in English).
|
||||
let bodyRuby = false;
|
||||
try {
|
||||
const res = await fetch(`https://www.linkedin.com/jobs/view/${j.id}/`, {
|
||||
headers: { "User-Agent": "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 html = await res.text();
|
||||
const body = (html.match(/<div class="show-more-less-html__markup[^"]*"[^>]*>([\s\S]*?)<\/div>/)?.[1] ?? "")
|
||||
.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ");
|
||||
bodyRuby = RUBY_RE.test(body);
|
||||
await Bun.sleep(350);
|
||||
} catch { /* fall back to the title signal */ }
|
||||
|
||||
if (!titleRuby && !bodyRuby) continue;
|
||||
if (!(await isEnglishContent(j.id))) {
|
||||
console.log(` dropped (non-English): ${j.title}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
seen.add(j.link);
|
||||
hits.push({
|
||||
title: j.title, company: j.company, location: (j as any).location ?? "?",
|
||||
link: j.link, label: config.label,
|
||||
via: titleRuby ? (bodyRuby ? "title+desc" : "title") : "desc",
|
||||
});
|
||||
console.log(` HIT ${existing.has(j.link) ? "(already in CSV)" : "(NEW)"}: ${j.title} | ${j.company}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n=== ${hits.length} RUBY/RAILS HITS ===\n`);
|
||||
for (const h of hits) {
|
||||
console.log(`${existing.has(h.link) ? "[seen] " : "[NEW] "}${h.title} | ${h.company} | ${h.location} | via ${h.via} | ${h.label}`);
|
||||
console.log(` ${h.link}`);
|
||||
}
|
||||
Reference in New Issue
Block a user