- Added new job analysis result repository and database migration system - Expanded job creation to support Glassdoor as a new provider alongside LinkedIn - Added CV analysis service launch configuration in VS Code - Updated Makefile with new commands for scraper service and migrations - Improved environment loading in cron analyzer to support local development - Added error handling for unsupported job providers - Integrated job analysis results
201 lines
6.1 KiB
TypeScript
201 lines
6.1 KiB
TypeScript
import { Page } from "puppeteer-core";
|
|
import { randomDelay } from "../utils/browser";
|
|
|
|
export interface JobListing {
|
|
title: string;
|
|
company: string;
|
|
location: string;
|
|
description: string;
|
|
skills: string[];
|
|
jobLink: string;
|
|
companyLink: string;
|
|
age: string;
|
|
jobId: string;
|
|
provider: number;
|
|
}
|
|
|
|
export class BrowserService {
|
|
private page: Page;
|
|
|
|
constructor(page: Page) {
|
|
this.page = page;
|
|
}
|
|
|
|
async searchJobs(keywords: string, location: string) {
|
|
try {
|
|
// Wait for search form to load
|
|
const keywordInput = await this.page.waitForSelector(
|
|
"#searchBar-jobTitle, input[name='sc.keyword'], input[placeholder*='Job title']",
|
|
{ timeout: 10000 }
|
|
);
|
|
|
|
if (keywordInput) {
|
|
await keywordInput.click({ clickCount: 3 });
|
|
await keywordInput.type(keywords, { delay: 130 });
|
|
await randomDelay(1000, 2000);
|
|
}
|
|
|
|
const locationInput = await this.page.waitForSelector(
|
|
"#searchBar-location, input[name='locKeyword'], input[placeholder*='Location']",
|
|
{ timeout: 5000 }
|
|
);
|
|
|
|
if (locationInput) {
|
|
await locationInput.click({ clickCount: 3 });
|
|
await locationInput.type(location, { delay: 130 });
|
|
await randomDelay(1000, 2000);
|
|
await locationInput.press("Enter");
|
|
|
|
// Wait for search results to load
|
|
await this.page.waitForNavigation({
|
|
waitUntil: "networkidle2",
|
|
timeout: 30000,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.log(
|
|
"Search form not found, attempting to extract jobs from current page..."
|
|
);
|
|
}
|
|
}
|
|
|
|
async extractJobListings(): Promise<JobListing[]> {
|
|
// Wait for job listings to appear
|
|
try {
|
|
await this.page.waitForSelector(
|
|
'li[data-test="jobListing"], .JobsList_jobListItem__wjTHv, [data-jobid]',
|
|
{ timeout: 10000 }
|
|
);
|
|
} catch (error) {
|
|
console.log("No job listings found on the page");
|
|
return [];
|
|
}
|
|
|
|
// Extract job data using page.evaluate
|
|
const jobs = await this.page.evaluate(() => {
|
|
// Multiple selectors to handle different Glassdoor layouts
|
|
const jobElements = document.querySelectorAll(`
|
|
li[data-test="jobListing"],
|
|
.JobsList_jobListItem__wjTHv,
|
|
li[data-jobid],
|
|
.react-job-listing
|
|
`);
|
|
|
|
const jobs: any[] = [];
|
|
|
|
jobElements.forEach((element, index) => {
|
|
try {
|
|
// Extract job ID
|
|
const jobId =
|
|
element.getAttribute("data-jobid") ||
|
|
element.getAttribute("data-brandviews")?.match(/jlid=(\d+)/)?.[1] ||
|
|
`job-${index + 1}`;
|
|
|
|
// Extract job title and link
|
|
const titleElement = element.querySelector(`
|
|
a[data-test="job-title"],
|
|
.JobCard_jobTitle__GLyJ1,
|
|
.jobLink,
|
|
a[id*="job-title"]
|
|
`) as HTMLAnchorElement;
|
|
|
|
// Extract company name and link
|
|
const companyElement = element.querySelector(`
|
|
.EmployerProfile_compactEmployerName__9MGcV,
|
|
[data-test="employer-name"] a,
|
|
.employerName,
|
|
.EmployerProfile_employerNameContainer__ptolz span
|
|
`) as HTMLElement;
|
|
|
|
// Extract location
|
|
const locationElement = element.querySelector(`
|
|
.JobCard_location__Ds1fM,
|
|
[data-test="emp-location"],
|
|
[data-test="job-location"],
|
|
.location,
|
|
[id*="job-location"]
|
|
`) as HTMLElement;
|
|
|
|
// Extract job description
|
|
const descriptionElement = element.querySelector(`
|
|
.JobCard_jobDescriptionSnippet__l1tnl,
|
|
[data-test="descSnippet"],
|
|
.jobDescriptionSnippet
|
|
`) as HTMLElement;
|
|
|
|
// Extract job age
|
|
const ageElement = element.querySelector(`
|
|
.JobCard_listingAge__jJsuc,
|
|
[data-test="job-age"],
|
|
.listingAge
|
|
`) as HTMLElement;
|
|
|
|
// Extract skills if available
|
|
const skillsText = descriptionElement?.textContent || "";
|
|
const skillsMatch = skillsText.match(
|
|
/(?:Vaardigheden|Skills):\s*([^.]+)/i
|
|
);
|
|
const skills = skillsMatch
|
|
? skillsMatch[1]
|
|
.split(",")
|
|
.map((skill) => skill.trim())
|
|
.filter((skill) => skill.length > 0)
|
|
: [];
|
|
|
|
if (titleElement && companyElement) {
|
|
const title = titleElement.textContent?.trim() || "";
|
|
const company = companyElement.textContent?.trim() || "";
|
|
const location = locationElement?.textContent?.trim() || "";
|
|
const description = descriptionElement?.textContent?.trim() || "";
|
|
const age = ageElement?.textContent?.trim() || "";
|
|
|
|
// Build full URLs for links
|
|
const jobLink = titleElement.href
|
|
? titleElement.href.startsWith("http")
|
|
? titleElement.href
|
|
: `https://www.glassdoor.com${titleElement.href}`
|
|
: "";
|
|
|
|
const companyLink =
|
|
companyElement.tagName === "A"
|
|
? (companyElement as HTMLAnchorElement).href?.startsWith("http")
|
|
? (companyElement as HTMLAnchorElement).href
|
|
: `https://www.glassdoor.com${
|
|
(companyElement as HTMLAnchorElement).href
|
|
}`
|
|
: "";
|
|
|
|
jobs.push({
|
|
title,
|
|
company,
|
|
location,
|
|
description: description.substring(0, 500), // Limit description length
|
|
skills,
|
|
jobLink,
|
|
companyLink,
|
|
age,
|
|
jobId,
|
|
provider: 2,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error extracting job ${index}:`, error);
|
|
}
|
|
});
|
|
|
|
return jobs;
|
|
});
|
|
|
|
console.log(`Found ${jobs.length} jobs on Glassdoor`);
|
|
jobs.forEach((job, index) => {
|
|
console.log(
|
|
`${index + 1}. ${job.title} at ${job.company} - ${job.location}`
|
|
);
|
|
if (job.skills.length > 0) {
|
|
console.log(` Skills: ${job.skills.join(", ")}`);
|
|
}
|
|
});
|
|
|
|
return jobs;
|
|
}
|
|
}
|