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(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") .replace(/"/g, '"').replace(/'/g, "'") .replace(/–/g, "–").replace(/—/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(/

\s*([\s\S]*?)\s*<\/h3>/); if (!titleM) continue; const companyM = card.match(/

[\s\S]{0,300}?]*>\s*([\s\S]*?)\s*<\/a>/); const timeM = card.match(/]+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 { 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(); 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}`); }