Files
linkme/scripts/brazil-techlead-temp.ts
rodado b4957025b1 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.
2026-08-27 09:03:05 +02:00

129 lines
4.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { JUNIOR_TITLE_RE, JAVA_TITLE_RE } from "../fetch.ts";
const TECH_LEAD_KEYWORDS = [
"Tech Lead", "Technical Lead", "Lead Engineer", "Lead Software Engineer",
"Lead Developer", "Staff Engineer", "Principal Engineer", "Engineering Manager",
"Lead Fullstack", "Lead Backend", "Lead Platform",
].join(" OR ");
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() : "";
const loc = jobLocation.toLowerCase();
const isBrazil = loc.includes("brazil") || loc.includes("brasil") || loc === "" || loc.includes("remote");
const isOtherCountry = /switzerland|germany|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 EN_RE = /\b(english|inglês|inglés)\b/i;
const ES_RE = /\b(spanish|espanhol|español)\b/i;
async function requiresEnglishAndSpanish(id: string): Promise<boolean> {
await Bun.sleep(400);
try {
const res = await fetch(`https://www.linkedin.com/jobs/view/${id}/`, { headers: HEADERS });
const html = await res.text();
const m = html.match(/class="show-more-less-html__markup[^"]*">([\s\S]{0,8000}?)<\/div>/);
const text = m ? m[1].replace(/<[^>]+>/g, " ") : html.replace(/<[^>]+>/g, " ");
return EN_RE.test(text) && ES_RE.test(text);
} catch {
return false;
}
}
const PAGE_SIZE = 25;
const MAX_PAGES = 6;
const baseUrl = `https://www.linkedin.com/jobs/search/?keywords=${encodeURIComponent(TECH_LEAD_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));
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(`\n${allJobs.length} vagas Tech Lead/Lead. Checando descrições por EN+ES…\n`);
const matches: Job[] = [];
for (const job of allJobs) {
process.stdout.write(` checking ${job.id}… `);
const ok = await requiresEnglishAndSpanish(job.id);
console.log(ok ? "✓ EN+ES" : "—");
if (ok) matches.push(job);
}
console.log(`\n✓ ${matches.length} vagas com inglês + espanhol\n`);
for (const j of matches) {
console.log(`${j.title} — ${j.company} (${j.location})`);
console.log(` ${j.link}`);
}