关于puppeteer
1 2 3
| puppeteer是一个node库,他提供了一组用来操纵Chrome的API(默认headless也就是无UI的chrome,也可以配置为有UI)
有点类似于PhantomJS,但Puppeteer是Chrome官方团队进行维护的,前景更好。
|
puppeteer可以做什么
- 利用网页生成PDF、图片
- 爬取SPA应用,并生成预渲染内容(即“SSR” 服务端渲染)
- 可以从网站抓取内容
- 自动化表单提交、UI测试、键盘输入等
- 帮你创建一个最新的自动化测试环境(chrome),可以直接在此运行测试用例
- 捕获站点的时间线,以便追踪你的网站,帮助分析网站性能问题
基本了解之后,我们今天打算来用puppeteer做一个小爬虫试试。
环境和安装
puppeteer依赖v6.4以上的Node,在这里我推荐安装v7.6以上版本的node,便于我们更好地使用async/wait,如果你的机器上已经安装了node,同时对版本又有特定的需求,我建议你了解一下nvm,node版本管理神器,实现多个node版本自由切换。另外,我这里采用v9.4.0版本的node。
puppeteer也是一个npm包,所以安装还是老套路。
puppeteer安装时自带一个最新版本的Chromium,如果你没有梯子的话,建议使用淘宝的cnpm安装,这样会自动地从淘宝的源下载Chromium。如果你本地已经安装Chromium,可以通过设置环境变量或者npm config中的PUPPETEER_SKIP_CHROMIUM_DOWNLOAD
跳过下载,我这里使用npm官方源安装。
小试牛刀-生成网页截图
puppeteer安装完成之后,我们先来个小demo练练手,对指定网站进行截图,我们就拿公司内网试试吧。创建screenshot.js文件,贴入以下代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| const puppeteer = require('puppeteer')
async function getPic() { const browser = await puppeteer.launch() const page = await browser.newPage() page.setViewport({ width: 1920, height: 2000 }) await page.goto('http://777.nd.com.cn/html/index.html') await page.screenshot({path: 'nd.png'}) await browser.close() }
getPic()
|
在当前目录执行node screenshot.js
命令,不出意外的话我们将会得到如下截图。
爬虫实践
我们可以轻易地想到,puppeteer功能如此强大,用来做爬虫是再好不过了。接下来的内容也是我自己的实践:网易云音乐的歌曲下面总是有各种各样的神评论,所以我打算用puppeteer来收集一下云音乐的评论。
需求分析
我们的需求是输入一个音乐人的主页链接,收集这个音乐人下的所有热门单曲的所有评论。例如音乐人赵雷的主页下有47首热门单曲,他的主页是http://music.163.com/#/artist?id=6731,接下来我们一步一步实现。
分析音乐人主页的DOM结构
我们发现主页的整个页面是由一个iframe包裹的,iframe中有一个id是song-list-pre-cache
的div,该div下有一个table,table下的每一个a标签,就是每一首歌的url以及歌名了。
接下来我们进行第一步。
获取主页歌曲列表
这一步开始前,我们先分析一下具体操作步骤:打开音乐人主页->取到对应iframe->取到id为song-list-pre-cache
的节点->遍历所有的a标签节点->取到歌曲的url和名称。
关于分析DOM结构,我这里使用cheerio,其简介为“为服务器特别定制的,快速、灵活的jQuery核心实现”。
我们新建song-list.js
文件,执行以下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| const puppeteer = require('puppeteer') const cheerio = require('cheerio') const _ = require('lodash')
const host = 'http://music.163.com/#'
puppeteer .launch({headless: true}) .then(async browser => { const page = await browser.newPage() page.setViewport({width: 1367, height: 768}) await page.goto(`${host}/artist?id=6731`, {waitUntil: 'networkidle0'}) const frames = await page.frames() const targetFrame = frames.find(frame => { return frame.name() === 'contentFrame' }) const content = await targetFrame.content() let $ = cheerio.load(content) const $list = $('#song-list-pre-cache') const songList = $list .find('a') .map((i, elem) => { const href = $(elem).attr('href') if (_.startsWith(href, '/song?id=')) { const title = $(elem).find('b').attr('title') return {href, title} } }) .get() console.log(songList) await browser.close() })
|
不出意外的话,控制台将会打印出音乐人的歌曲列表,列表的每一项包含歌曲的url和title,如下图。
跳转到歌曲页面
在上一步,我们拿到了歌曲的url,那么接下来自然就是跳转到每首歌的页面,进行评论抓取了。
歌曲页面的DOM结构和音乐人页面很类似,也有一个iframe,iframe中有一个id为comment-box
的div节点,在这个节点在遍历class为itm
的div节点,在itm
下,通过具体分析,我们就能取到用户名和评论内容了。关于热评和最新评论,我们先简单处理,不区分。
我们将每首歌第一页的评论都写在content.html
文件中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| const puppeteer = require('puppeteer') const cheerio = require('cheerio') const he = require('he') const fs = require('fs') const _ = require('lodash')
const host = 'http://music.163.com/#'
* 抽象出抓取评论的函数,便于进一步开发 * @param {*} sTargetFrame */ const collectCommentList = async(sTargetFrame) => { const sContent = await sTargetFrame.content() const $ = cheerio.load(sContent) const res = $('#comment-box') .find('.itm') .map((i, elem) => { const brk = $(elem).find('.cntwrap .f-brk') const brkText = brk.text() const username = $(brk) .find('a') .text() const commentStr = $(brk) .text() .replace(username, '') return {username, commentStr} }) .get() fs.appendFile('./content.html', JSON.stringify(res) + '\n') return res }
puppeteer .launch({headless: true}) .then(async browser => { const page = await browser.newPage() page.setViewport({width: 1367, height: 768}) await page.goto(`${host}/artist?id=6731`, {waitUntil: 'networkidle0'})
const frames = await page.frames()
const targetFrame = frames.find(frame => { return frame.name() === 'contentFrame' })
const content = await targetFrame.content()
let $ = cheerio.load(content)
const $list = $('#song-list-pre-cache')
const songList = $list .find('a') .map((i, elem) => { const href = $(elem).attr('href') if (_.startsWith(href, '/song?id=')) { const title = $(elem).find('b').attr('title') return {href, title} } }) .get()
let index = 0 let songItem console.log(`一共${songList.length}首歌`) while (songItem = songList[index++]) { const sPage = await browser.newPage() console.log(`正在收集${songItem.title}下的评论`) await sPage.goto(`${host}${songItem.href}`, { waitUntil: 'networkidle2', timeout: 0 }) const sFrames = await sPage.frames() const sTargetFrame = sFrames.find(frame => { return frame.name() === 'contentFrame' })
let commentList = await collectCommentList(sTargetFrame)
sPage.close() }
await browser.close() })
|
到此为止,我们已经可以抓取每一首歌的第一页评论了,但是这远远不够,一首热门歌曲往往有上万条评论,这就需要翻页抓取了。
评论翻页
puppeteer功能强大,模拟按钮点击功能当然也不在话下。所以我们接下来要做的事情就是点击下一页按钮后,再重复一次抓取动作即可。
避免篇幅过大,我这里贴出主要的翻页代码,完整代码会在文末给出。
1 2 3 4 5 6 7 8 9 10 11 12
| do { let commentList = await collectCommentList(sTargetFrame) let nextBtnHandler = await sTargetFrame.$('.znxt') if (!nextBtnHandler) { break } nextBtnHandler.click() await timeout(1500) } while (await isBtnAbled(sTargetFrame))
let commentList = await collectCommentList(sTargetFrame)
|
1 2 3 4 5
| const isBtnAbled = async(sTargetFrame, className) => { const sContent = await sTargetFrame.content() const $ = cheerio.load(sContent) return !$('.znxt').hasClass('js-disabled') }
|
1 2 3
| async function timeout(ms) { return new Promise(resolve => setTimeout(resolve, ms)) }
|
到现在,我们的爬虫功能就基本完成了,我这里暂时只把评论内容存入文本,还没有存放在数据库中。如果你感兴趣,可以把源码clone下来,拓展一下。
关于puppeteer更多更好玩的API,可以自行查看文档,这是一个不断更新的库,所以隔一段时间相关API可能就会有更新,因此阅读文档可能是深入学习的最好方式。
参考文章