Puppeteer 是什么
- Puppeteer 是 Node.js 工具引擎
- Puppeteer 提供了一系列 API,通过 Chrome DevTools Protocol 协议控制 Chromium/Chrome 浏览器的行为
- Puppeteer 默认情况下是以 headless 启动 Chrome 的,也可以通过参数控制启动有界面的 Chrome
- Puppeteer 默认绑定最新的 Chromium 版本,也可以自己设置不同版本的绑定
- Puppeteer 让我们不需要了解太多的底层 CDP 协议实现与浏览器的通信
Puppeteer 能做什么
官方称:“Most things that you can do manually in the browser can be done using Puppeteer”,那么具体可以做些什么呢?
- 网页截图或者生成 PDF
- 爬取 SPA 或 SSR 网站
- UI 自动化测试,模拟表单提交,键盘输入,点击等行为
- 捕获网站的时间线,帮助诊断性能问题
- 创建一个最新的自动化测试环境,使用最新的 js 和最新的 Chrome 浏览器运行测试用例
- 测试 Chrome 扩展程序 ...
Puppeteer 常用API
- Browser: 对应一个浏览器实例,一个 Browser 可以包含多个 BrowserContext
- BrowserContext: 对应浏览器一个上下文会话,就像我们打开一个普通的 Chrome 之后又打开一个隐身模式的浏览器一样,BrowserContext 具有独立的 Session(cookie 和 cache 独立不共享),一个 BrowserContext 可以包含多个 Page
- Page:表示一个 Tab 页面,通过 browserContext.newPage()/browser.newPage() 创建,browser.newPage() 创建页面时会使用默认的 BrowserContext,一个 Page 可以包含多个 Frame
- Frame: 一个框架,每个页面有一个主框架(page.MainFrame()),也可以多个子框架,主要由 iframe 标签创建产生的
- ExecutionContext: 是 javascript 的执行环境,每一个 Frame 都一个默认的 javascript 执行环境
- ElementHandle: 对应 DOM 的一个元素节点,通过该该实例可以实现对元素的点击,填写表单等行为,我们可以通过选择器,xPath 等来获取对应的元素
- JsHandle:对应 DOM 中的 javascript 对象,ElementHandle 继承于 JsHandle,由于我们无法直接操作 DOM 中对象,所以封装成 JsHandle 来实现相关功能
- CDPSession:可以直接与原生的 CDP 进行通信,通过 session.send 函数直接发消息,通过 session.on 接收消息,可以实现 Puppeteer API 中没有涉及的功能
- Coverage:获取 JavaScript 和 CSS 代码覆盖率
- Tracing:抓取性能数据进行分析
- Response: 页面收到的响应
- Request: 页面发出的请求
更多请查阅官方文档 github地址
准备工作
安装模块
# 安装puppeteer
npm install puppeteer --save
# puppeteer 需要安装 chromium,可以选择修改 puppeteer 的下载源:
npm config set puppeteer_download_host https://npm.taobao.org/mirrors
# 安装chalk
npm install chalk --save
先思考
我们可以先去观察下这个 页面 的HTML
结构, 爬取这个页面以及每个详情页面我们需要什么
- 爬取页面所需要的页面元素
- 跟页面的交互(比如分页)
- 该怎么样获取多页内容
- 怎样跳到详情页面 ...
基本思路
- 打开我们要爬取的页面
- 获取数据列表以及分页按钮
- 循环列表打开每一个item
- 在页面中执行js拿到我们想要拿到的数据
- 当前页数据抓取完毕点击下一页按钮 ...
爬取豆瓣电影
async index() {
const { ctx } = this;
const log = console.log;
/// 图方便我把常用的数据写在了这里
const scrape = {
url: 'https://movie.douban.com/tag/#/',
click: '.more',
page: 2,
itemList: '.list-wp > a',
};
/// 启动一个浏览器环境
const browser = await puppeteer.launch();
log(chalk.green('服务正常启动'));
/// 捕获错误
try {
/// 打开一个新的页面
const page = await browser.newPage();
/// 我们需要监听 request的话 必须要开启
await page.setRequestInterception(true);
/// 监听内部的request
page.on('request', interceptedRequest => {
if (interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg'))
interceptedRequest.abort();
else
interceptedRequest.continue();
});
/// 打开一个页面 config 请自行查阅文档
await page.goto(scrape.url, {
waitUntil: 'networkidle2',
timeout: 0,
});
/// scrape.page 爬取几页内容
for (let l = 0; l < scrape.page; l++) {
/// 等待页面元素加载完毕
await page.waitForSelector(scrape.click);
if (l > 0) {
const submit = await page.$(scrape.click);
if (!submit) {
log(chalk.red('按钮不存在!'));
return;
}
await submit.click();
await page.waitFor(2500);
}
console.clear();
/// 格式化进度
log(chalk.yellow(ctx.helper.formatProgress(ctx, l, scrape.page)));
/// 获取当前列表
const itemList = await page.?(scrape.itemList);
// 开始循环当前列表 我们看到列表一页数据在20条 所以我们从当前页面*20开始循环 这样就不会出现每次重新循环都从第一条数据开始
for (let i = l * 20; i < itemList.length; i++) {
const items = await page.?(scrape.itemList);
const item = items[i];
/// 获取列表的第i个元素的url
const url = await page.evaluate(item => {
return item ? item.href : '';
}, items[i]);
if (url) {
const page2 = await browser.newPage();
await page2.goto(url, {
timeout: 0,
});
/// 等待一会
await page2.waitFor(1000);
/// page2.evaluate 使我们可以在页面中执行js方法
const result = await page2.evaluate(() => {
const info = document.querySelector('#info');
const screenwriter = [];
const starring = [];
const types = [];
info.querySelectorAll('.attrs')[1].querySelectorAll('a').forEach(item => {
screenwriter.push(item.innerText);
});
info.querySelectorAll('.attrs')[2].querySelectorAll('a').forEach(item => {
starring.push(item.innerText);
});
info.querySelectorAll('[property="v:genre"]').forEach(item => {
types.push(item.innerText);
});
return {
title: document.querySelector('h1') ? document.querySelector('h1').innerText : '',
mainpic: document.querySelector('.nbgnbg > img') ? document.querySelector('.nbgnbg > img').src : '',
director: info.querySelectorAll('.attrs')[0].querySelector('a') ? info.querySelectorAll('.attrs')[0].querySelector('a').innerText : '',
screenwriter,
starring,
types,
release_date: info.querySelector('[property="v:initialReleaseDate"]') ? info.querySelector('[property="v:initialReleaseDate"]').innerText : '',
length: info.querySelector('[property="v:runtime"]') ? info.querySelector('[property="v:runtime"]').innerText : '',
rating_num: document.querySelector('[property="v:average"]') ? document.querySelector('[property="v:average"]').innerText : '',
};
});
/// 写入文件
fs.appendFile('./movie.json', JSON.stringify(result, null, '\t', {
flag: 'a',
}), function(err) {
if (err) {
throw err;
}
});
}
}
}
await browser.close();
log(chalk.green('服务正常结束'));
} catch (error) {
console.log(error);
log(chalk.red('服务意外终止'));
await browser.close();
}
}
爬取的内容
{
"title": "致命女人 第一季 Why Women Kill Season 1 (2019)",
"mainpic": "https://img3.doubanio.com/view/photo/s_ratio_poster/public/p2566967861.webp",
"director": "大卫·格罗斯曼",
"screenwriter": [
"马克·切利",
"艾莉莎·荣格"
],
"starring": [
"刘玉玲",
"金妮弗·古德温",
"柯尔比·豪威尔-巴普蒂斯特",
"杰克·达文波特",
"山姆·贾格",
"里德·斯科特",
"亚历珊德拉·达达里奥",
"赛迪·卡尔瓦诺",
"里奥·霍华德",
"艾丽莎·科波拉",
"凯蒂·芬内朗",
"更多..."
],
"types": [
"剧情",
"喜剧",
"犯罪"
],
"release_date": "2019-07-21(洛杉矶LGBT电影节)",
"length": "",
"rating_num": "9.4"
}
结尾
无事撸一撸,撸的不对勿喷,大家一起学习一起成长 Come on