使用Puppeteer实现Glassdoor求职页面分页采集的问题咨询
实现方案
你可以通过两种方式实现分页采集,以下是可直接复用的改造逻辑:
方案1:URL拼接分页(最简单稳定)
Glassdoor的列表页URL末尾_IPx.htm中的x就是当前页码,你只要循环修改这个参数即可,步骤如下:
- 先把登录逻辑从
scrapeListings中抽离,避免每次爬新页码重复登录触发反爬:
// 新增单独的登录函数 async function loginToGlassdoor(page) { try { await page.goto("https://www.glassdoor.co.uk/index.htm"); await page.click( "#SiteNav > nav > div.d-lg-none.d-flex.align-items-center.justify-content-between.px-std.py-xsm.px-md-lg.py-md-std.LockedHomeHeaderStyles__bottomBorder.LockedHomeHeaderStyles__fullWidth > div.d-flex.justify-content-center.order-1.order-md-2.LockedHomeHeaderStyles__flexibleContainer > button", { delay: 200 } ); await page.type("#userEmail", "*******", { delay: 200 }); await page.type("#userPassword", "*******", { delay: 200 }); await page.click(".mt-std.d-flex.flex-column.align-items-center", { delay: 200 }); await page.waitForNavigation(); } catch (err) { console.error("登录失败:", err); } }
- 修改
scrapeListings,支持传入页码参数爬对应页的列表:
async function scrapeListings(page, pageNum) { try { // 拼接对应页码的URL await page.goto(`https://www.glassdoor.co.uk/Job/london-internship-jobs-SRCH_IL.0,6_IC2671300_KO7,17_IP${pageNum}.htm`); await page.waitForSelector("[data-test='jobListing']", { timeout: 5000 }); // 等待列表加载完成 const html = await page.content(); const $ = cheerio.load(html); const listings = $("[data-test='jobListing']") .map((index, element) => { const titleElement = $(element).find(".css-l2wjgv.e1n63ojh0.jobLink"); const timeElement = $(element).find("[data-test='job-age']"); const companyName = $(titleElement).text(); const url = "https://www.glassdoor.co.uk" + $(titleElement).attr("href"); const datePosted = $(timeElement).text(); return { companyName, url, datePosted }; }) .get(); return listings; } catch (err) { console.error(`爬取第${pageNum}页失败:`, err); return []; } }
- 修改
main函数,加分页循环,你可以自定义要爬的总页数,也可以判断当返回的列表为空时终止循环:
async function main() { await connectToMongoDb(); const browser = await puppeteer.launch({ headless: false }); const page = await browser.newPage(); // 先执行一次登录 await loginToGlassdoor(page); // 定义要爬的总页数,比如爬10页就设为10,想爬所有结果就设个大数值,等列表返回空自动终止 const totalPage = 10; for (let currentPage = 1; currentPage <= totalPage; currentPage++) { console.log(`正在爬取第${currentPage}页`); const listings = await scrapeListings(page, currentPage); if (listings.length === 0) { console.log("没有更多结果,爬取终止"); break; } // 爬当前页的职位详情 await scrapeJobDescriptions(listings, page); // 翻页间隔2秒,避免触发反爬 await sleep(2000); } await browser.close(); }
方案2:模拟点击下一页按钮(不需要提前知道总页数)
如果不想拼接URL,也可以找到页面底部的下一页按钮,每次爬完一页点击跳转,直到按钮不可点击为止:
你只需要在main函数里修改循环逻辑即可,示例如下:
async function main() { await connectToMongoDb(); const browser = await puppeteer.launch({ headless: false }); const page = await browser.newPage(); await loginToGlassdoor(page); // 先跳转到第一页 await page.goto("https://www.glassdoor.co.uk/Job/london-internship-jobs-SRCH_IL.0,6_IC2671300_KO7,17_IP1.htm"); while (true) { // 爬当前页列表 await page.waitForSelector("[data-test='jobListing']", { timeout: 5000 }); const html = await page.content(); const $ = cheerio.load(html); const listings = $("[data-test='jobListing']") .map((index, element) => { const titleElement = $(element).find(".css-l2wjgv.e1n63ojh0.jobLink"); const timeElement = $(element).find("[data-test='job-age']"); const companyName = $(titleElement).text(); const url = "https://www.glassdoor.co.uk" + $(titleElement).attr("href"); const datePosted = $(timeElement).text(); return { companyName, url, datePosted }; }) .get(); if (listings.length === 0) break; await scrapeJobDescriptions(listings, page); // 判断下一页按钮是否可点击,选择器不匹配可以自己F12抓取更新 const nextBtnDisabled = await page.$eval("button[aria-label='下一页']", btn => btn.disabled).catch(() => true); if (nextBtnDisabled) { console.log("已到最后一页,爬取结束"); break; } // 点击下一页 await page.click("button[aria-label='下一页']", { delay: 200 }); await page.waitForNavigation({ waitUntil: 'networkidle0' }); await sleep(2000); } await browser.close(); }
注意事项
- 原代码里
scrapeListings中return listings之后的await page.waitForNavigation是无效代码,执行不到,已经在修改版里删掉了。 - 可以适当加大翻页的延迟时间,避免触发Glassdoor的反爬机制导致IP被封。
- 如果遇到元素选择器失效的情况,重新抓取页面元素更新选择器即可。
内容的提问来源于stack exchange,提问作者Christopher
相关产品推荐
相关产品推荐

