无头浏览器(Headless Browser),顾名思义是没有显示用户图形界面的浏览器,在其他方面与普通浏览器并无区别,只是将原来通过键盘鼠标触发的操作事件转为由代码执行。无头浏览器最初用于自动化测试中,但其特性十分适合爬虫实现,如今也是一种不错的爬虫实现方式。
无头浏览器的实现工具除了puppteer还有selenium和phantomjs,选择puppteer的主要原因是因为它由谷歌公司推出,基于Chrome或Chromium,且仍在维护。
本项目在Node.js中实现,自然要先 npm i puppeteer安装依赖包,但它同时会下载对应版本的Chromium,个人尝试过程中npm下载会在下载Chromium时卡很久,使用cnpm则最终提示下载失败。电脑中已下载了Chrome浏览器的话可以选择不下载,直接使用该Chrome运行。方法就是在安装puppeteer前执行以下命令:
npm config set puppeteer_skip_chromium_download = 1
除此之外网上有通过修改npm配置文件来避免下载Chromium的方法,个人尝试后没有生效。
在安装完毕进行使用时,page.evaluate中的选择器无法使用,额外安装了jQuery便可以了,不太清楚具体原因,有同样问题的同学可以尝试。
首先确定的爬取目标是拉勾网,因为自己已经通过node-crawler模块实现了一个爬虫,对puppteer只是简单的尝试,所以只对主要内容作介绍。puppteer简单的使用例子网上很容易找到与理解,或者在这查看:https://www.npmjs.com/package/puppeteer,还可以查看API文档。
puppeteer的方法调用都是通过async await实现的,之前对ES6的promise不了解,经过这次实践后也算知道了如何简单地使用,还达不到能给人介绍的程度。简单说明下实践中的了解与体验,async定义的异步函数可以在定义时直接执行,但它的返回值永远是一个Promise对象,所以在定义完异步函数想要去调用它时必须将它赋值给一个变量或常量。
在这里以实现对拉勾网Java职位的数据爬取为例,定义一个可进行调用的异步函数:
async function init(){
const browser = await puppeteer.launch({//加载一个浏览器对象
headless:false,//是否为无头模式,为false则会弹出图形界面
executablePath:'C:\\Users\\mj\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe',
//chrome浏览器对应的路径
});
const page = await browser.newPage();//生成一个新页面
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134');
//设置User-Agent信息
await addCookies(cookiestr,page,'www.lagou.com');//addCookies为自定义函数,具体实现在下面
let jobList = [];//储存该职业全部的招聘信息
let nextBtnDisabled = false;//判断下一页按钮是否可点击,不可点击则表示数据爬取完毕
await page.goto(`https://www.lagou.com/zhaopin/Java/1`,{waitUntil:'networkidle2'});//页面进入目标地址,后一参数表示等待页面加载完毕
page.on('console',msg=> console.log('page log:',msg.text()));
//监听page.evaluate中的console事件,否则无法使用console.log输出
while (!nextBtnDisabled){
let t = await page.evaluate(()=>{//用于获取页面中数据
let $ = window.$;//获取jQuery对象,若爬取页面不支持jQuery需自己注入
let tempList = [];//当前页的招聘信息
let nextBtn = $('.page_no').last();//分页按钮的最后一个为下一页按钮
nextBtnDisabled = nextBtn.length==0 || nextBtn.hasClass('pager_next_disabled');
//不存在下一页按钮,即被反爬虫时,或者下一页按钮不可用时停止爬取
$(".con_list_item").each(function (i,elem) {//以下为对DOM结构的分析获取所需数据
$(elem).find('.money').empty();
let requirement = $(elem).find('.position .li_b_l').text().trim().split(' / ');//20k-30k 经验3-5年 / 本科
let temp = {
positionId:$(elem).attr('data-positionid'),
name:$(elem).attr('data-positionname'),
link:$(elem).find('.position_link').attr('href'),
location:$(elem).find('.add > em').text(),
company:$(elem).attr('data-company'),
companyLink:$(elem).find('.company_name > a').attr('href'),
companySize:($(elem).find('.industry').text().trim().split(' / '))[2],
salary:$(elem).attr('data-salary'),
exp:requirement[0],
edu:requirement[1]
};
tempList.push(temp)
});
return tempList
});//evaluate end
jobList = jobList.concat(t);
if(nextBtnDisabled){//爬取结束将数据储存,可继续进行对其他职位数据的爬取
fs.writeFileSync(`./data/Java.json`,JSON.stringify(jobList));
jobList = [];
await page.goto(`https://www.lagou.com/zhaopin/${currJob}/1`,{waitUntil:'networkidle2'});
nextBtnDisabled = false;
}else {//爬取未结束则在等待一段时间后点击下一页按钮并等待页面加载
const r = await Promise.all([
page.waitFor(5000 + Math.random() * 2000),
page.click(".page_no:last-child"),//页面click时间,括号内使用JQ选择器
page.waitForNavigation()//等待页面加载
])
}
}
await browser.close();//关闭浏览器
}
addCookies函数的实现使用的是网上的方法:
//cookies_str通过在目标页面的控制台中执行document.cookie直接获得,page为puppteer中的page对象,domain为目标页面的主域名
async function addCookies(cookies_str, page, domain){
let cookies = cookies_str.split(';').map(pair=>{
let name = pair.trim().slice(0,pair.trim().indexOf('='))
let value = pair.trim().slice(pair.trim().indexOf('=')+1)
return {name,value,domain}
});
await Promise.all(cookies.map(pair=>{
return page.setCookie(pair)
}))
}
在未登录的情况下,爬取十页就会进入登录页面,所以一定要设置登录后的cookie。但即使设置了登录后的cookie,在爬取了一定数量的页面后仍然会进入验证码验证界面,之前查过node中有识别验证码的库,但是之后没有再修改这个项目了,以后可以继续尝试。由于个人水平较低,有错误之处欢迎指出。