#!/usr/bin/env bun import { 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", ].join(" OR "); const RUBY_RE = /\bruby\b|\brails\b/i; const DE_FR_RE = /\b(wir|sind|für|mit|und|oder|eine|einen|notre|nous|vous|votre|pour|avec|dans|des)\b/gi; interface Job { id: string; title: string; company: string; location: string; link: string } function unescape(s: string) { return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") .replace(/'/g, "'").replace(/"/g, '"').replace(/\s+/g, " ").trim(); } async function fetchPage(start: number): Promise { const params = new URLSearchParams({ keywords: RUBY_KEYWORDS, location: "Switzerland", f_TPR: "r2592000", f_WT: "2", start: String(start), }); const url = `https://www.linkedin.com/jobs/search/?${params}`; const res = await fetch(url, { 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 jobs: Job[] = []; const cardRe = /]*class="[^"]*base-search-card[^"]*"[^>]*data-entity-urn="urn:li:jobPosting:(\d+)"[^>]*>([\s\S]*?)(?=]*class="[^"]*base-search-card[^"]*"[^>]*data-entity-urn|<\/ul>)/g; let m; while ((m = cardRe.exec(html)) !== null) { const id = m[1]; const chunk = m[2]; const titleM = chunk.match(/class="[^"]*base-search-card__title[^"]*"[^>]*>\s*([\s\S]*?)\s*<\/h3>/); const companyM = chunk.match(/class="[^"]*base-search-card__subtitle[^"]*"[\s\S]*?]*>\s*([\s\S]*?)\s*<\/a>/); const locationM = chunk.match(/class="[^"]*job-search-card__location[^"]*"[^>]*>\s*([\s\S]*?)\s*<\//); const title = titleM ? unescape(titleM[1].replace(/<[^>]+>/g, "")) : ""; const company = companyM ? unescape(companyM[1].replace(/<[^>]+>/g, "")) : ""; const location = locationM ? unescape(locationM[1]) : ""; if (!title || !id) continue; jobs.push({ id, title, company, location, link: `https://www.linkedin.com/jobs/view/${id}/` }); } return jobs; } async function checkDescription(id: string): Promise<{ hasRuby: boolean; isEnglish: boolean }> { const res = await fetch(`https://www.linkedin.com/jobs/view/${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 deMatches = (html.match(DE_FR_RE) || []).length; return { hasRuby: RUBY_RE.test(html), isEnglish: deMatches < 6 }; } const seenIds = new Set(); const results: (Job & { note: string })[] = []; for (let page = 0; page < 4; page++) { const jobs = await fetchPage(page * 25); if (!jobs.length) break; for (const job of jobs) { if (seenIds.has(job.id)) continue; if (NON_ENGLISH_RE.test(job.title) || JUNIOR_TITLE_RE.test(job.title)) continue; seenIds.add(job.id); process.stdout.write(` checking: ${job.title} @ ${job.company} … `); const { hasRuby, isEnglish } = await checkDescription(job.id); if (!isEnglish) { console.log("non-English"); continue; } if (!hasRuby) { console.log("no Ruby"); continue; } console.log("RUBY!"); results.push({ ...job, note: RUBY_RE.test(job.title) ? "title+desc" : "desc only" }); await Bun.sleep(200); } await Bun.sleep(300); } console.log(`\n=== ${results.length} Ruby remote jobs (Switzerland) ===\n`); for (const j of results) { console.log(`[${j.note}] ${j.title} | ${j.company} | ${j.location}`); console.log(` ${j.link}`); }