node.js 爬取招聘信息分析各職業錢途(爬蟲+動態IP代理+數據可視化分析)

前前言

本文首發於 github blogjavascript

不想看爬蟲過程只想看職位錢途數據分析請看這裏:
前端招聘崗位分析
C++招聘崗位分析
JAVA招聘崗位分析
PHP招聘崗位分析
Python招聘崗位分析php

想看源碼或想本身爬一個請看這裏:本文github源碼html

前言

早在一年前大學校招期間,爲了充實下簡歷,就寫了個node爬蟲,惋惜當時能力有限,工程存在必定的侷限性,很差意思拿出來裝逼分享。 前端

一年過去了,如今能力依然有限,可是臉皮卻練厚了,因而就有了這篇文章。java

題綱

關於爬蟲,主流技術是用python,然而隨着node的出現,對於對python瞭解有限的前端同窗,用node來實現一個爬蟲也不失爲一個不錯的選擇。node

固然不管是python爬蟲仍是node爬蟲或者其餘品種的爬蟲,其實除了語言特性以外,其思路基本大同小異。下面我就爲你們詳細介紹下node爬蟲的具體思路與實現,內容大概以下:python

  • 爬前準備git

    • 選擇目標
    • 分析可收集數據與目標可爬取入口
  • 爬蟲github

    • 爬取JSON數據
    • 爬取HTML文檔,提取有用信息
    • Mongodb 數據存儲
    • 併發控制
    • 動態IP代理(防止IP被禁)
  • 數據可視化展現

爬前準備

選擇目標

既然要寫爬蟲,固然要爬一些利益相關的數據比較好玩啦。爬取招聘網站的招聘信息,來看看互聯網圈子裏各個工種的目前薪酬情況及其發展前景,想來是不錯的選擇。web

經我夜觀天下,掐指一算,就選拉勾網吧。

分析可收集數據

一個職位招聘信息,通常來講,咱們關注的重點信息會是:

  • 薪酬(毫無疑問,重中之重)
  • 工做城市
  • 學歷要求
  • 工做年限要求
  • 僱主公司
  • 公司領域
  • 公司規模

帶着想要收集的信息,首先,進入拉勾官網,搜索web前端崗位,能看到
clipboard.png

很好,咱們想要的信息基本都有了。

分析目標可爬取入口

PC端入口

F12 分析請求資源,可得https://www.lagou.com/jobs/positionAjax.json?needAddtionalResult=false&isSchoolJob=0
post 請求體

{
    first:false,
    pn:1,
    kd:`web前端`
}

響應JSON數據

clipboard.png

完美!!! 數據格式都已經幫咱們整理好了,直接爬就好了。

但,完美的數據總不會這麼輕易讓你獲得,經我用 nodepython,還有postman 攜帶瀏覽器所有header信息一一測試,均發現:
clipboard.png
好吧,此路不通。(此接口反爬蟲機制不明,有研究的大神請留言=_=)

所謂條條大路通羅馬,此路不通,咱繞路走。

移動端入口

通過一番探索,發現 拉勾移動端站點 空門大開!

提示: 通常有點技術含量的網站均可能會存在不一樣強度的反爬蟲機制,而通常其移動端站點的反爬蟲機制相對於PC站點較弱,是一個不錯的着手點。再不行的話,還能夠去其app端抓包分析是否存在想要的請求哦。

clipboard.png

GET請求: https://m.lagou.com/search.js...
響應信息:
clipboard.png

很好,雖然數據信息有點少,可是總算是一個能爬的接口了。

爬蟲

好了,分析也分析完了,如今正式設計爬蟲程序。

JSON數據爬取

  1. 首先,把請求的路徑與參數單獨抽離。

    let spider = {
        requestUrl : "http://m.lagou.com/search.json",
        query: {
            city: '',
            pageNum: '',
            job: '',
        },
        ...
    }
  2. 發出請求,此處的服務端構造請求使用 superagent,固然,用 request 等相似的包也能夠,並沒有限定。

    let spider = {
        ....
    /**
     * 發起單個請求
     * @return {<Promise<Array>> | <Promise<String>>} 請求成功resolve原始數據,不然reject
      **/
        request() {
            return new Promise((resolve,reject)=>{
                superagent
                .get(this.requestUrl)
                .query({
                    city: this.query.city,
                    pageNo: this.query.pageNum,
                    positionName: this.query.job
                }).end((err, res)=>{
                    let dataList = [];
                    if (err || !res || !res.ok) {
                        console.error(err);
                        reject('request failed!')
                    } else  {
                        dataList = res.body.content.data.page.result
                        if (dataList.length === 0) {
                            // 當請求結果數組長度爲0,即認爲已經到末頁,結束爬蟲
                            reject('finish');                     
                        } else {
                            resolve(dataList)
                        }
                    } 
                })
            })
        },
  3. 處理數據

    let spider = {
        ....
    /**
     * 處理爬取到的原始數據,提取出所需的數據
     * @param {<Array>} - companyList : 原始數據
     * @return {<Promise<Array>>} resolve處理過的數據
      **/
        handleCallbackData(companyList) {
           
            //處理數據
             let arr = companyList.map((item) => {
                let salary = item.salary.split('-');
                
                //工資兩種狀況:」10k以上「 or "10k-15k", 平均工資取中位數
                aveSalary = salary.length == 1 ? parseInt(salary[0])*1000 : (parseInt(salary[0]) + parseInt( salary[1] ) )*500;
    
                //過濾出所需數據
                return {
                    companyFullName: item.companyFullName,
                    positionId : item.positionId ,
                    salary:aveSalary ,
                    city:item.city ,
    
                    field: '',
                    companySize:'',
                    workYear:'' ,
                    qualification: '',
                }
            });
    
            return Promise.resolve(arr)
        }
  4. 保存數據,此處數據庫使用mongodbORM使用 moogoose

    save2db(jobList) {
        return new Promise((resolve, reject)=>{
            Job.create(jobList,function (err,product) {
                if (err) {
                    console.error(err.errmsg)
                    err.code == 11000 && resolve('丟棄重複數據')
                    reject(err);
                } else {
                    resolve("save data to database successfully")
                }
            })    
        })
    },

HTML 數據解析爬取

從上述的json數據其實咱們能夠看到,JSON返回的信息十分有限,那麼咱們須要爬取更多的信息,就須要在招聘詳情頁解析 html 後提取出所需的信息
隨便打開一個移動端的招聘詳情頁https://m.lagou.com/jobs/3638173.html,目測出url結構很簡單,就是jobs/{{positionId}}.html
clipboard.png

從詳情頁中能夠找出 JSON 數據中缺乏的數據項:工做年限要求,學歷要求,僱主公司領域,僱主公司融資狀況,僱主公司規模大小。

爬取方法和上述爬取 JSON 數據相差無幾,主要差異就是數據解析部分,這裏須要用到cherrio來解析 爬取到的HTML,從而更簡單地提取必要信息。

handleCallbackData({res, jobId}) {
        var $ = cheerio.load(res.text);

        let workYear = $('#content > div.detail > div.items > span.item.workyear > span').text(),
            qualification = $('#content > div.detail > div.items > span.item.education').text().trim(),
            field = $('#content > div.company.activeable > div > div > p').text().trim().split(/\s*\/\s*/)[0]
            companySize = $('#content > div.company.activeable > div > div > p').text().trim().split(/\s*\/\s*/)[2];

        /* 若是這四項數據都沒有提取到,頗有多是被拉勾的反爬蟲機制攔截了 */
        if ( !(workYear || qualification || field || companySize) ) {
            console.log(res.text)
            return Promise.reject({code:-1, msg:'wrong response!', jobId});
        }

        return {
            id: jobId,
            jobInfo: {
                workYear,
                qualification,
                field,
                // financeStage,
                companySize,
            }
        }
    },

併發控制

作過爬蟲的都知道,爬蟲的請求併發量是必需要作的,爲何要控制併發?

  1. 控制其爬取頻率,以避免沒爬幾個就網站被封IP了。
  2. 控制爬蟲應用運行內存,不控制併發的話一會兒處理N個請求,內存分分鐘爆炸。

實現併發控制可使用npmasync.mapLimit,這裏爲了自由度更大我使用了本身實現的 15 行代碼實現併發控制

具體代碼以下:

let ids = [2213545,5332233, ...], // 招聘崗位詳情id列表
    limit = 10, // 併發數
    runningRequestNum = 0 , // 當前併發數
    count = 0; // 累計爬取數據項計數
    
mapLimit(ids, limit, async (jobId)=>{
    let requestUrl = `http://m.lagou.com/jobs/${jobId}.html?source=home_hot&i=home_hot-6` ;
    let delay = parseInt(Math.random() * 2000);

    let currentIndex = count++;
    runningRequestNum++

    await sleep( delay );  // 避免爬太快被封ip,休眠一兩秒
    
    let result = await spiderHTML.run({
                    requestUrl,
                    jobId,
                    proxyIp
                })
    console.log(`當前併發數`, runningRequestNum)
    runningRequestNum--
    
    return result;
}).then(mapResult => {
    // 併發控制下將 ids 所有迭代完畢
    // do something 
})



然而,即使嚴格控制了請求頻率,咱們仍是不可避免地中招了。

clipboard.png

對於反爬蟲措施比較暴躁的網站來講,一個IP爬取太過頻繁,被識別成機器爬蟲幾乎是不可避免的。

通常來說,咱們最簡單直接的方法就是:換IP。這個IP訪問頻率過高了被反爬攔截到,換個IP就好了嘛。

動態IP代理

單個IP爬蟲對於反爬較爲嚴厲的網站是走不通的。那麼咱們須要用到動態IP池,每次爬取時從IP池中拉取一個IP出來爬數據。

道理很簡單,
1秒內1個IP訪問了100個頁面,即使是單身20多年的手速也沒法企及。只能是機器爬蟲無疑。
但1秒內100個IP訪問100個頁面,平均每一個IP一秒內訪問了1個頁面,那基本不會被反爬幹掉

怎麼搭建動態IP池?

  1. 首先咱們得有一個IP源,動態IP池的補充都從這裏拉取,這個網上搜一下"免費代理IP"就有不少出來,選其中一個,收費的IP源比較穩定可靠,免費的就一分錢一分貨了。
  2. 其次,每次從IP源中拉取的IP都是沒法確認其是否可用的,咱們必須篩選一遍,提取出可用的IP。(PS: 此處和步驟4目的一直,若是IP源較爲可靠,能夠省略)
  3. 設計從IP池中拉取單個IP的策略,使得每一個IP使用頻率均勻,儘可能避免單個IP使用頻率太高而失效。
  4. 移除失效IP。儘管設計了拉取策略,但依舊不可避免某些IP失效,此時須要將其移出IP池廢棄。

動態IP池工做流程:

st=>start: 提取一個可用IP
e=>end: 根據策略返回一個可用IP
isEnought=>condition: 池中IP數量是否足夠
fetch=>operation: 從IP源拉取IP
valid=>operation: 篩選出有效IP並存入IP池
st->isEnought(yes)->e
isEnought(no,right)->fetch(right)->valid(right)->isEnought

具體實現代碼其實和上面的爬蟲差很少,無非就是爬崗位變成了爬IP而已,具體實現源碼在這,就不在這寫了。

數據可視化分析

咱們最終折騰爬蟲,無非就是想要看爬到的數據到底說明了什麼。
成功爬取了拉鉤網上多個招聘崗位的具體信息後,數據可視化並得出分析結果以下:

clipboard.png
從總體看,北上廣深杭這五個城市前端工程師招聘崗位,北京是遙遙領先,是深圳的兩倍,是廣州的三倍,其次到上海,深圳,杭州,廣州居末。

從需求量大概能夠看出,總體互聯網產業發達程度是 北 > 上 > 深 > 杭 > 廣

clipboard.png
由平均工資曲線圖能夠看到,每隔2K算一檔的話,北京一檔,上海一檔,杭州深圳一檔,空一檔,廣州吊車尾,杭州居然比深圳高了300,這就表明着深圳雖然招聘需求比杭州大,但二者薪酬待遇其實差很少。

從不一樣薪酬的招聘數量也能看出一些很大的區別,招聘提供薪資水平中,廣泛數量最多的是10k-20k這個水平,但,北京牛逼,招聘崗位60%以上都是20K以上的。咱們具體來看看,各個城市對高端人才(提供薪酬20k以上)的招聘比例,那就能夠看出明顯區別了:

  • 北京:招聘的薪資水平是"20k以上",大概是招聘總數的59.7%
  • 上海:招聘的薪資水平是"20k以上",大概是招聘總數的41.3%
  • 深圳:招聘的薪資水平是"20k以上",大概是招聘總數的29.2%
  • 杭州:招聘的薪資水平是"20k以上",大概是招聘總數的30.4%,和深圳相差不大
  • 廣州:招聘的薪資水平是"20k以上",大概是招聘總數的……10.4%。

clipboard.png
基本能夠看到一個明顯的趨勢,公司規模越大,能提供的薪酬越高,不差錢。
另外,從不一樣規模的公司的前端招聘數量來看,北京又一枝獨秀,大公司招聘需求很高。

但從全國來看,不一樣規模的公司(除了15人如下的)招聘數量基本在同一水平,基本說明:大公司少,可是每一個公司招聘的人多;小公司多,可是每一個公司招聘的人少。好像這是句廢話。

clipboard.png
從圖上看,工做經歷在1-5年的如今需求最旺盛,而且理所固然地,工做資歷越高,薪資越高。
其中3-5年的最吃香,廣州有點奇怪,1-3年的最吃香?綜合上面的多項數據,感受像是1-3年工資比3-5年低因此廣州互聯網公司多招1-3年

固然,這裏存在這一個倖存者誤差,拉勾上大部分的都是社招性質的招聘,而應屆生和1年經驗的大部分都跑校招去了吧,因此數量低也不出奇。

clipboard.png
移動互聯網佔據了大半壁江山,剩下之中,金融,電子商務,企業服務,數據服務在同一層次。另外,物聯網,智能硬件各有一招聘崗位,薪酬都是5K...嗯雖然說node如今也能夠作物聯網了(還別說,我還真的用node搞過硬件串口通訊Orz),可是終究不是主流技術,數據展現代表,前端基本與硬件絕緣。

薪酬待遇卻是都在同一水平上,「大數據」工資卻是一枝獨秀,可是數據量太少,參考價值不大。

總結:北京錢多機會多當之無愧第一檔;上海稍遜一籌;杭州深圳又低一籌;廣州真的是差了兩個身位。 而對於前端來講,北京 移動互聯網 大公司,錢多!坑多!速來!

相關文章
相關標籤/搜索