本教程仅用于学习,不要用于商业。以往通常使用请求获取页面(request、superagent…)+操作网页提取需要的数据(cheerio)的方式来写爬虫,现在已经基本被废掉了,因为很多网站都是通过异步请求获取数据然后渲染页面,这样使我们请求获取的页面不是最终展示的页面,怎么处理这个问题呢?接下来跟着我来学习一下。
案例
爬取掘金首页前端页面前10篇文章
环境
- 谷歌浏览器
- node
核心类库
puppeteer
文档
http://www.puppeteerjs.com/
安装
npm i puppeteer -S
第一步获取网页内容
const puppeteer = require('puppeteer');
puppeteer.launch({
// 在导航期间忽略 HTTPS 错误
ignoreHTTPSErrors: true,
// 不在 无头模式 下运行浏览器
headless: false,
// 将 Puppeteer 操作减少指定的毫秒数
slowMo: 250,
// 等待浏览器实例启动的最长时间,0禁用超时
timeout: 0
}).then(async browser => {
// 一个新的 Page 对象
let page = await browser.newPage();
// 启用js
await page.setJavaScriptEnabled(true);
// 打开URL
await page.goto("https://juejin.cn/frontend");
console.log(await page.content())
// 关闭页面
await page.close();
// 关闭浏览器
await browser.close();
})
上面代码可以获取到整个页面,测试发现已经解决了问题,其实puppeteer有专门解决这个问题的方法page.waitForSelector
等待指定的选择器匹配的元素出现在页面中。
const puppeteer = require('puppeteer');
puppeteer.launch({
// 在导航期间忽略 HTTPS 错误
ignoreHTTPSErrors: true,
// 不在 无头模式 下运行浏览器
headless: false,
// 将 Puppeteer 操作减少指定的毫秒数
slowMo: 250,
// 等待浏览器实例启动的最长时间,0禁用超时
timeout: 0
}).then(async browser => {
// 一个新的 Page 对象
let page = await browser.newPage();
// 启用js
await page.setJavaScriptEnabled(true);
// 打开URL
await page.goto("https://juejin.cn/frontend");
// 等待列表出现
await page.waitForSelector('.entry-list-wrap .entry-list .item');
console.log(await page.content())
// 关闭页面
await page.close();
// 关闭浏览器
await browser.close();
})
第二步获取列表中标题和链接
const puppeteer = require('puppeteer');
puppeteer.launch({
// 在导航期间忽略 HTTPS 错误
ignoreHTTPSErrors: true,
// 不在 无头模式 下运行浏览器
headless: false,
// 将 Puppeteer 操作减少指定的毫秒数
slowMo: 250,
// 等待浏览器实例启动的最长时间,0禁用超时
timeout: 0
}).then(async browser => {
// 一个新的 Page 对象
let page = await browser.newPage();
// 启用js
await page.setJavaScriptEnabled(true);
// 打开URL
await page.goto("https://juejin.cn/frontend");
// 等待列表出现
await page.waitForSelector('.entry-list-wrap .entry-list .item');
// 获取列表中标题和链接
const list = await page.$$eval('.entry-list-wrap .entry-list .title-row a', list => {
return list.map(item => {
return {
title: item.innerText,
href: item.href,
}
});
});
console.log(list.length)
console.log(list)
// 关闭页面
await page.close();
// 关闭浏览器
await browser.close();
})
第三步获取文章详情
const puppeteer = require('puppeteer');
puppeteer.launch({
// 在导航期间忽略 HTTPS 错误
ignoreHTTPSErrors: true,
// 不在 无头模式 下运行浏览器
headless: false,
// 将 Puppeteer 操作减少指定的毫秒数
slowMo: 250,
// 等待浏览器实例启动的最长时间,0禁用超时
timeout: 0
}).then(async browser => {
// 一个新的 Page 对象
let page = await browser.newPage();
// 启用js
await page.setJavaScriptEnabled(true);
// 打开URL
await page.goto("https://juejin.cn/frontend");
// 等待列表出现
await page.waitForSelector('.entry-list-wrap .entry-list .item');
// 获取列表中标题和链接
let list = await page.$$eval('.entry-list-wrap .entry-list .title-row a', list => {
return list.map(item => {
return {
title: item.innerText,
href: item.href,
}
});
});
list = list.slice(0, 10)
// 获取文章详情
for (let i = 0; i < list.length; i++) {
await page.goto(list[i].href);
list[i].content = await page.$eval('.article-content', e => e.innerHTML)
}
console.log(list[0])
// 关闭页面
await page.close();
// 关闭浏览器
await browser.close();
})
第四步保存数据
此处不介绍存到数据库,只是简单的写一个json文件。
const puppeteer = require('puppeteer');
const fs = require('fs')
const path = require('path')
puppeteer.launch({
// 在导航期间忽略 HTTPS 错误
ignoreHTTPSErrors: true,
// 不在 无头模式 下运行浏览器
headless: false,
// 将 Puppeteer 操作减少指定的毫秒数
slowMo: 250,
// 等待浏览器实例启动的最长时间,0禁用超时
timeout: 0
}).then(async browser => {
// 一个新的 Page 对象
let page = await browser.newPage();
// 启用js
await page.setJavaScriptEnabled(true);
// 打开URL
await page.goto("https://juejin.cn/frontend");
// 等待列表出现
await page.waitForSelector('.entry-list-wrap .entry-list .item');
// 获取列表中标题和链接
let list = await page.$$eval('.entry-list-wrap .entry-list .title-row a', list => {
return list.map(item => {
return {
title: item.innerText,
href: item.href,
}
});
});
list = list.slice(0, 10)
// 获取文章详情
for (let i = 0; i < list.length; i++) {
await page.goto(list[i].href);
list[i].content = await page.$eval('.article-content', e => e.innerHTML)
}
// 保存数据
fs.writeFileSync(path.resolve(__dirname, '../out/articles.json'), JSON.stringify(list, null, 2))
// 关闭页面
await page.close();
// 关闭浏览器
await browser.close();
})
最后进行方法抽离等处理
方法文件
const fs = require('fs')
exports.init = async function (browser) {
let page = await browser.newPage();
await page.setJavaScriptEnabled(true);
await page.goto("https://juejin.cn/frontend");
await page.waitForSelector('.entry-list-wrap .entry-list .item');
return page;
}
exports.getList = async function (page) {
let list = await page.$$eval('.entry-list-wrap .entry-list .title-row a', list => {
return list.map(item => {
return {
title: item.innerText,
href: item.href,
}
});
});
return list.slice(0, 10)
}
exports.getListDetail = async function (list, page) {
for (let i = 0; i < list.length; i++) {
await page.goto(list[i].href, {
timeout: 0
});
list[i].content = await page.$eval('.article-content', e => e.innerHTML)
}
return list
}
exports.writeFile = async function (name, content) {
fs.writeFileSync(name, content)
}
主文件
const puppeteer = require('puppeteer');
const path = require('path')
const methods = require('./methods')
puppeteer.launch({
// 在导航期间忽略 HTTPS 错误
ignoreHTTPSErrors: true,
// 不在 无头模式 下运行浏览器
headless: false,
// 将 Puppeteer 操作减少指定的毫秒数
slowMo: 250,
// 等待浏览器实例启动的最长时间,0禁用超时
timeout: 0
}).then(async browser => {
try {
// 初始化
let page = await methods.init(browser);
// 获取列表中标题和链接
let list = await methods.getList(page)
// 获取文章详情
list = await methods.getListDetail(list, page)
// 保存数据
methods.writeFile(path.resolve(__dirname, '../out/articles.json'), JSON.stringify(list, null, 2))
// 关闭页面
await page.close();
// 关闭浏览器
await browser.close();
} catch (error) {
console.error(error)
// 关闭浏览器
await browser.close();
}
})
项目地址
https://gitee.com/lydxwj/juejin