node.js 89行爬蟲爬取智聯招聘信息

寫在前面的話,html

   .......仍是不寫了,直接上效果圖。附上源碼地址 github.lonhonnode

clipboard.png
clipboard.png

ok, 正文開始,先列出用到的和require的東西:python

node.js,這個是必須的
request,然發送網絡請求更方便
bluebird,讓Promise更高效
cheerio,像jQuery同樣優雅的解析頁面
fs,讀寫本地文件
以前寫的代理ip的爬取結果,代理池

因爲本身的比較偏好數據方面,以前一直就想用python作一些爬蟲的東西,奈何一直糾結2.7仍是3.x(逃...git

上週在看慕課網上的node教程,就跟着課程敲了一次爬蟲,從慕課網上的課程開始入手,而後就開始了愉快的爬蟲之路。
這兩週的爬取路程以下:
慕課網全部課程含章節列表-->拉勾網招聘信息-->xiciIP的代理ip-->boss直聘招聘信息-->我的貼吧回帖記錄-->最後就是此次講的智聯招聘的爬蟲代碼。github

智聯其實一共寫了兩次,有興趣的能夠在源碼看看,初版的是回調版,只能一次一頁的爬取。如今講的是promise版(文件位置/zlzp/zlzp-pure.js),可以很好的利用node的異步,快速爬取多個頁面,寫的時候測試了一下,爬取30頁,每頁60條數據,耗時73s,這個結果主要受代理ip影響。web

"use strict";
var http = require('http')
var cheerio = require('cheerio')
var request = require('request')
var fs = require('fs')
var Promise = require('bluebird')//雖然原生已經支持,但bluebird效率更高
var iplist = require('../ip_http.json') //代理池

//發送請求,成功寫入文件,失敗換代理
var getHtml = function (url,ipac,ppp) {
    return new Promise(function(resolve,reject){
        if (ipac >= iplist.length){        
            console.log('page:'+ppp+'all died'); //代理用完,取消當前頁面ppp的請求
            reject(url,false);
        }
        let prox = {    //設置代理
            url: url,
            proxy: 'http://' + iplist[ipac],
            timeout: 5000,
            headers: {
                'Host': 'sou.zhaopin.com',
                'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.96 Safari/537.36'
            }
        };
        request(prox, function (err, res, body) {
            if (err) {
                reject(url)//失敗,回傳當前請求的頁面url
            } else {
                resolve(body, url)//成功回傳html和url
            }
        })
    }) 
}
//解析doc
function filterHtml(html,p,noww){
    let res = [];//存放結果集
    var $ = cheerio.load(html);
    if($('title').text().indexOf('招聘') === -1) {    //根據title判斷是否被代理重定向
        iplist.splice(noww[2],1);   //刪除假代理。
        return lhlh(noww[0],noww[1],noww[2]+1);
    }
    $('.newlist').each(function(item){
        res.push({
            zwmc: $(this).find('.zwmc').find('div').text().replace(/\s+/g,"").replace(/\n/g,''),
            gsmc: $(this).find('.gsmc').find('a').eq(0).text().replace(/\s+/g,"").replace(/\n/g,''),
            zwyx: $(this).find('.zwyx').text().replace(/\s+/g,"").replace(/\n/g,''),
            gzdd: $(this).find('.gzdd').text().replace(/\s+/g,"").replace(/\n/g,''),
            gxsj: $(this).find('.gxsj').find('span').text().replace(/\s+/g,"").replace(/\n/g,'')
        })
    })
    res.shift();//刪除表頭行
    if(res.length < 60){
        return lhlh(noww[0],noww[1],noww[2]+1);
    }
    return creatfile(res,p);
}
//寫入本地
function creatfile(list,page) {
    var ttxt = 'page:' + page + '\r\n';//每頁標題
    list.forEach(function(el) {  //遍歷數據爲文本
        ttxt += el.zwmc + ','+ el.gsmc + ','+ el.zwyx + ','+ el.gzdd + ','+ el.gxsj + '\r\n';
    });
    fs.appendFile('./' + 'zlzp-pure.txt', 'page:'+ttxt+'\r\n' , 'utf-8', function (err) {
        if (!err) {
            let currTime = Math.round((Date.parse(new Date()) - startTime) / 1000); 
            console.log('page:' + page +' is ok:' +list.length + ',spend:' + currTime + 's' ); // page:1 is ok
        }
    })
}

//請求封裝爲promise
function lhlh(url,page,ipac){
    getHtml(url,ipac,page).then((html,oldurl)=>{
        let noww= [url,page,ipac]
        filterHtml(html,page,noww);
    })
    .catch((url,type = true)=>{
        if(type){
            ipac += 1;
            lhlh(url,page,ipac);
        }
    })
} 
var target = 'http://sou.zhaopin.com/jobs/searchresult.ashx?jl=%e6%88%90%e9%83%bd&kw=web%e5%89%8d%e7%ab%af&isadv=0&sg=8cd66893b0d14261bde1e33b154456f2&p=';
let ipacc = 0;
var startTime = Date.parse(new Date());
for(let i=1; i<31; i++){
    let ourl = target + i;
    lhlh(ourl, i, 0);
}

先貼出源碼,在線地址能夠在文首獲取chrome

如今說說本次爬蟲的流程數據庫

  1. 循環請求爬取的頁面,這裏經過target和循環變量i,拼裝請求連接ourl;這裏因爲請求的是http協議連接,因此用的http的代理,若是是https,則切換爲https代理池文件。json

  2. 進入lhlh方法,這裏是對實際發送網絡請求的getHtnl方法作一個Promise的調用,也是經過遞歸該方法實現代理ip的切換。promise

  3. getHtml方法,首先是對代理池是否用完作一個判斷,若是溢出則終止對當前頁面的爬取,而後是配置request的option+代理的設置,而後return一個promise

  4. filterHtml方法,對請求回來的頁面作解析,提取所需的數據

  5. createfile方法,實現數據的本地存儲

接下來具體解析

一、怎麼發送請求?

for(let i=1; i<31; i++){
    let ourl = target + i;
    lhlh(ourl, i, 0);
}

包括頭部的require、生成url。這裏由於智聯每次最新的結果只有30頁,全部循環30次
這裏使用循環發送request,由於request是一個異步操做,因此這裏須要將url和當前請求頁面page做爲參數傳出去.
在調用lhlh方法的時候還傳入了一個0,這是一個代理池的初始值。

2.lhlh方法作了什麼?
lhlh函數體內主要是對getHtml方法返回的Promise作成功和失敗的處理,邏輯上是:
  成功-->調用filterHtml,並傳入請求結果
  失敗-->根據type判斷異常狀況 ①切換代理,從新請求 ②代理用完,取消本頁請求
另外,對傳入進來的url、page、代理池下標作一個閉包,傳入每次的請求中,從而保證每次請求的可控性。

3.主角——getHtml方法,返回Promise
在一開始作一個判斷,代理池是否溢出,溢出就拋出reject。
生成請求option,主要配置了代理池和Headers,目的也是爲了解決網站的反爬。(代理ip的爬取在上級的ip.js中,自取)
接下來就是把請求發送出去,發送請求意味着有兩種結果:
  成功-->返回response,進行下一步解析
  失敗-->返回當前請求的url
4.filterHtml對response解析
這裏就須要結合頁面結構進行代碼的編寫了,先看看咱們要請求的頁面長什麼樣子:

clipboard.png

用chrome的開發工具能夠很容易看到招聘數據是放在一個class=「newlist」的table中,再結合cheerio,可以很優雅的對頁面中的dom進行提取,具體步驟就是遍歷table,取出數據,而後push到result中。

ps:①其實這裏還可以提升代碼質量和效率的,就是直接生成建立txt文件所需的文本,這樣就少了對數據的一次遍歷,可是爲了易於理解過程,仍是push到result中傳出了。
②紅框中的第一個table實際上是放表頭的,並無實際數據,因此代碼中用了result.shift()刪除了第一個元素

5.本地保存爬回來的數據

對傳入的參數也就是上一步的result進行遍歷,生成建立txt文件所需的字符串。

經過fs.appendFile方法就能夠建立本地文件了 ,格式爲:fs.appendFile 具體的用法能夠百度一下。
最後在生成txt文件後打印了當前頁流程走完的提示信息和所消耗的時間。
PS: ①.這裏其實應該存入本地數據庫or生成表格文件(將數據結構化),可是因爲須要搭建數據庫環境or引入新的模塊,故生成的是txt文件。另在createflie中遍歷生成ttxt時候,我在不一樣數據之間插入的分隔符「,」,這樣能夠方便的導入到表格or數據庫中
②fs.appendFile之類的文件操做是異步的。從實際狀況考慮,因爲每次寫入的內容不一樣和磁盤讀寫性能的影響,也註定fs的文件操做是一個異步過程。

我的總結

若是你看到這裏了,相信你對本文感興趣,能夠的話來個 star
promise的目的1:在異步操做結束把異步結果返回,讓代碼更扁平,好比:

function c(val){
  //本函數功能須要b的返回值才能實現
}
function b(){  放一些異步操做,返回 Promise   }
function a(){
    調用異步方法b
    b().then(function(val:resolve的返回值){
        這時候就能夠直接使用c(val)
        使用原來的回調函數就必須把c方法放在async方法中執行,當回調過多的時候函數調用就會變成a(b(c(d(e(f(...)))))),層層嵌套
        而使用Promise函數調用就能夠扁平爲a()->b()->c()...,特別是當理解了Promise的運行步驟後,
    })
}

promise缺點:性能上和回調函數比較會遜色一些,這也是本次爬蟲在node.js v-7.10.0完美實現promise的狀況下還引入bluebird的主要緣由。

閉包:閉包實現了面向對象中的封裝。
異步操做的時候經過閉包能夠獲取同一個變量,而不會改變其它線程在使用的變量,這也是js實現私有變量的
好比本次爬蟲中每次filterHtml解析網頁完成後的結果集res,若是放在外層,則會被正在運行的其它異步操做影響,致使傳入creatfile的res被影響,
再好比每次爬取的page,若是取for循環裏面的i,那最後獲得的是i最後的值,因此須要將i傳入方法,經過閉包實現每次輸出到txt文件的page是當前爬取的page。
當異步函數A、B同時在運行時,

異步A    異步B
00:01       A=1      
00:02                A=2
00:03       A===2
相關文章
相關標籤/搜索