Files
linkme/scripts/brazil-remote-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

105 lines
3.8 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 { 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}`);
}