智联招聘爬虫——学习笔记(1).md
2017.4.2-2017.4.5
在这个星期中,运用了上一个机票爬虫中学到的思路和技术点,爬取了智联招聘中互联网工作招聘的信息。
准备工作:
- 观察智联招聘页面结构,确定待爬取的目标内容 ==> 互联网方面的招聘
- 通过chrome中的network标签卡,查看网站的数据请求为get方式或post方式 ==> get方式,收到的数据包不是json数据,是完整html,需要解析后抓取数据
- 注意事项:在互联网工作类别中,一共有11904条招聘信息,但是每页显示数据60条,页码栏最多只能翻到90页。也就是说只能看到11904条信息中的前5400条。==> 控制请求内容数,每次只请求小于5400条。
在互联网的大类中,逐个查询小类,即可实现数量控制。 - 查看翻页请求的数据请求方式,发现同样是用get方式请求数据
发现两者的区别就是p参数的不同,可以选择修改向网页发起get请求时的发送的查询参数,也可以直接修改url字符串的末尾。
软件工程师类别中的第一页Url:
http://sou.zhaopin.com/jobs/searchresult.ashx?jl=%E6%9D%AD%E5%B7%9E&sm=0&fl=653&isadv=0&ispts=1&isfilter=1&p=1&bj=160000&sj=045
软件工程师类别中的第二页Url:
http://sou.zhaopin.com/jobs/searchresult.ashx?jl=%E6%9D%AD%E5%B7%9E&sm=0&fl=653&isadv=0&ispts=1&isfilter=1&bj=160000&sj=045&sg=e161067ed4ce4128b7d036296fe5eee4&p=2
软件工程师类别中的第三页Url:
http://sou.zhaopin.com/jobs/searchresult.ashx?jl=%E6%9D%AD%E5%B7%9E&sm=0&fl=653&isadv=0&ispts=1&isfilter=1&bj=160000&sj=045&sg=e161067ed4ce4128b7d036296fe5eee4&p=3



主要编程思路:
瀑布流控制
------查询每一个类别的互联网工作的Url(通过导航栏中的a标签href属性获得)
------------获取单个类别下分页地址(即仅查询参数中p值改变的url)
------------------获取每一个单类单个分页中的60条招聘信息详情页的url(同样通过a标签href属性获得)
------------------------逐个抓取招聘信息详情页的内容
使用的工具
- nodejs
- superAgent
- cheerio
- async
- mongodb
笔记导航:
- 学习老师的编码风格
- Vue.js学习
- 智联招聘网站信息爬取
- 招聘信息整理,正则表达式学习
代码分析
1. 查询导航栏中包含的每个工种的url
使用superagent向浏览器发出get请求
使用cheerio分析html页面
function query(cb) {
var jobType = [];
superAgent.get("http://sou.zhaopin.com/jobs/searchresult.ashx?jl=%E6%9D%AD%E5%B7%9E&sm=0&isfilter=1&p=1&bj=160000")
.set(headers)
.end(function (err ,res) {
if(err){
console.log(err);
}else {
var $ = cheerio.load(res.text);
//获取每一个a标签的href属性
$("#search_jobtype_tag a").each(function (index, link) {
var $link = $(link);
var newLink = {
href:$link.attr("href"),
desc:$link.text()
};
jobType.push(newLink);
});
//去除开头的两个无用链接
jobType.splice(0,2);
}
cb(null, jobType);
})
}
2. 获取单个工种下连续分页页面的url
//传入参数类型
//typeUrls = {
// href: String
// desc: String
//}
//返回类型
//result = {
// url: String
// page: Number
//}
function getPageUrl(typeUrls, cb){
console.log("get page url");
async.mapLimit(typeUrls, 1, function (typeUrl , callback) {
superAgent.get("http://sou.zhaopin.com" + typeUrl.href)
.set(headers)
.end(function (err, res) {
if(err){
console.log(err);
}else {
var $ = cheerio.load(res.text);
//判断页码栏中“下一页“按钮是否存在href属性
//如果没有,说明该工种的数据不超过一页,就赋原址
//如果存在href属性,保存具有可以通过递推获得连续url的地址
var nextPageButton = $(".newlist_main .pagesDown-pos a");
if(nextPageButton.length > 0){
var newLink = nextPageButton.attr('href');
//typeUrl是一个类似"软件工程师(2242)"的字符串,取60余数取顶,即总页数
var count = parseInt((typeUrl.desc).substring(typeUrl.desc.indexOf("(")+1, typeUrl.desc.indexOf(")")));
var newTypeUrl = {
url: newLink,
page: Math.ceil(count/60),
};
}else {
var newTypeUrl = {
url: 'http://sou.zhaopin.com' + typeUrl.href,
page: 1
}
}
callback(null, newTypeUrl);
}
})
},function (err, result) {
cb(null, result);
});
}
3. 获取单个工种的所有招聘单页的url
function getDetailUrl(typeUrl, callback) {
var detailUrls = []; //储存该工种的所有招聘详情页的的url
if(typeUrl.page == 1){ //如果只有一页,抓当前页的招聘贴url即可,不需要修改url中的p参数
superAgent.get(typeUrl.url)
.set(headers)
.end(function (err, res) {
if(err){
console.log(err);
} else {
var $ = cheerio.load(res.text);
//获取当前序号页面中的links
$("#newlist_list_content_table table").each(function (index, element){
var $element = $(element);
var detailPageUrl = $element.find(".zwmc div a").attr('href');
if(typeof(detailPageUrl) !== undefined){
detailUrls.push(detailPageUrl);
}
})
setTimeout(function () {
callback(null, detailUrls);
},1000);
}
})
}else { //查询结果不止一页
//获取从第一页到最后一页的序列地址 ==> links
var originalLink = typeUrl.url.substring(0,typeUrl.url.length-1);
var paginationLinks = [];
for(var i = 1; i <= typeUrl.page ; i++){
paginationLinks.push(originalLink+i);
}
async.mapLimit(paginationLinks, 1, function (pageLink, cb) {
superAgent.get(pageLink)
.set(headers)
.end(function (err, res) {
if(err){
console.log(err);
} else {
var $ = cheerio.load(res.text);
//获取当前序号页面中的60条links
$("#newlist_list_content_table table").each(function (index, element){
var $element = $(element);
var detailPageUrl = $element.find(".zwmc div a").attr('href');
if(index > 0){ //跳过第一个表头
detailUrls.push(detailPageUrl);
}
})
cb(null, 'oneDetailPageUrl');
}
})
},function (err, rs) {
setTimeout(function () {
callback(null, detailUrls);
},1000);
});
}
}
4. 获取单页招聘信息的dom节点中的数据
function getDetailInfo(jobUrl, callback) {
//在大量抓取中可能会出现超时错误或其他,使用try catch抓取错误信息,继续下一个查询
try {
superAgent.get(jobUrl)
.query({
'ssidkey':'y',
'ss':201,
'ff':03,
})
.set(detailPageHeaders)
.end(function (err, res) {
if(err){
console.log(err);
} else {
var $ = cheerio.load(res.text);
// 获取页面中的dom
var title = $(".top-fixed-box .inner-left");
var jobTitle = $(title).find("h1").text();
var company = $(title).find('a').text();
var bonus = '';
$(title).find("span").each(function (index, bonusSpan) {
var $bonusTag = $(bonusSpan);
// console.log($bonusTag.text());
bonus += $bonusTag.text()+ "、";
})
bonus = bonus.substr(0,bonus.length-1);
var detail = $(".terminal-ul li");
var salary = $(detail).eq(0).find('strong').text();
var citylocation = $(detail).eq(1).find('strong').text();
var publishDate = $(detail).eq(1).find('strong').text();
var natureOfWork = $(detail).eq(3).find('strong').text();
var experienceRequirement = $(detail).eq(4).find('strong').text();
var eduRequirement = $(detail).eq(5).find('strong').text();
var requiredNum = $(detail).eq(6).find('strong').text();
var jobType = $(detail).eq(7).find('strong').text();
var jobDesc = '';
$(".tab-inner-cont p").each(function (index, pDesc) {
var $desc = $(pDesc);
jobDesc += $desc.text();
})
var workplaceLocation = $(".tab-inner-cont h2").text().replace('查看职位地图','').trim();
var newZhaopin = {
jobTitle: jobTitle,
company: company,
bonus: bonus,
salary: salary,
cityLocation: citylocation,
publishDate: publishDate,
natureOfWork: natureOfWork,
expReq: experienceRequirement,
numReq: requiredNum,
jobType: jobType,
jobDesc: jobDesc,
workPlaceLocation: workplaceLocation
}
//调用mongodb数据库的操作函数,把数据去重保存入数据库中
db.saveJobInfo(jobTitle,company,bonus,salary,citylocation,publishDate,natureOfWork,experienceRequirement,requiredNum,jobType,jobDesc,workplaceLocation);
callback(null, newZhaopin);
}
})
} catch(e){
console.log('error.......');
callback(null, {});
}
}
5. 利用mongodb模块去重保存到mongodb数据库
mongodb模块用法与终端操作指令基本相似
//db.js
var mongo = require('mongodb').MongoClient;
var db;
//连接数据库 并保存连接实例
exports.connect = function () {
mongo.connect('mongodb://127.0.0.1:27017/zhaopin', function (err, database) {
if(err){
return;
}
console.log('connected to mongo');
db = database;
})
}
//关闭数据库操作
exports.disconnect = function () {
db.close();
}
//利用update的upsert参数去重保存
exports.saveJobInfo = function (jobTitle,company,bonus,salary,citylocation,publishDate,natureOfWork,exp,requiredNum,jobType,jobDesc,workplaceLocation) {
var job = {
jobTitle: jobTitle,
company: company,
bonus: bonus,
salary: salary,
cityLocation: citylocation,
publishDate: publishDate,
natureOfWork: natureOfWork,
expReq: exp,
numReq: requiredNum,
jobType: jobType,
jobDesc: jobDesc,
workPlaceLocation: workplaceLocation,
};
//upsert参数:表示在找不到可以更新的对象记录的时候,插入这条新记录
var options = {
upsert: true
};
var collection = db.collection('jobs');
collection.updateOne(job, {$set:{jobTitle:job.jobTitle}}, options, function (err, res) {
if(err){
console.log(err);
}else {
console.log('insert success');
}
})
}
技术问题
1.mongodb去重插入数据
先前使用mongoose模块,没有去重插入的功能,当时采用先查找再插入的方法,代码比较复杂冗余。
在老师的代码中发现了mongodb这个模块,语法使用上更类似终端操作mongodb,个人感觉比mongoose好用
改用mongodb模块操作mongodb数据库,使用upsert
db.collection.updateOne(data, {$set:{key:value}, {upsert:true},function(err, res));
2.适时sleep,以防屏蔽
每个抓取页面函数同步执行,间隔一秒,连续三次数据抓取到3700条左右,失去服务器响应。
解决:每爬取3000个网页,使用setTimeout()函数,进入一小段较长的休眠(60s)。
结果:一共抓取11800条数据左右,总的休眠等待时间为950s左右。大约半小时运行完毕....吧。
3.如何重复查询
代码逻辑:
抓取不同类别的工作招聘首页的地址url(因总类别的数据无法完全显示,因此分类抓取)
==> 获取单个类别下的所有页面(page=1~最末页)
==> 获取单个招聘的单独页url
==> 抓取单个招聘的页面信息
以上逻辑包含在一个async.waterfall中,因此每次都是从头开始执行,无法跳过无效数据
解决: 比较简便的还没有想到
一个替代方法,把每个招聘详情的url存入数据库,每使用一条删除对应记录。
写在最后:
通过这一个月多来的简单爬虫实践,重新回顾理解了上学期在nodejs中的一些问题,比如异步机制、以及get/post请求。
分享一个最近才终于get到的梗。

新的一周整理一下抓取到的数据,vue简单实例,还有填一填我的机票坑……