Skip to content

Commit 4ccd8ea

Browse files
committed
refactor: update job scraping logic and enhance configuration options
1 parent 1ef61ab commit 4ccd8ea

5 files changed

Lines changed: 104 additions & 146 deletions

File tree

README.md

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,13 @@ Bot de automacao para buscar vagas no LinkedIn com foco em Brasil remoto e expor
1515
- `index.js`: entrypoint e tratamento de falhas
1616
- `src/app.js`: fluxo principal (coleta e exportacao)
1717
- `src/config.js`: leitura das variaveis de ambiente
18-
- `src/browser.js`: inicializacao do navegador para o Puppeteer
19-
- `src/linkedinScraper.js`: montagem da URL e extracao das vagas
18+
- `src/linkedinScraper.js`: coleta HTTP, paginacao e extracao das vagas
2019
- `src/exporter.js`: geracao de Excel e PDF
2120
- `src/logger.js`: logs padronizados
2221

2322
## Requisitos
2423

2524
- Node.js 22+
26-
- Google Chrome ou Microsoft Edge instalado
2725
- npm
2826

2927
## Instalacao
@@ -41,25 +39,22 @@ Bot de automacao para buscar vagas no LinkedIn com foco em Brasil remoto e expor
4139

4240
Todas sao opcionais.
4341

44-
- `HEADLESS` (padrao: `false`)
4542
- `WAIT_BETWEEN_SEARCHES_MS` (padrao: `5000`)
4643
- `PAGE_TIMEOUT_MS` (padrao: `10000`)
47-
- `VIEWPORT_WIDTH` (padrao: `1280`)
48-
- `VIEWPORT_HEIGHT` (padrao: `800`)
44+
- `MAX_PAGES_PER_KEYWORD` (padrao: `5`)
4945
- `OUTPUT_FILE` (padrao: `vagas_linkedin.xlsx`)
5046
- `PDF_FILE` (padrao: `vagas_linkedin.pdf`)
5147
- `SEARCH_LOCATION` (padrao: `Brasil`)
5248
- `SEARCH_GEO_ID` (padrao: `106057199`)
5349
- `SEARCH_LANGUAGE` (padrao: `pt`)
5450
- `JOB_TYPES` (padrao: `C,F`)
55-
Valores comuns: `C` (PJ), `F` (CLT), `C,F` (ambos)
51+
Valores comuns: `C` (PJ), `F` (CLT), `C,F` (ambos)
5652
- `SEARCH_KEYWORDS` (lista separada por virgula)
57-
- `CHROME_PATH` (caminho do executavel do navegador)
5853

5954
Exemplo no Windows cmd (Brasil remoto, PJ+CLT):
6055

6156
```bat
62-
set SEARCH_GEO_ID=106057199&& set JOB_TYPES=C,F&& set HEADLESS=true&& npm start
57+
set SEARCH_GEO_ID=106057199&& set JOB_TYPES=C,F&& set MAX_PAGES_PER_KEYWORD=5&& npm start
6358
```
6459

6560
Exemplo com palavras-chave personalizadas:
@@ -87,16 +82,11 @@ No PDF, os links sao normalizados para uma versao curta do LinkedIn quando possi
8782

8883
## Troubleshooting
8984

90-
Chrome/Edge nao encontrado:
85+
Poucas ou nenhuma vaga retornada:
9186

92-
- Instale Google Chrome ou Microsoft Edge
93-
- Ou informe `CHROME_PATH` manualmente
94-
95-
Exemplo no Windows cmd:
96-
97-
```bat
98-
set CHROME_PATH=C:\Program Files\Google\Chrome\Application\chrome.exe&& npm start
99-
```
87+
- Aumente `MAX_PAGES_PER_KEYWORD`
88+
- Aumente `WAIT_BETWEEN_SEARCHES_MS` para reduzir bloqueios temporarios
89+
- Reduza a quantidade de keywords por execucao
10090

10191
## Avisos
10292

package.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "vagas-full",
33
"version": "1.0.0",
4-
"description": "Bot automatizado para buscar vagas de tecnologia no LinkedIn em escala global",
4+
"description": "Bot HTTP para buscar vagas no LinkedIn (Brasil remoto) e exportar em Excel e PDF",
55
"main": "index.js",
66
"type": "module",
77
"scripts": {
@@ -14,19 +14,21 @@
1414
"linkedin",
1515
"jobs",
1616
"scraping",
17-
"puppeteer",
17+
"http",
1818
"vagas",
1919
"emprego",
20-
"global",
20+
"brasil",
2121
"tecnologia",
2222
"automation",
23-
"puppeteer-core"
23+
"axios",
24+
"cheerio"
2425
],
2526
"author": "Bene Tesla Dev",
2627
"license": "ISC",
2728
"dependencies": {
29+
"axios": "^1.13.6",
30+
"cheerio": "^1.2.0",
2831
"pdfkit": "^0.18.0",
29-
"puppeteer-core": "^24.22.3",
3032
"xlsx": "^0.18.5"
3133
},
3234
"engines": {

src/app.js

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,17 @@
1-
import { openBrowser } from "./browser.js";
21
import { getConfig } from "./config.js";
32
import { exportToExcel, exportToPDF } from "./exporter.js";
43
import { scrapeLinkedinJobs } from "./linkedinScraper.js";
54
import { logInfo } from "./logger.js";
65

76
export async function run() {
87
const config = getConfig();
9-
const browser = await openBrowser(config);
108

11-
try {
12-
const page = await browser.newPage();
13-
await page.setViewport(config.viewport);
9+
logInfo(`Localizacao da busca: ${config.searchLocation}`);
10+
logInfo(`Total de palavras-chave: ${config.keywords.length}`);
1411

15-
logInfo(`Localizacao da busca: ${config.searchLocation}`);
16-
logInfo(`Total de palavras-chave: ${config.keywords.length}`);
12+
const jobs = await scrapeLinkedinJobs(config);
13+
exportToExcel(jobs, config.outputFile);
14+
await exportToPDF(jobs, config.pdfFile);
1715

18-
const jobs = await scrapeLinkedinJobs(page, config);
19-
exportToExcel(jobs, config.outputFile);
20-
await exportToPDF(jobs, config.pdfFile);
21-
22-
logInfo(`Total de vagas unicas exportadas: ${jobs.length}`);
23-
} finally {
24-
await browser.close();
25-
}
16+
logInfo(`Total de vagas unicas exportadas: ${jobs.length}`);
2617
}

src/config.js

Lines changed: 3 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,5 @@
1-
const DEFAULT_KEYWORDS = [
2-
"UX Designer",
3-
"UI Designer",
4-
"UX UI Designer",
5-
"Product Designer",
6-
"UX Researcher",
7-
"Product Manager",
8-
"Product Owner",
9-
"PO Remoto",
10-
"Product Manager Remoto",
11-
"Gerente de Produto",
12-
"Analista de UX",
13-
"Analista de UI",
14-
"Analista de UX UI",
15-
"Analista de Produto",
16-
"Analista de UX Remoto",
17-
"Analista de UI Remoto",
18-
"Analista de UX UI Remoto",
19-
"Analista de Produto Remoto",
20-
"Frontend Developer",
21-
"Desenvolvedor Frontend",
22-
"Desenvolvedor Front-end",
23-
"Engenheiro Frontend",
24-
"Engenheiro Front-end",
25-
"Frontend Developer Remoto",
26-
"Desenvolvedor Frontend Remoto",
27-
"Desenvolvedor Front-end Remoto",
28-
"Engenheiro Frontend Remoto",
29-
"Engenheiro Front-end Remoto",
30-
"Backend Developer",
31-
"Desenvolvedor Backend",
32-
"Desenvolvedor Back-end",
33-
"Engenheiro Backend",
34-
"Engenheiro Back-end",
35-
"Backend Developer Remoto",
36-
"Desenvolvedor Backend Remoto",
37-
"Node.js Developer Remoto",
38-
"Desenvolvedor Back-end Remoto",
39-
"Engenheiro Backend Remoto",
40-
"Engenheiro Back-end Remoto",
41-
"nodejs Developer",
42-
"Desenvolvedor Node.js",
43-
"Desenvolvedor Node",
44-
"MuleSoft Developer",
45-
"Desenvolvedor MuleSoft",
46-
"MuleSoft Developer Remoto",
47-
"Desenvolvedor MuleSoft Remoto",
48-
"Java Developer",
49-
"Desenvolvedor Java",
50-
"Engenheiro Java",
51-
"Java Developer Remoto",
52-
"Desenvolvedor Java Remoto",
53-
"Engenheiro Java Remoto"
1+
const DEFAULT_KEYWORDS = [
2+
"
543
];
554

565
function parseBoolean(value, fallback) {
@@ -95,6 +44,7 @@ export function getConfig() {
9544
headless: parseBoolean(process.env.HEADLESS, false),
9645
waitBetweenSearchesMs: parseNumber(process.env.WAIT_BETWEEN_SEARCHES_MS, 5000),
9746
pageTimeoutMs: parseNumber(process.env.PAGE_TIMEOUT_MS, 10000),
47+
maxPagesPerKeyword: parseNumber(process.env.MAX_PAGES_PER_KEYWORD, 5),
9848
viewport: {
9949
width: parseNumber(process.env.VIEWPORT_WIDTH, 1280),
10050
height: parseNumber(process.env.VIEWPORT_HEIGHT, 800)

src/linkedinScraper.js

Lines changed: 80 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,62 @@
1+
import axios from "axios";
2+
import * as cheerio from "cheerio";
13
import { logInfo, logWarn } from "./logger.js";
24

3-
function buildSearchUrl(keyword, config) {
4-
const url = new URL("https://www.linkedin.com/jobs/search/");
5+
function buildSearchUrl(keyword, config, start = 0) {
6+
const url = new URL("https://www.linkedin.com/jobs-guest/jobs/api/seeMoreJobPostings/search");
57
url.searchParams.set("keywords", keyword);
68
url.searchParams.set("location", config.searchLocation);
79
url.searchParams.set("geoId", config.searchGeoId);
810
url.searchParams.set("lang", config.searchLanguage);
9-
// f_WT=2 = remoto; f_WT=3 = híbrido; f_WT=1 = presencial
11+
// f_WT=2 = remoto; f_WT=3 = hibrido; f_WT=1 = presencial
1012
url.searchParams.set("f_WT", "2");
1113
if (config.jobTypes) {
1214
url.searchParams.set("f_JT", config.jobTypes);
1315
}
16+
url.searchParams.set("start", String(start));
1417
return url.toString();
1518
}
1619

20+
async function fetchJobsChunk(keyword, config, start) {
21+
const url = buildSearchUrl(keyword, config, start);
22+
23+
const response = await axios.get(url, {
24+
timeout: config.pageTimeoutMs,
25+
headers: {
26+
"user-agent":
27+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
28+
"accept-language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7"
29+
}
30+
});
31+
32+
const $ = cheerio.load(response.data);
33+
const jobs = [];
34+
35+
$(".base-card, .job-search-card").each((_, card) => {
36+
const node = $(card);
37+
const titulo =
38+
node.find(".base-search-card__title").text().trim() ||
39+
node.find("h3").first().text().trim() ||
40+
"";
41+
42+
const empresa =
43+
node.find(".base-search-card__subtitle").text().trim() ||
44+
node.find("h4").first().text().trim() ||
45+
"";
46+
47+
const local = node.find(".job-search-card__location").text().trim() || "";
48+
49+
const link =
50+
node.find("a.base-card__full-link").attr("href") ||
51+
node.find("a[href*='/jobs/view/']").attr("href") ||
52+
"";
53+
54+
jobs.push({ titulo, empresa, local, link });
55+
});
56+
57+
return jobs;
58+
}
59+
1760
function normalizeJob(job) {
1861
return {
1962
titulo: job.titulo?.trim() || "",
@@ -37,66 +80,48 @@ function dedupeJobs(jobs) {
3780
return [...unique.values()];
3881
}
3982

40-
export async function scrapeLinkedinJobs(page, config) {
83+
export async function scrapeLinkedinJobs(config) {
4184
const allJobs = [];
85+
const maxPagesPerKeyword = config.maxPagesPerKeyword || 5;
86+
const pageStep = 25;
4287

4388
for (const keyword of config.keywords) {
4489
logInfo(`Buscando vagas para: ${keyword}`);
4590

46-
const url = buildSearchUrl(keyword, config);
47-
await page.goto(url, { waitUntil: "networkidle2" });
48-
await new Promise((resolve) => setTimeout(resolve, config.waitBetweenSearchesMs));
91+
let jobsCountForKeyword = 0;
92+
93+
for (let pageIndex = 0; pageIndex < maxPagesPerKeyword; pageIndex++) {
94+
const start = pageIndex * pageStep;
95+
let jobs = [];
96+
97+
try {
98+
jobs = await fetchJobsChunk(keyword, config, start);
99+
} catch {
100+
logWarn(`Falha HTTP na busca para: ${keyword} (start=${start})`);
101+
}
102+
103+
if (jobs.length === 0) {
104+
if (pageIndex === 0) {
105+
logWarn(`Elementos de vaga nao encontrados para: ${keyword}`);
106+
}
107+
break;
108+
}
109+
110+
const normalized = jobs.map(normalizeJob);
111+
112+
allJobs.push(
113+
...normalized.map((job) => ({
114+
palavra: keyword,
115+
...job
116+
}))
117+
);
118+
119+
jobsCountForKeyword += jobs.length;
49120

50-
try {
51-
await page.waitForSelector(".job-search-card", { timeout: config.pageTimeoutMs });
52-
} catch {
53-
logWarn(`Elementos de vaga nao encontrados para: ${keyword}`);
121+
await new Promise((resolve) => setTimeout(resolve, config.waitBetweenSearchesMs));
54122
}
55123

56-
const jobs = await page
57-
.$$eval(".job-search-card", (items) =>
58-
items.map((jobCard) => {
59-
const titulo =
60-
jobCard.querySelector("h3.base-search-card__title")?.innerText?.trim() ||
61-
jobCard
62-
.querySelector("a[data-tracking-control-name='public_jobs_jserp-result_search-card']")
63-
?.innerText?.trim() ||
64-
"";
65-
66-
const empresa =
67-
jobCard.querySelector("h4.base-search-card__subtitle")?.innerText?.trim() ||
68-
jobCard
69-
.querySelector(
70-
"a[data-tracking-control-name='public_jobs_jserp-result_job-search-card-subtitle']"
71-
)
72-
?.innerText?.trim() ||
73-
"";
74-
75-
const local =
76-
jobCard.querySelector("span.job-search-card__location")?.innerText?.trim() || "";
77-
78-
const link =
79-
jobCard.querySelector("h3.base-search-card__title a")?.href ||
80-
jobCard.querySelector(
81-
"a[data-tracking-control-name='public_jobs_jserp-result_search-card']"
82-
)?.href ||
83-
"";
84-
85-
return { titulo, empresa, local, link };
86-
})
87-
)
88-
.catch(() => []);
89-
90-
const normalized = jobs.map(normalizeJob);
91-
92-
allJobs.push(
93-
...normalized.map((job) => ({
94-
palavra: keyword,
95-
...job
96-
}))
97-
);
98-
99-
logInfo(`${jobs.length} vagas encontradas para: ${keyword}`);
124+
logInfo(`${jobsCountForKeyword} vagas encontradas para: ${keyword}`);
100125
}
101126

102127
return dedupeJobs(allJobs);

0 commit comments

Comments
 (0)