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:
+37
-53
@@ -1,40 +1,28 @@
|
||||
#!/usr/bin/env bun
|
||||
import { isEnglishContent, NON_ENGLISH_RE, JUNIOR_TITLE_RE } from "../fetch.ts";
|
||||
|
||||
const CSV = "./jobs.csv";
|
||||
const existingLinks = new Set<string>();
|
||||
try {
|
||||
const text = await Bun.file(CSV).text();
|
||||
for (const line of text.split("\n").slice(1)) {
|
||||
const url = line.match(/"(https?:\/\/[^"]+)"/)?.[1];
|
||||
if (url) existingLinks.add(url);
|
||||
}
|
||||
} catch {}
|
||||
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 searches = [
|
||||
{ label: "United States Remote (Ruby)", location: "United States", workType: "2" },
|
||||
];
|
||||
|
||||
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; postedAt: string }
|
||||
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(location: string, workType: string, start: number): Promise<Job[]> {
|
||||
async function fetchPage(start: number): Promise<Job[]> {
|
||||
const params = new URLSearchParams({
|
||||
keywords: RUBY_KEYWORDS,
|
||||
location,
|
||||
location: "Switzerland",
|
||||
f_TPR: "r2592000",
|
||||
f_WT: workType,
|
||||
f_WT: "2",
|
||||
start: String(start),
|
||||
});
|
||||
const url = `https://www.linkedin.com/jobs/search/?${params}`;
|
||||
@@ -42,7 +30,6 @@ async function fetchPage(location: string, workType: string, start: number): Pro
|
||||
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 = /<div[^>]*class="[^"]*base-search-card[^"]*"[^>]*data-entity-urn="urn:li:jobPosting:(\d+)"[^>]*>([\s\S]*?)(?=<div[^>]*class="[^"]*base-search-card[^"]*"[^>]*data-entity-urn|<\/ul>)/g;
|
||||
let m;
|
||||
@@ -52,50 +39,47 @@ async function fetchPage(location: string, workType: string, start: number): Pro
|
||||
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]*?<a[^>]*>\s*([\s\S]*?)\s*<\/a>/);
|
||||
const locationM = chunk.match(/class="[^"]*job-search-card__location[^"]*"[^>]*>\s*([\s\S]*?)\s*<\//);
|
||||
const postedM = chunk.match(/datetime="([^"]+)"/);
|
||||
|
||||
const title = titleM ? unescape(titleM[1].replace(/<[^>]+>/g, "")) : "";
|
||||
const company = companyM ? unescape(companyM[1].replace(/<[^>]+>/g, "")) : "";
|
||||
const location = locationM ? unescape(locationM[1]) : "";
|
||||
const postedAt = postedM ? postedM[1] : "";
|
||||
|
||||
if (!title || !id) continue;
|
||||
jobs.push({ id, title, company, location, link: `https://www.linkedin.com/jobs/view/${id}/`, postedAt });
|
||||
jobs.push({ id, title, company, location, link: `https://www.linkedin.com/jobs/view/${id}/` });
|
||||
}
|
||||
return jobs;
|
||||
}
|
||||
|
||||
const newJobs: Array<Job & { search: string }> = [];
|
||||
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<string>();
|
||||
const results: (Job & { note: string })[] = [];
|
||||
|
||||
for (const s of searches) {
|
||||
console.log(`\n[${s.label}]`);
|
||||
let totalFound = 0, added = 0;
|
||||
|
||||
for (let page = 0; page < 4; page++) {
|
||||
const jobs = await fetchPage(s.location, s.workType, page * 25);
|
||||
if (jobs.length === 0) { console.log(` page ${page + 1} empty, stopping`); break; }
|
||||
console.log(` page ${page + 1} … ${jobs.length} cards`);
|
||||
totalFound += jobs.length;
|
||||
|
||||
for (const job of jobs) {
|
||||
if (existingLinks.has(job.link) || seenIds.has(job.id)) continue;
|
||||
if (NON_ENGLISH_RE.test(job.title) || JUNIOR_TITLE_RE.test(job.title)) continue;
|
||||
if (!RUBY_RE.test(job.title)) continue;
|
||||
|
||||
seenIds.add(job.id);
|
||||
process.stdout.write(` checking: ${job.title} @ ${job.company} (${job.location}) … `);
|
||||
const english = await isEnglishContent(job.id);
|
||||
if (!english) { console.log("dropped (non-English)"); continue; }
|
||||
console.log("ok");
|
||||
newJobs.push({ ...job, search: s.label });
|
||||
added++;
|
||||
}
|
||||
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);
|
||||
}
|
||||
console.log(` ${totalFound} total, ${added} new`);
|
||||
await Bun.sleep(300);
|
||||
}
|
||||
|
||||
console.log(`\n=== ${newJobs.length} Ruby remote jobs ===\n`);
|
||||
for (const j of newJobs) {
|
||||
console.log(`${j.title} | ${j.company} | ${j.location} | ${j.link}`);
|
||||
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}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user