Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
21 changes: 14 additions & 7 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@
"name": "backend",
"version": "1.0.0",
"description": "Backend para scraping e API de vagas do LinkedIn",
"main": "index.js",
"main": "dist/index.js",
"type": "module",
"scripts": {
"start": "node src/server.js",
"dev": "node src/server.js",
"scraper": "node index.js",
"scraper:watch": "nodemon index.js",
"api": "node src/server.js",
"build": "tsc -p tsconfig.json",
"start": "node dist/src/server.js",
"dev": "tsx src/server.ts",
"scraper": "tsx index.ts",
"scraper:watch": "tsx watch index.ts",
"api": "tsx src/server.ts",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest --watch",
"validate": "npm test"
"validate": "npm test && npm run build"
},
"keywords": [
"linkedin",
Expand Down Expand Up @@ -43,9 +44,15 @@
"node": ">=22.0.0"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.5",
"@types/node": "^22.15.35",
"@types/pdfkit": "^0.17.3",
"@vitest/coverage-v8": "^3.2.4",
"nodemon": "^3.1.14",
"supertest": "^7.1.4",
"tsx": "^4.20.6",
"typescript": "^5.8.3",
"vitest": "^3.2.4"
}
}
File renamed without changes.
103 changes: 103 additions & 0 deletions backend/src/adapters/empregaCampinas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import axios from "axios";
import * as cheerio from "cheerio";

function buildEmpregaCampinasSearchUrl(keyword) {
const url = new URL("https://www.empregacampinas.com.br/");
url.searchParams.set("s", keyword);
return url.toString();
}

function buildEmpregaCampinasRssUrl(keyword) {
const url = new URL("https://www.empregacampinas.com.br/feed/");
url.searchParams.set("s", keyword);
return url.toString();
}

function normalizeText(value) {
return String(value || "").replace(/\s+/g, " ").trim();
}

function parseRssItems(xml, keyword) {
const $ = cheerio.load(xml, { xmlMode: true });
const jobs = [];

$("item").each((_, node) => {
const item = $(node);
const titulo = normalizeText(item.find("title").first().text());
const link = normalizeText(item.find("link").first().text());
const description = normalizeText(item.find("description").first().text());

jobs.push({
source: "Emprega Campinas",
keyword,
titulo,
empresa: "",
local: "Campinas/SP",
link,
descricao: description,
dataPublicacao: normalizeText(item.find("pubDate").first().text()),
});
});

return jobs.filter((job) => job.titulo || job.link);
}

function parseHtmlItems(html, keyword) {
const $ = cheerio.load(html);
const jobs = [];

$("article, .post").each((_, node) => {
const item = $(node);
const link =
item.find("h2 a, .entry-title a").first().attr("href") ||
item.find("a").first().attr("href") ||
"";

const titulo =
normalizeText(item.find("h2 a, .entry-title a").first().text()) ||
normalizeText(item.find("h2, .entry-title").first().text());

jobs.push({
source: "Emprega Campinas",
keyword,
titulo,
empresa: "",
local: "Campinas/SP",
link: normalizeText(link),
});
});

return jobs.filter((job) => job.titulo || job.link);
}

export const empregaCampinasAdapter = {
sourceName: "Emprega Campinas",

async search(keyword, config) {
const timeout = config.pageTimeoutMs || 15000;
const headers = {
"user-agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
"accept-language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7",
};

try {
const rssUrl = buildEmpregaCampinasRssUrl(keyword);
const rssResponse = await axios.get(rssUrl, { timeout, headers });
const fromRss = parseRssItems(rssResponse.data, keyword);
if (fromRss.length > 0) {
return fromRss;
}
} catch {
// fallback para HTML
}

try {
const htmlUrl = buildEmpregaCampinasSearchUrl(keyword);
const htmlResponse = await axios.get(htmlUrl, { timeout, headers });
return parseHtmlItems(htmlResponse.data, keyword);
} catch {
return [];
}
},
};
123 changes: 123 additions & 0 deletions backend/src/adapters/glassdoor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import axios from "axios";
import * as cheerio from "cheerio";

function buildGlassdoorUrl(keyword, location) {
const url = new URL("https://www.glassdoor.com.br/Job/jobs.htm");
url.searchParams.set("sc.keyword", keyword);
if (location) {
url.searchParams.set("locT", "C");
url.searchParams.set("locId", location);
}
return url.toString();
}

function normalizeText(value) {
return String(value || "").replace(/\s+/g, " ").trim();
}

function parseJsonLdJobs(html, keyword) {
const $ = cheerio.load(html);
const jobs = [];

$("script[type='application/ld+json']").each((_, node) => {
const rawJson = $(node).contents().text();

try {
const parsed = JSON.parse(rawJson);
const items = Array.isArray(parsed) ? parsed : [parsed];

for (const item of items) {
if (item?.['@type'] !== "JobPosting") {
continue;
}

jobs.push({
source: "Glassdoor",
keyword,
titulo: normalizeText(item.title),
empresa: normalizeText(item.hiringOrganization?.name),
local: normalizeText(
item.jobLocation?.address?.addressLocality ||
item.jobLocation?.address?.addressRegion,
),
link: normalizeText(item.url),
dataPublicacao: normalizeText(item.datePosted),
});
}
} catch {
// ignora JSON inválido
}
});

return jobs;
}

function parseHtmlJobs(html, keyword) {
const $ = cheerio.load(html);
const jobs = [];

$("li[data-test='jobListing'], article").each((_, node) => {
const item = $(node);

const title = normalizeText(
item.find("a[data-test='job-link'], a.jobLink, a").first().text(),
);

const company = normalizeText(
item.find("[data-test='employer-name'], .EmployerProfile_compactEmployerName__LE242").first().text(),
);

const location = normalizeText(
item.find("[data-test='emp-location'], [data-test='location'], .JobCard_location__Ds1fM").first().text(),
);

const href =
item.find("a[data-test='job-link'], a.jobLink, a").first().attr("href") || "";

const link = href.startsWith("http")
? href
: href
? `https://www.glassdoor.com.br${href}`
: "";

jobs.push({
source: "Glassdoor",
keyword,
titulo: title,
empresa: company,
local: location,
link: normalizeText(link),
});
});

return jobs.filter((job) => job.titulo || job.link);
}

export const glassdoorAdapter = {
sourceName: "Glassdoor",

async search(keyword, config) {
const timeout = config.pageTimeoutMs || 15000;
const url = buildGlassdoorUrl(keyword, config.searchLocation || "");

try {
const response = await axios.get(url, {
timeout,
headers: {
"user-agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
"accept-language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7",
},
});

const fromJsonLd = parseJsonLdJobs(response.data, keyword);
if (fromJsonLd.length > 0) {
return fromJsonLd;
}

return parseHtmlJobs(response.data, keyword);
} catch {
return [];
}
},
};
Loading
Loading