Puppeteer是一个通过Chrome DevTools Protocal程序性操作Chrome的官方node库,其功能十分强大,Chrome能做的基本都能做。最明显的用处大概是抓数据,现在用各种framework写出来的页面通常直接http get只能得到一个加载页面,而真正的内容需要执行JavaScript才能加载出来, 放在Chrome里运行自然就不成问题了。当然最大的优点也是其最大的缺陷,那就是需要安装并运行Chrome,比较耗费资源,规模太大的话就不太适合了。
这次找到puppeteer是因为需要做一个生成Steam销量报告的工具。 这个信息只能手动登录查看,而且登录的时候会出现Steam Guard, 要输入邮件里的验证码才能继续。最后的解决方法是创建了一个bot用的Steam帐号,模拟用户操作输入,自动读取发到Gmail的验证码并输入,然后就能抓到数据了。同时还用了第二个功能,用html生成一个报告,直接放在Chrome里渲染成一个pdf发回来。用浏览器渲染pdf可能有点小题大做的感觉,但既然Chrome已经运行起来了,就顺便了。下面记录几个个人在开发中遇到的问题。
1.运行环境
Puppeteer默认会下载Chromium. 这个可以通过在npmrc里写PUPPETEER_SKIP_CHROMIUM_DOWNLOAD来避免下载,或者直接安装puppeteer-core. 这样配合chrome-launcher就可以很灵活地启动不同版本的Chrome.
const launchOptions = {
logLevel: ‘silent’,
chromeFlags: [‘–headless’]
}
const chrome = await chromeLauncher.launch(launchOptions)
const debugPort = chrome.port
const resp = await util.promisify(request)(`http://localhost:${debugPort}/json/version`)
const {webSocketDebuggerUrl} = JSON.parse(resp.body)
const browser = await puppeteer.connect({browserWSEndpoint:webSocketDebuggerUrl})
const page = await browser.newPage()
开发的时候怎么来都可以,但是因为Chrome的存在,部署是个问题。开始本来想把整个功能放在AWS Lambda上,有人专门针对这个需要做了个瘦身版的serverless-chrome,能将将塞进50MB的空间里,但是由于最开始的设想是手动输入邮箱验证码,所以没有采用。然后转向了Elastic Beanstalk, 但是运行环境没有Chrome需要的dependency, 而且不能自己配置,最后还是搞了个EC2. 系统是Ubuntu 18.04LTS, 默认也是没有dependency,需要安装。由于需要输出PDF, 还需要安装字体,不然英文之外显示的是方块。
2.异步处理
一般说到JavaScript的异步处理就是用promise, 在操作puppeteer时则需要用到async/await的写法。Async返回一个promise, 而await是要等promise resolve. 无论是转向新页面、等待页面元素出现、获取页面内容,全部需要await, 很容易写丢。Await也只能在async函数里才能使用。
await page.waitForNavigation({waituntil:’networkidle2′});
await page.waitForSelector(‘#login_btn_wait’,{hidden: true, timeout:30000})
const successButton = await page.$(‘#success_continue_btn’)
await successButton.click()
const html = await page.content()
需要抓取的内容可以通过selector单独选取,但感觉需要获得的内容多了的话,与其一个一个await, 不如直接取得整个页面,然后用别的库来处理html. cheerio貌似很流行,可以用jQuery的写法。
3. 选择页面元素
自己写网页的话用不到很复杂的CSS selector,也就是选个class选个id, 但是解析别人写的网页就比较麻烦了,尤其是遇到需要选择的元素没有id的情况。其实CSS selector还有:nth-of-type(n), :nth-child(n)这些写法,而且能一层一层串起来,这样就能选到需要的元素了。这是个人之前没有注意到的。
4.异常处理
使用puppeteer需要处理的异常很多。首先http返回值不是200要处理,需要的页面元素没有出现也要处理,总之一不留神脚本就会停止执行。异常处理同时也可以作为判断页面状态的依据,例如一定时间内某个元素没有出现,就说明登录失败了。
try{
const response = await page.goto(url,{waituntil:’networkidle2′})
if (!response.ok){
console.log(“HTTP Error”)
}
}catch (err){
console.log(“Failed to load URL” + err)
}
try{
const contents = await page.content();
return cheerio.load(contents,{decodeEntities: false});
}catch (err){
console.log(“Most likely page is redirected”);
}
try{
await page.waitForSelector(‘div#header_menu’,{visible:true,timeout:5000})
console.log(“Login success”)
}catch(err){
console.log(“Login error ” + err)
}
5. Bot判定
这个要根据目标网站来调整。比如有的网站如果访问频次过快可能会屏蔽IP, 那么每次跳转页面之间就最好sleep上几秒。Puppeteer已经是在模拟用户操作,如果目标网站防bot对策不是很强的话应该没什么问题。但如果还是遇到Captcha的话就束手无策了。