前端社团程序员

智联招聘爬虫——学习笔记(1).md

2017-04-09  本文已影响113人  ccminn

2017.4.2-2017.4.5

在这个星期中,运用了上一个机票爬虫中学到的思路和技术点,爬取了智联招聘中互联网工作招聘的信息。

准备工作:

软件工程师类别中的第一页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  
通过chrome控制台中的network标签查看获取方式 智联招聘主页的页面结构 页码栏

主要编程思路:

瀑布流控制
------查询每一个类别的互联网工作的Url(通过导航栏中的a标签href属性获得)
------------获取单个类别下分页地址(即仅查询参数中p值改变的url)
------------------获取每一个单类单个分页中的60条招聘信息详情页的url(同样通过a标签href属性获得)
------------------------逐个抓取招聘信息详情页的内容

使用的工具

笔记导航:

代码分析

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简单实例,还有填一填我的机票坑……

上一篇 下一篇

猜你喜欢

热点阅读