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
+104
View File
@@ -0,0 +1,104 @@
import { KEYWORDS } from "../config.ts";
import { JUNIOR_TITLE_RE, JAVA_TITLE_RE, SOFTWARE_TITLE_RE } from "../fetch.ts";
// Supplement for Portuguese job titles not caught by SOFTWARE_TITLE_RE
const PT_SOFT_RE = /\b(desenvolvedor|engenheiro\s+de\s+software|arquiteto|analista\s+de\s+(sistemas|dados)|tech\s+lead|dados|plataforma|ia\b|inteligência\s+artificial)\b/i;
const 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",
"Accept-Language": "en-US,en;q=0.9",
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
};
function clean(raw: string): string {
return raw
.replace(/<[^>]+>/g, "")
.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">")
.replace(/&quot;/g, '"').replace(/&#39;/g, "'")
.replace(/&ndash;/g, "–").replace(/&mdash;/g, "—")
.replace(/\s+/g, " ").trim();
}
interface Job {
id: string;
title: string;
company: string;
location: string;
postedAt: string;
link: string;
}
function parseJobs(html: string): Job[] {
const jobs: Job[] = [];
const idRe = /data-entity-urn="urn:li:jobPosting:(\d+)"/g;
const positions: { id: string; start: number }[] = [];
let m;
while ((m = idRe.exec(html)) !== null) positions.push({ id: m[1], start: m.index });
for (let i = 0; i < positions.length; i++) {
const { id, start } = positions[i];
const card = html.slice(start, Math.min(positions[i + 1]?.start ?? Infinity, start + 3000));
const titleM = card.match(/<h3 class="base-search-card__title">\s*([\s\S]*?)\s*<\/h3>/);
if (!titleM) continue;
const companyM = card.match(/<h4 class="base-search-card__subtitle">[\s\S]{0,300}?<a[^>]*>\s*([\s\S]*?)\s*<\/a>/);
const timeM = card.match(/<time[^>]+datetime="([^"]+)"/);
const locM = card.match(/job-search-card__location[^>]*>\s*([^<]+)/);
const jobLocation = locM ? locM[1].trim() : "";
// Accept Brazil locations, Remote, or empty (unknown)
// Reject jobs clearly anchored to other countries
const loc = jobLocation.toLowerCase();
const isBrazil = loc.includes("brazil") || loc.includes("brasil") || loc === "" || loc.includes("remote");
const isOtherCountry = /switzerland|germany|deutschland|united states|france|uk\b|united kingdom|netherlands|spain|italy/i.test(jobLocation);
if (!isBrazil || isOtherCountry) continue;
jobs.push({
id,
title: clean(titleM[1]),
company: companyM ? clean(companyM[1]) : "",
location: jobLocation || "Remote",
postedAt: timeM ? timeM[1] : "",
link: `https://www.linkedin.com/jobs/view/${id}/`,
});
}
return jobs;
}
const PAGE_SIZE = 25;
const MAX_PAGES = 6;
const baseUrl = `https://www.linkedin.com/jobs/search/?keywords=${encodeURIComponent(KEYWORDS)}&location=Brazil&f_WT=2&f_TPR=r2592000`;
const allJobs: Job[] = [];
const seen = new Set<string>();
for (let page = 0; page < MAX_PAGES; page++) {
if (page > 0) await Bun.sleep(500);
const url = `${baseUrl}&start=${page * PAGE_SIZE}`;
process.stdout.write(` page ${page + 1} … `);
const res = await fetch(url, { headers: HEADERS });
if (!res.ok) { console.error(`failed (${res.status})`); break; }
const html = await res.text();
const jobs = parseJobs(html)
.filter((j) => !JUNIOR_TITLE_RE.test(j.title))
.filter((j) => !JAVA_TITLE_RE.test(j.title))
.filter((j) => SOFTWARE_TITLE_RE.test(j.title) || PT_SOFT_RE.test(j.title));
let newOnPage = 0;
for (const job of jobs) {
if (!seen.has(job.id)) { seen.add(job.id); allJobs.push(job); newOnPage++; }
}
console.log(`${jobs.length} found, ${newOnPage} new`);
if (newOnPage === 0) break;
}
console.log(`\nTotal: ${allJobs.length} vagas remotas no Brasil\n`);
for (const j of allJobs) {
console.log(`${j.title} — ${j.company} (${j.location})`);
console.log(` ${j.link}`);
}