162 lines
5.4 KiB
JavaScript
162 lines
5.4 KiB
JavaScript
// Description:
|
|||
|
|
// Puppeteer scraper that fetches Adzuna job details using a headless browser
|
||
|
|
|
||
|
|
// This should be called something like this: `node adzuna_scraper.js https://www.adzuna.com/details/5694360484`
|
||
|
|
// The script uses puppeteer-core with chromium binary to scrape the page
|
||
|
|
// I included the html of a page for example in the sidecars/page_example.html file
|
||
|
|
|
||
|
|
import puppeteer from 'puppeteer-core';
|
||
|
|
import chromium from '@sparticuz/chromium';
|
||
|
|
|
||
|
|
// Get URL from command line arguments
|
||
|
|
const url = process.argv[2];
|
||
|
|
|
||
|
|
if (!url) {
|
||
|
|
console.error('Error: Please provide a URL as an argument');
|
||
|
|
console.error('Usage: node adzuna_scraper.js <url>');
|
||
|
|
process.exit(1);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function scrapeAdzunaJob(url) {
|
||
|
|
let browser;
|
||
|
|
|
||
|
|
try {
|
||
|
|
console.log(`Launching browser...`);
|
||
|
|
|
||
|
|
// Launch browser with chromium binary
|
||
|
|
browser = await puppeteer.launch({
|
||
|
|
executablePath: await chromium.executablePath(),
|
||
|
|
headless: chromium.headless,
|
||
|
|
args: [
|
||
|
|
...chromium.args,
|
||
|
|
'--no-sandbox',
|
||
|
|
'--disable-setuid-sandbox',
|
||
|
|
'--disable-dev-shm-usage',
|
||
|
|
'--disable-accelerated-2d-canvas',
|
||
|
|
'--disable-gpu',
|
||
|
|
'--window-size=1920x1080'
|
||
|
|
]
|
||
|
|
});
|
||
|
|
|
||
|
|
const page = await browser.newPage();
|
||
|
|
|
||
|
|
// Set user agent to avoid being blocked
|
||
|
|
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
|
||
|
|
|
||
|
|
// Navigate to the URL
|
||
|
|
console.log(`Navigating to ${url}...`);
|
||
|
|
await page.goto(url, {
|
||
|
|
waitUntil: 'networkidle2',
|
||
|
|
timeout: 30000
|
||
|
|
});
|
||
|
|
|
||
|
|
// Wait a bit for dynamic content to load
|
||
|
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||
|
|
|
||
|
|
// Extract the job description content
|
||
|
|
console.log('Extracting job description...');
|
||
|
|
|
||
|
|
let jobDescription = null;
|
||
|
|
|
||
|
|
// First, check if the page loaded correctly by getting the HTML content
|
||
|
|
const bodyHTML = await page.content();
|
||
|
|
|
||
|
|
// If page is very small, it might be an error page
|
||
|
|
if (bodyHTML.length < 1000) {
|
||
|
|
console.log('Warning: Page content is very small, might be blocked');
|
||
|
|
console.log('First 500 chars:', bodyHTML.substring(0, 500));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Try multiple selectors for the job description
|
||
|
|
const descriptionSelectors = [
|
||
|
|
'section.adp-body',
|
||
|
|
'.ui-foreign-click-description section',
|
||
|
|
'div[class*="description"] section',
|
||
|
|
'section[class*="body"]'
|
||
|
|
];
|
||
|
|
|
||
|
|
for (const selector of descriptionSelectors) {
|
||
|
|
const element = await page.$(selector);
|
||
|
|
if (element) {
|
||
|
|
jobDescription = await page.evaluate(el => {
|
||
|
|
return {
|
||
|
|
html: el.innerHTML,
|
||
|
|
text: el.textContent.trim()
|
||
|
|
};
|
||
|
|
}, element);
|
||
|
|
console.log(`Found job description with selector: ${selector}`);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// If not found with puppeteer, try regex extraction as fallback
|
||
|
|
if (!jobDescription) {
|
||
|
|
console.log('Trying fallback regex extraction...');
|
||
|
|
const patterns = [
|
||
|
|
/<section\s+class="adp-body[^"]*"[^>]*>([\s\S]*?)<\/section>/i,
|
||
|
|
/<section\s+class="[^"]*adp-body[^"]*"[^>]*>([\s\S]*?)<\/section>/i
|
||
|
|
];
|
||
|
|
|
||
|
|
for (const pattern of patterns) {
|
||
|
|
const match = bodyHTML.match(pattern);
|
||
|
|
if (match && match[1]) {
|
||
|
|
const htmlContent = match[1].trim();
|
||
|
|
const textContent = htmlContent
|
||
|
|
.replace(/<br\s*\/?>/gi, '\n')
|
||
|
|
.replace(/<\/?(p|div|li|ul|ol|h\d)[^>]*>/gi, '\n')
|
||
|
|
.replace(/<[^>]+>/g, '')
|
||
|
|
.replace(/&/g, '&')
|
||
|
|
.replace(/</g, '<')
|
||
|
|
.replace(/>/g, '>')
|
||
|
|
.replace(/"/g, '"')
|
||
|
|
.replace(/'/g, "'")
|
||
|
|
.replace(/\n\s*\n\s*\n/g, '\n\n')
|
||
|
|
.trim();
|
||
|
|
|
||
|
|
jobDescription = {
|
||
|
|
html: htmlContent,
|
||
|
|
text: textContent
|
||
|
|
};
|
||
|
|
console.log('Found job description with regex fallback');
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!jobDescription) {
|
||
|
|
throw new Error('Could not find job description on the page');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Output the result as JSON
|
||
|
|
const result = {
|
||
|
|
url: url,
|
||
|
|
jobDescription: jobDescription,
|
||
|
|
timestamp: new Date().toISOString()
|
||
|
|
};
|
||
|
|
|
||
|
|
console.log('\n=== RESULT ===');
|
||
|
|
console.log(JSON.stringify(result, null, 2));
|
||
|
|
|
||
|
|
return result;
|
||
|
|
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error scraping job:', error.message);
|
||
|
|
throw error;
|
||
|
|
} finally {
|
||
|
|
if (browser) {
|
||
|
|
await browser.close();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Run the scraper
|
||
|
|
scrapeAdzunaJob(url)
|
||
|
|
.then(() => {
|
||
|
|
console.log('\nScraping completed successfully');
|
||
|
|
process.exit(0);
|
||
|
|
})
|
||
|
|
.catch((error) => {
|
||
|
|
console.error('\nScraping failed:', error);
|
||
|
|
process.exit(1);
|
||
|
|
});
|