手把手教你寫帶登入的NodeJS爬蟲+資料展示

Teal發表於2018-02-20

更新:立馬金庫在4月23日就停止在平臺上售賣,所以之後的是沒有資料的

其實在早之前,就做過立馬理財的銷售額統計,只不過是用前端js寫的,需要在首頁的console除錯皮膚裡貼上一段程式碼執行,點選這裡。主要是通過定時爬取https://www.lmlc.com/s/web/home/user_buying非同步介面來獲取資料。然後通過一定的排重演算法來獲取最終的資料。但是這樣做有以下缺點:

  1. 程式碼只能在瀏覽器視窗下執行,關閉瀏覽器或者電腦就失效了
  2. 只能爬取一個頁面的資料,不能整合其他頁面的資料
  3. 爬取的資料無法儲存到本地
  4. 上面的非同步介面資料會部分過濾,導致我們的排重演算法失效

由於最近學習了node爬蟲相關知識,我們可以在後臺自己模擬請求,爬取頁面資料。並且我開通了阿里雲伺服器,可以把程式碼放到雲端跑。這樣,1、2、3都可以解決。4是因為之前不知道這個ajax介面是每三分鐘更新一次,這樣我們可以根據這個來排重,確保資料不會重複。說到爬蟲,大家想到的比較多的還是python,確實python有Scrapy等成熟的框架,可以實現很強大的爬取功能。但是node也有自身的優點,憑藉強大的非同步特性,可以很輕鬆的實現高效的非同步併發請求,節省cpu的開銷。其實node爬蟲還是比較簡單的,下面我們就來分析整個爬蟲爬取的流程和最終如何展示資料的。

線上地址

一、爬蟲流程

我們最終的目標是實現爬取立馬理財每日的銷售額,並知道賣了哪些產品,每個產品又被哪些使用者在什麼時間點買的。首先,介紹下爬蟲爬取的主要步驟:

1. 結構分析

我們要爬取頁面的資料,第一步當然是要先分析清楚頁面結構,要爬哪些頁面,頁面的結構是怎樣的,需不需要登入;有沒有ajax介面,返回什麼樣的資料等。

2. 資料抓取

分析清楚要爬取哪些頁面和ajax,就要去抓取資料了。如今的網頁的資料,大體分為同步頁面和ajax介面。同步頁面資料的抓取就需要我們先分析網頁的結構,python抓取資料一般是通過正規表示式匹配來獲取需要的資料;node有一個cheerio的工具,可以將獲取的頁面內容轉換成jquery物件,然後就可以用jquery強大的dom API來獲取節點相關資料, 其實大家看原始碼,這些API本質也就是正則匹配。ajax介面資料一般都是json格式的,處理起來還是比較簡單的。

3. 資料儲存

抓取的資料後,會做簡單的篩選,然後將需要的資料先儲存起來,以便後續的分析處理。當然我們可以用MySQL和Mongodb等資料庫儲存資料。這裡,我們為了方便,直接採用檔案儲存。

4. 資料分析

因為我們最終是要展示資料的,所以我們要將原始的資料按照一定維度去處理分析,然後返回給客戶端。這個過程可以在儲存的時候去處理,也可以在展示的時候,前端傳送請求,後臺取出儲存的資料再處理。這個看我們要怎麼展示資料了。

5. 結果展示

做了這麼多工作,一點展示輸出都沒有,怎麼甘心呢?這又回到了我們的老本行,前端展示頁面大家應該都很熟悉了。將資料展示出來才更直觀,方便我們分析統計。

二、爬蟲常用庫介紹

1. Superagent

Superagent是個輕量的的http方面的庫,是nodejs裡一個非常方便的客戶端請求代理模組,當我們需要進行get、post、head等網路請求時,嘗試下它吧。

2. Cheerio

Cheerio大家可以理解成一個 Node.js 版的 jquery,用來從網頁中以 css selector 取資料,使用方式跟 jquery 一模一樣。

3. Async

Async是一個流程控制工具包,提供了直接而強大的非同步功能mapLimit(arr, limit, iterator, callback),我們主要用到這個方法,大家可以去看看官網的API。

4. arr-del

arr-del是我自己寫的一個刪除陣列元素方法的工具。可以通過傳入待刪除陣列元素index組成的陣列進行一次性刪除。

5. arr-sort

arr-sort是我自己寫的一個陣列排序方法的工具。可以根據一個或者多個屬性進行排序,支援巢狀的屬性。而且可以再每個條件中指定排序的方向,並支援傳入比較函式。

三、頁面結構分析

先屢一下我們爬取的思路。立馬理財線上的產品主要是定期和立馬金庫(最新上線的光大銀行理財產品因為手續比較麻煩,而且起投金額高,基本沒人買,這裡不統計)。定期我們可以爬取理財頁的ajax介面:https://www.lmlc.com/web/product/product_list?pageSize=10&pageNo=1&type=0。(update: 定期近期沒貨,可能看不到資料,可以看1月19號以前的)資料如下圖所示:

理財頁ajax介面資料
理財頁ajax介面資料

這裡包含了所有線上正在銷售的定期產品,ajax資料只有產品本身相關的資訊,比如產品id、籌集金額、當前銷售額、年化收益率、投資天數等,並沒有產品被哪些使用者購買的資訊。所以我們需要帶著id引數去它的產品詳情頁爬取,比如立馬聚財-12月期HLB01239511。詳情頁有一欄投資記錄,裡邊包含了我們需要的資訊,如下圖所示:

詳情頁投資記錄
詳情頁投資記錄

但是,詳情頁需要我們在登入的狀態下才可以檢視,這就需要我們帶著cookie去訪問,而且cookie是有有效期限制的,如何保持我們cookie一直在登入態呢?請看後文。

其實立馬金庫也有類似的ajax介面:https://www.lmlc.com/web/product/product_list?pageSize=10&pageNo=1&type=1,但是裡邊的相關資料都是寫死的,沒有意義。而且金庫的詳情頁也沒有投資記錄資訊。這就需要我們爬取一開始說的首頁的ajax介面:https://www.lmlc.com/s/web/home/user_buying。但是後來才發現這個介面是三分鐘更新一次,就是說後臺每隔三分鐘向伺服器請求一次資料。而一次是10條資料,所以如果在三分鐘內,購買產品的記錄數超過10條,資料就會有遺漏。這是沒有辦法的,所以立馬金庫的統計資料會比真實的偏少。

四、爬蟲程式碼分析

1. 獲取登入cookie

因為產品詳情頁需要登入,所以我們要先拿到登入的cookie才行。getCookie方法如下:

function getCookie() {
    superagent.post('https://www.lmlc.com/user/s/web/logon')
        .type('form')
        .send({
            phone: phone,
            password: password,
            productCode: "LMLC",
            origin: "PC"
        })
        .end(function(err, res) {
            if (err) {
                handleErr(err.message);
                return;
            }
            cookie = res.header['set-cookie']; //從response中得到cookie
            emitter.emit("setCookeie");
        })
}
複製程式碼

phone和password引數是從命令列裡傳進來的,就是立馬理財用手機號登入的賬號和密碼。我們用superagent去模擬請求立馬理財登入介面:https://www.lmlc.com/user/s/web/logon。傳入相應的引數,在回撥中,我們拿到header的set-cookie資訊,併發出一個setCookeie事件。因為我們設定了監聽事件:emitter.on("setCookie", requestData),所以一旦獲取cookie,我們就會去執行requestData方法。

2. 理財頁ajax的爬取

requestData方法的程式碼如下:

function requestData() {
    superagent.get('https://www.lmlc.com/web/product/product_list?pageSize=100&pageNo=1&type=0')
    .end(function(err,pres){
        // 常規的錯誤處理
        if (err) {
            handleErr(err.message);
            return;
        }
        // 在這裡清空資料,避免一個檔案被同時寫入
        if(clearProd){
            fs.writeFileSync('data/prod.json', JSON.stringify([]));
            clearProd = false;
        }
        let addData = JSON.parse(pres.text).data;
        let formatedAddData = formatData(addData.result);
        let pageUrls = [];
        if(addData.totalPage > 1){
            handleErr('產品個數超過100個!');
            return;
        }
        for(let i=0,len=addData.result.length; i<len; i++){
            if(+new Date() < addData.result[i].buyStartTime){
                if(preIds.indexOf(addData.result[i].id) == -1){
                    preIds.push(addData.result[i].id);
                    setPreId(addData.result[i].buyStartTime, addData.result[i].id);
                }
            }else{
                pageUrls.push('https://www.lmlc.com/web/product/product_detail.html?id=' + addData.result[i].id);
            }
        }
        function setPreId(time, id){
            cache[id] = setInterval(function(){
                if(time - (+new Date()) < 1000){
                    // 預售產品開始搶購,直接修改爬取頻次為1s,防止丟失資料
                    clearInterval(cache[id]);
                    clearInterval(timer);
                    delay = 1000;
                    timer = setInterval(function(){
                        requestData();
                    }, delay);
                    // 同時刪除id記錄
                    let index = preIds.indexOf(id);
                    sort.delArrByIndex(preIds, [index]);
                }
            }, 1000)
        }
        // 處理售賣金額資訊
        let oldData = JSON.parse(fs.readFileSync('data/prod.json', 'utf-8'));
        for(let i=0, len=formatedAddData.length; i<len; i++){
            let isNewProduct = true;
            for(let j=0, len2=oldData.length; j<len2; j++){
                if(formatedAddData[i].productId === oldData[j].productId){
                    isNewProduct = false;
                }
            }
            if(isNewProduct){
                oldData.push(formatedAddData[i]);
            }
        }
        fs.writeFileSync('data/prod.json', JSON.stringify(oldData));
        let time = (new Date()).format("yyyy-MM-dd hh:mm:ss");
        console.log((`理財列表ajax介面爬取完畢,時間:${time}`).warn);
        if(!pageUrls.length){
            delay = 32*1000;
            clearInterval(timer);
            timer = setInterval(function(){
                requestData();
            }, delay);
            return
        }
        getDetailData();
    });
}
複製程式碼

程式碼很長,getDetailData函式程式碼後面分析。

請求的ajax介面是個分頁介面,因為一般在售的總產品數不會超過10條,我們這裡設定引數pageSize為100,這樣就可以一次性獲取所有產品。

clearProd是全域性reset訊號,每天0點整的時候,會清空prod(定期產品)和user(首頁使用者)資料。

因為有時候產品較少會採用搶購的方式,比如每天10點,這樣在每天10點的時候資料會更新很快,我們必須要增加爬取的頻次,以防丟失資料。所以針對預售產品即buyStartTime大於當前時間,我們要記錄下,並設定計時器,當開售時,調整爬取頻次為1次/秒,見setPreId方法。

如果沒有正在售賣的產品,即pageUrls為空,我們將爬取的頻次設定為最大32s。

requestData函式的這部分程式碼主要記錄下是否有新產品,如果有的話,新建一個物件,記錄產品資訊,push到prod陣列裡。prod.json資料結構如下:

[{
  "productName": "立馬聚財-12月期HLB01230901",
  "financeTotalAmount": 1000000,
  "productId": "201801151830PD84123120",
  "yearReturnRate": 6.4,
  "investementDays": 364,
  "interestStartTime": "2018年01月23日",
  "interestEndTime": "2019年01月22日",
  "getDataTime": 1516118401299,
  "alreadyBuyAmount": 875000,
  "records": [
  {
    "username": "劉**",
    "buyTime": 1516117093472,
    "buyAmount": 30000,
    "uniqueId": "劉**151611709347230,000元"
  },
  {
    "username": "劉**",
    "buyTime": 1516116780799,
    "buyAmount": 50000,
    "uniqueId": "劉**151611678079950,000元"
  }]
}]
複製程式碼

是一個物件陣列,每個物件表示一個新產品,records屬性記錄著售賣資訊。

3. 產品詳情頁的爬取

我們再看下getDetailData的程式碼:

function getDetailData(){
    // 請求使用者資訊介面,來判斷登入是否還有效,在產品詳情頁判斷麻煩還要造成五次登入請求
    superagent
        .post('https://www.lmlc.com/s/web/m/user_info')
        .set('Cookie', cookie)
        .end(function(err,pres){
        // 常規的錯誤處理
        if (err) {
            handleErr(err.message);
            return;
        }
        let retcode = JSON.parse(pres.text).retcode;
        if(retcode === 410){
            handleErr('登陸cookie已失效,嘗試重新登陸...');
            getCookie();
            return;
        }
        var reptileLink = function(url,callback){
            // 如果爬取頁面有限制爬取次數,這裡可設定延遲
            console.log( '正在爬取產品詳情頁面:' + url);
            superagent
                .get(url)
                .set('Cookie', cookie)
                .end(function(err,pres){
                    // 常規的錯誤處理
                    if (err) {
                        handleErr(err.message);
                        return;
                    }
                    var $ = cheerio.load(pres.text);
                    var records = [];
                    var $table = $('.buy-records table');
                    if(!$table.length){
                        $table = $('.tabcontent table');
                    }
                    var $tr = $table.find('tr').slice(1);
                    $tr.each(function(){
                        records.push({
                            username: $('td', $(this)).eq(0).text(),
                            buyTime: parseInt($('td', $(this)).eq(1).attr('data-time').replace(/,/g, '')),
                            buyAmount: parseFloat($('td', $(this)).eq(2).text().replace(/,/g, '')),
                            uniqueId: $('td', $(this)).eq(0).text() + $('td', $(this)).eq(1).attr('data-time').replace(/,/g, '') + $('td', $(this)).eq(2).text()
                        })
                    });
                    callback(null, {
                        productId: url.split('?id=')[1],
                        records: records
                    });
                });
        };
        async.mapLimit(pageUrls, 10 ,function (url, callback) {
          reptileLink(url, callback);
        }, function (err,result) {
            let time = (new Date()).format("yyyy-MM-dd hh:mm:ss");
            console.log(`所有產品詳情頁爬取完畢,時間:${time}`.info);
            let oldRecord = JSON.parse(fs.readFileSync('data/prod.json', 'utf-8'));
            let counts = [];
            for(let i=0,len=result.length; i<len; i++){
                for(let j=0,len2=oldRecord.length; j<len2; j++){
                    if(result[i].productId === oldRecord[j].productId){
                        let count = 0;
                        let newRecords = [];
                        for(let k=0,len3=result[i].records.length; k<len3; k++){
                            let isNewRec = true;
                            for(let m=0,len4=oldRecord[j].records.length; m<len4; m++){
                                if(result[i].records[k].uniqueId === oldRecord[j].records[m].uniqueId){
                                    isNewRec = false;
                                }
                            }
                            if(isNewRec){
                                count++;
                                newRecords.push(result[i].records[k]);
                            }
                        }
                        oldRecord[j].records = oldRecord[j].records.concat(newRecords);
                        counts.push(count);
                    }
                }
            }
            let oldDelay = delay;
            delay = getNewDelay(delay, counts);
            function getNewDelay(delay, counts){
                let nowDate = (new Date()).toLocaleDateString();
                let time1 = Date.parse(nowDate + ' 00:00:00');
                let time2 = +new Date();
                // 根據這次更新情況,來動態設定爬取頻次
                let maxNum = Math.max(...counts);
                if(maxNum >=0 && maxNum <= 2){
                    delay = delay + 1000;
                }
                if(maxNum >=8 && maxNum <= 10){
                    delay = delay/2;
                }
                // 每天0點,prod資料清空,排除這個情況
                if(maxNum == 10 && (time2 - time1 >= 60*1000)){
                    handleErr('部分資料可能丟失!');
                }
                if(delay <= 1000){
                    delay = 1000;
                }
                if(delay >= 32*1000){
                    delay = 32*1000;
                }
                return delay
            }
            if(oldDelay != delay){
                clearInterval(timer);
                timer = setInterval(function(){
                    requestData();
                }, delay);
            }
            fs.writeFileSync('data/prod.json', JSON.stringify(oldRecord));
        })
    });
}
複製程式碼

我們先去請求使用者資訊介面,來判斷登入是否還有效,因為在產品詳情頁判斷麻煩還要造成五次登入請求。帶cookie請求很簡單,在post後面set下我們之前得到的cookie即可:.set('Cookie', cookie)。如果後臺返回的retcode為410表示登入的cookie已失效,需要重新執行getCookie()。這樣就能保證爬蟲一直在登入狀態。

async的mapLimit方法,會將pageUrls進行併發請求,一次併發量為10。對於每個pageUrl會執行reptileLink方法。等所有的非同步執行完畢後,再執行回撥函式。回撥函式的result引數是每個reptileLink函式返回資料組成的陣列。

reptileLink函式是獲取產品詳情頁的投資記錄列表資訊,uniqueId是由已知的username、buyTime、buyAmount引數組成的字串,用來排重的。

async的回撥主要是將最新的投資記錄資訊寫入對應的產品物件裡,同時生成了counts陣列。counts陣列是每個產品這次爬取新增的售賣記錄個陣列成的陣列,和delay一起傳入getNewDelay函式。getNewDelay動態調節爬取頻次,counts是調節delay的唯一依據。delay過大可能產生資料丟失,過小會增加伺服器負擔,可能會被管理員封ip。這裡設定delay最大值為32,最小值為1。

4. 首頁使用者ajax爬取

先上程式碼:

function requestData1() {
    superagent.get(ajaxUrl1)
    .end(function(err,pres){
        // 常規的錯誤處理
        if (err) {
            handleErr(err.message);
            return;
        }
        let newData = JSON.parse(pres.text).data;
        let formatNewData = formatData1(newData);
        // 在這裡清空資料,避免一個檔案被同時寫入
        if(clearUser){
            fs.writeFileSync('data/user.json', '');
            clearUser = false;
        }
        let data = fs.readFileSync('data/user.json', 'utf-8');
        if(!data){
            fs.writeFileSync('data/user.json', JSON.stringify(formatNewData));
            let time = (new Date()).format("yyyy-MM-dd hh:mm:ss");
            console.log((`首頁使用者購買ajax爬取完畢,時間:${time}`).silly);
        }else{
            let oldData = JSON.parse(data);
            let addData = [];
            // 排重演算法,如果uniqueId不一樣那肯定是新生成的,否則看時間差如果是0(三分鐘內請求多次)或者三分鐘則是舊資料
            for(let i=0, len=formatNewData.length; i<len; i++){
                let matchArr = [];
                for(let len2=oldData.length, j=Math.max(0,len2 - 20); j<len2; j++){
                    if(formatNewData[i].uniqueId === oldData[j].uniqueId){
                        matchArr.push(j);
                    }
                }
                if(matchArr.length === 0){
                    addData.push(formatNewData[i]);
                }else{
                    let isNewBuy = true;
                    for(let k=0, len3=matchArr.length; k<len3; k++){
                        let delta = formatNewData[i].time - oldData[matchArr[k]].time;
                        if(delta == 0 || (Math.abs(delta - 3*60*1000) < 1000)){
                            isNewBuy = false;
                            // 更新時間,這樣下一次判斷還是三分鐘
                            oldData[matchArr[k]].time = formatNewData[i].time;
                        }
                    }
                    if(isNewBuy){
                        addData.push(formatNewData[i]);
                    }
                }
            }
            fs.writeFileSync('data/user.json', JSON.stringify(oldData.concat(addData)));
            let time = (new Date()).format("yyyy-MM-dd hh:mm:ss");
            console.log((`首頁使用者購買ajax爬取完畢,時間:${time}`).silly);
        }
    });
}
複製程式碼

user.js的爬取和prod.js類似,這裡主要想說一下如何排重的。user.json資料格式如下:

[
{
  "payAmount": 5067.31,
  "productId": "jsfund",
  "productName": "立馬金庫",
  "productType": 6,
  "time": 1548489,
  "username": "鄭**",
  "buyTime": 1516118397758,
  "uniqueId": "5067.31jsfund鄭**"
}, {
  "payAmount": 30000,
  "productId": "201801151830PD84123120",
  "productName": "立馬聚財-12月期HLB01230901",
  "productType": 0,
  "time": 1306573,
  "username": "劉**",
  "buyTime": 1516117199684,
  "uniqueId": "30000201801151830PD84123120劉**"
}]
複製程式碼

和產品詳情頁類似,我們也生成一個uniqueId引數用來排除,它是payAmount、productId、username引數的拼成的字串。如果uniqueId不一樣,那肯定是一條新的記錄。如果相同那一定是一條新記錄嗎?答案是否定的。因為這個介面資料是三分鐘更新一次,而且給出的時間是相對時間,即資料更新時的時間減去購買的時間。所以每次更新後,即使是同一條記錄,時間也會不一樣。那如何排重呢?其實很簡單,如果uniqueId一樣,我們就判斷這個buyTime,如果buyTime的差正好接近180s,那麼幾乎可以肯定是舊資料。如果同一個人正好在三分鐘後購買同一個產品相同的金額那我也沒轍了,哈哈。

5. 零點整合資料

每天零點我們需要整理user.json和prod.json資料,生成最終的資料。程式碼:

let globalTimer = setInterval(function(){
    let nowTime = +new Date();
    let nowStr = (new Date()).format("hh:mm:ss");
    let max = nowTime;
    let min = nowTime - 24*60*60*1000;
    // 每天00:00分的時候寫入當天的資料
    if(nowStr === "00:00:00"){
        // 先儲存資料
        let prod = JSON.parse(fs.readFileSync('data/prod.json', 'utf-8'));
        let user = JSON.parse(fs.readFileSync('data/user.json', 'utf-8'));
        let lmlc = JSON.parse(JSON.stringify(prod));
        // 清空快取資料
        clearProd = true;
        clearUser = true;
        // 不足一天的不統計
        // if(nowTime - initialTime < 24*60*60*1000) return
        // 篩選prod.records資料
        for(let i=0, len=prod.length; i<len; i++){
            let delArr1 = [];
            for(let j=0, len2=prod[i].records.length; j<len2; j++){
                if(prod[i].records[j].buyTime < min || prod[i].records[j].buyTime >= max){
                    delArr1.push(j);
                }
            }
            sort.delArrByIndex(lmlc[i].records, delArr1);
        }
        // 刪掉prod.records為空的資料
        let delArr2 = [];
        for(let i=0, len=lmlc.length; i<len; i++){
            if(!lmlc[i].records.length){
                delArr2.push(i);
            }
        }
        sort.delArrByIndex(lmlc, delArr2);

        // 初始化lmlc裡的立馬金庫資料
        lmlc.unshift({
            "productName": "立馬金庫",
            "financeTotalAmount": 100000000,
            "productId": "jsfund",
            "yearReturnRate": 4.0,
            "investementDays": 1,
            "interestStartTime": (new Date(min)).format("yyyy年MM月dd日"),
            "interestEndTime": (new Date(max)).format("yyyy年MM月dd日"),
            "getDataTime": min,
            "alreadyBuyAmount": 0,
            "records": []
        });
        // 篩選user資料
        for(let i=0, len=user.length; i<len; i++){
            if(user[i].productId === "jsfund" && user[i].buyTime >= min && user[i].buyTime < max){
                lmlc[0].records.push({
                    "username": user[i].username,
                    "buyTime": user[i].buyTime,
                    "buyAmount": user[i].payAmount,
                });
            }
        }
        // 刪除無用屬性,按照時間排序
        lmlc[0].records.sort(function(a,b){return a.buyTime - b.buyTime});
        for(let i=1, len=lmlc.length; i<len; i++){
            lmlc[i].records.sort(function(a,b){return a.buyTime - b.buyTime});
            for(let j=0, len2=lmlc[i].records.length; j<len2; j++){
                delete lmlc[i].records[j].uniqueId
            }
        }
        // 爬取金庫收益,寫入前一天的資料,清空user.json和prod.json
        let dateStr = (new Date(nowTime - 10*60*1000)).format("yyyyMMdd");
        superagent
            .get('https://www.lmlc.com/web/product/product_list?pageSize=10&pageNo=1&type=1')
            .end(function(err,pres){
                // 常規的錯誤處理
                if (err) {
                    handleErr(err.message);
                    return;
                }
                var data = JSON.parse(pres.text).data;
                var rate = data.result[0].yearReturnRate||4.0;
                lmlc[0].yearReturnRate = rate;
                fs.writeFileSync(`data/${dateStr}.json`, JSON.stringify(lmlc));
        })
    }
}, 1000);
複製程式碼

globalTimer是個全域性定時器,每隔1s執行一次,當時間為00:00:00時,clearProd和clearUser全域性引數為true,這樣在下次爬取過程時會清空user.json和prod.json檔案。沒有同步清空是因為防止多處同時修改同一檔案報錯。取出user.json裡的所有金庫記錄,獲取當天金庫相關資訊,生成一條立馬金庫的prod資訊並unshift進prod.json裡。刪除一些無用屬性,排序陣列最終生成帶有當天時間戳的json檔案,如:20180101.json。

五、前端展示

1、整體思路

前端總共就兩個頁面,首頁和詳情頁,首頁主要展示實時銷售額、某一時間段內的銷售情況、具體某天的銷售情況。詳情頁展示某天的具體某一產品銷售情況。頁面有兩個入口,而且比較簡單,這裡我們採用gulp來打包壓縮構建前端工程。後臺用express搭建的,匹配到路由,從data資料夾裡取到資料再分析處理再返回給前端。

2、前端用到的元件介紹

  • Echarts Echarts是一個繪圖利器,百度公司不可多得的良心之作。能方便的繪製各種圖形,官網已經更新到4.0了,功能更加強大。我們這裡主要用到的是直方圖

  • DataTables Datatables是一款jquery表格外掛。它是一個高度靈活的工具,可以將任何HTML表格新增高階的互動功能。功能非常強大,有豐富的API,大家可以去官網學習。

  • Datepicker Datepicker是一款基於jquery的日期選擇器,需要的功能基本都有,主要樣式比較好看,比jqueryUI官網的Datepicker好看太多。

3、gulp配置

gulp配置比較簡單,程式碼如下:

var gulp = require('gulp');
var uglify = require("gulp-uglify");
var less = require("gulp-less");
var minifyCss = require("gulp-minify-css");
var livereload = require('gulp-livereload');
var connect = require('gulp-connect');
var minimist = require('minimist');
var babel = require('gulp-babel');

var knownOptions = {
  string: 'env',
  default: { env: process.env.NODE_ENV || 'production' }
};

var options = minimist(process.argv.slice(2), knownOptions);

// js檔案壓縮
gulp.task('minify-js', function() {
    gulp.src('src/js/*.js')
        .pipe(babel({
          presets: ['es2015']
        }))
        .pipe(uglify())
        .pipe(gulp.dest('dist/'));
});

// js移動檔案
gulp.task('move-js', function() {
    gulp.src('src/js/*.js')
        .pipe(babel({
          presets: ['es2015']
        }))
        .pipe(gulp.dest('dist/'))
        .pipe(connect.reload());
});

// less編譯
gulp.task('compile-less', function() {
    gulp.src('src/css/*.less')
        .pipe(less())
        .pipe(gulp.dest('dist/'))
        .pipe(connect.reload());
});

// less檔案編譯壓縮
gulp.task('compile-minify-css', function() {
    gulp.src('src/css/*.less')
        .pipe(less())
        .pipe(minifyCss())
        .pipe(gulp.dest('dist/'));
});

// html頁面自動重新整理
gulp.task('html', function () {
  gulp.src('views/*.html')
    .pipe(connect.reload());
});

// 頁面自動重新整理啟動
gulp.task('connect', function() {
    connect.server({
        livereload: true
    });
});

// 監測檔案的改動
gulp.task('watch', function() {
    gulp.watch('src/css/*.less', ['compile-less']);
    gulp.watch('src/js/*.js', ['move-js']);
    gulp.watch('views/*.html', ['html']);
});

// 啟用瀏覽器livereload友好提示
gulp.task('tip', function() {
    console.log('\n<----- 請用chrome瀏覽器開啟 http://localhost:5000 頁面,並啟用livereload外掛 ----->\n');
});

if (options.env === 'development') {
    gulp.task('default', ['move-js', 'compile-less', 'connect', 'watch', 'tip']);
}else{
    gulp.task('default', ['minify-js', 'compile-minify-css']);
}
複製程式碼

開發和生產環境都是將檔案打包到dist目錄。不同的是:開發環境只是編譯es6和less檔案;生產環境會再壓縮混淆。支援livereload外掛,在開發環境下,檔案改動會自動重新整理頁面。

後記

至此,一個完整的爬蟲就完成了。其實我覺得最需要花時間的是在分析頁面結構,處理資料還有解決各種問題,比如如何保持一直在登入狀態等。

本爬蟲程式碼只做研究學習用處,禁止用作任何商業分析。再說,統計的資料也不準確。

因為程式碼開源,希望大家照著程式碼去爬取其他網站,如果都拿立馬理財來爬,估計伺服器會承受不了的額。

歡迎大家star學習交流:線上地址 | github地址 | 我的部落格

相關文章