当遇到React/Vue等现代前端框架构建的SPA(单页应用)时,传统爬虫无法获取JavaScript动态渲染的内容。本文将教你使用Puppeteer破解这个难题,实现真正的动态网页抓取。
1. 安装Node.js 版本至少要在14以上才行哦
2. 初始化开发项目的命令在这里
mkdir puppeteer-crawler && cd puppeteer-crawler
npm init -y
3. 在项目里边安装我们的依赖吧
npm install puppeteer
1. 引入库并进行虚拟浏览器页面的加载
const puppeteer = require('puppeteer');
async function main() {
const browser = await puppeteer.launch({
headless: false
});
const page = await browser.newPage();
// 你需要进行数据处理的代码...
await browser.close();
}
2. 开始加载我们需要爬取数据的页面,这里要设置我们的网络等待方式和时间
await page.goto('https://你要设置的域名', {
/** 等待网络空闲 */
waitUntil: 'networkidle2',
/** 30秒超时 */
timeout: 30000
})
3. 等待页面加载完成或则是等某个指定元素加载完成
await page.waitForSelector('.list-box', {
visible: true,
timeout: 5000
});
4. 对得到的页面进行数据提取
let result = await page.$$eval('.list-box ul li', el => (
el.map(ele => {
let spans = ele.querySelectorAll('.ext-info span')
let tags = []
ele.querySelectorAll('.ktags').forEach(node => {
tags.push(node.innerText)
})
return {
title: ele.querySelector('.title a')?.innerText,
tags,
date: spans && spans[0].innerText,
city: spans && spans[1].innerText
}
})
))
5. 设置浏览器指纹
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...');
await page.setViewport({ width: 1366, height: 768 });
6. 我们可以对页面的请求进行拦截,不必要的内容直接不加载,提升加载速度
await page.setRequestInterception(true);
page.on('request', (req) => {
if (['image', 'stylesheet', 'font', 'video'].includes(req.resourceType())) {
req.abort();
} else {
req.continue();
}
})
7. 如果有弹出框和验证框的话,我们也可以直接给它关闭了,当然要考虑你的具体情况而行哦
page.on('dialog', async dialog => {
await dialog.dismiss();
});
8. 绕过反爬机制
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'webdriver', { get: () => false });
});
const puppeteer = require('puppeteer')
const fs = require('fs')
/** 设置请求头 */
function setHeaders(page) {
return Promise.all([
/** 设置cookie数据,在这里一定要注意cookie的格式是{name: '', value: '', url: ''} */
page.setCookie(...[
{
"name":"Du4_vi-ds",
"value":"a06bc51efc49f2706c5d2434e10e9ec0",
"url":"https://你要设置的域名"
}
]),
/** 设置UserAgent数据,模拟一个真实的用户环境 */
page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'),
page.setViewport({ width: 1366, height: 768 })
])
}
async function main() {
/** 启动一个虚拟浏览器 */
const browser = await puppeteer.launch({
headless: false
})
/** 新建一个空白页面 */
const page = await browser.newPage()
/** 设置不必要加载内容拦截 */
await page.setRequestInterception(true);
page.on('request', (req) => {
if (['image', 'stylesheet', 'font'].includes(req.resourceType())) {
req.abort();
} else {
req.continue();
}
})
/** 弹出框关闭,这里一定要符合你自己的需求而定的 */
page.on('dialog', async dialog => {
await dialog.dismiss();
});
/** 禁用WebDriver特征 */
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'webdriver', { get: () => false });
});
/** 设置请求头 */
await setHeaders(page)
try {
/** 跳转到指定页面 */
await page.goto('https://你要设置的域名', {
/** 等待网络空闲 */
waitUntil: 'networkidle2',
/** 30秒超时 */
timeout: 30000
})
/** 开始获取当前页的数据内容 */
await handleHtml(page, 'page1.json')
/** 处理分页的数据,这里可以采用递归的方式来实现 */
await handlePaging(page, 2)
} catch (e) {
console.log('------------------------------ 错误')
console.log(e)
} finally {
/** 关闭应用 */
await browser.close()
}
}
async function handlePaging(page) {
/** 计算页面的最大分页值,然后进行逐页递归 */
let pagings = await page.$$eval(
'.pages a',
elements => elements.map(el => (el.textContent.trim()))
)
let max = Math.max(...(pagings.filter(it => !isNaN(it)).map(it => it * 1)))
return new Promise(async resolve => {
try {
/** 递归每个页面的数据,并进行数据处理 */
async function loop(nowPage) {
/** 获取所有的分页按钮 */
let btns = await page.$$('.pages a')
for (let i = 0;i < btns.length;i ++) {
let num = btns[i].evaluate(e => e.textContent.trim())
if (!isNaN(num) && num == 2) {
btns[i].click()
break;
}
}
/** 等待页面加载完成 */
await page.waitForNavigation();
/** 处理页面加载的数据 */
await handleHtml(page, 'page' + nowPage + '.json')
nowPage += 1;
if (nowPage > max) {
resolve()
} else {
setTimeout(() => {
loop(nowPage)
}, 1000)
}
}
setTimeout(() => {
loop(2)
}, 1000)
} catch {
resolve()
}
})
}
function handleHtml(page, filename) {
/** 使用cheerio加载html并进行页面数据解析,把解析后的数据存储起来 */
return new Promise(async resolve => {
try {
let result = await page.$$eval('.list-box ul li', el => (
el.map(ele => {
let spans = ele.querySelectorAll('.ext-info span')
let tags = []
ele.querySelectorAll('.ktags').forEach(node => {
tags.push(node.innerText)
})
return {
title: ele.querySelector('.title a')?.innerText,
tags,
date: spans && spans[0].innerText,
city: spans && spans[1].innerText
}
})
))
fs.writeFileSync(filename, JSON.stringify(result), {encoding: 'utf-8'})
resolve()
} catch (e) {
console.log(e)
resolve()
}
})
}
/** 调用方法 */
main()
相比传统爬虫,Puppeteer虽然资源消耗较大,但能完美解决现代Web应用的爬取难题。建议将Puppeteer与Cheerio结合使用,在需要执行交互操作时启动浏览器,静态内容解析时使用轻量级方案。