手把手教你寫帶登陸的NodeJS爬蟲+數據展現

更新:立馬金庫在4月23日就中止在平臺上售賣,因此以後的是沒有數據的css

其實在早以前,就作過立馬理財的銷售額統計,只不過是用前端js寫的,須要在首頁的console調試面板裏粘貼一段代碼執行,點擊這裏。主要是經過定時爬取https://www.lmlc.com/s/web/home/user_buying異步接口來獲取數據。而後經過必定的排重算法來獲取最終的數據。可是這樣作有如下缺點:html

  1. 代碼只能在瀏覽器窗口下運行,關閉瀏覽器或者電腦就失效了
  2. 只能爬取一個頁面的數據,不能整合其餘頁面的數據
  3. 爬取的數據沒法存儲到本地
  4. 上面的異步接口數據會部分過濾,致使咱們的排重算法失效

因爲最近學習了node爬蟲相關知識,咱們能夠在後臺本身模擬請求,爬取頁面數據。而且我開通了阿里雲服務器,能夠把代碼放到雲端跑。這樣,一、二、3均可以解決。4是由於以前不知道這個ajax接口是每三分鐘更新一次,這樣咱們能夠根據這個來排重,確保數據不會重複。說到爬蟲,你們想到的比較多的仍是python,確實python有Scrapy等成熟的框架,能夠實現很強大的爬取功能。可是node也有自身的優勢,憑藉強大的異步特性,能夠很輕鬆的實現高效的異步併發請求,節省cpu的開銷。其實node爬蟲仍是比較簡單的,下面咱們就來分析整個爬蟲爬取的流程和最終如何展現數據的。前端

線上地址node

1、爬蟲流程

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

1. 結構分析

咱們要爬取頁面的數據,第一步固然是要先分析清楚頁面結構,要爬哪些頁面,頁面的結構是怎樣的,需不須要登陸;有沒有ajax接口,返回什麼樣的數據等。jquery

2. 數據抓取

分析清楚要爬取哪些頁面和ajax,就要去抓取數據了。現在的網頁的數據,大致分爲同步頁面和ajax接口。同步頁面數據的抓取就須要咱們先分析網頁的結構,python抓取數據通常是經過正則表達式匹配來獲取須要的數據;node有一個cheerio的工具,能夠將獲取的頁面內容轉換成jquery對象,而後就能夠用jquery強大的dom API來獲取節點相關數據, 其實你們看源碼,這些API本質也就是正則匹配。ajax接口數據通常都是json格式的,處理起來仍是比較簡單的。git

3. 數據存儲

抓取的數據後,會作簡單的篩選,而後將須要的數據先保存起來,以便後續的分析處理。固然咱們能夠用MySQL和Mongodb等數據庫存儲數據。這裏,咱們爲了方便,直接採用文件存儲。es6

4. 數據分析

由於咱們最終是要展現數據的,因此咱們要將原始的數據按照必定維度去處理分析,而後返回給客戶端。這個過程能夠在存儲的時候去處理,也能夠在展現的時候,前端發送請求,後臺取出存儲的數據再處理。這個看咱們要怎麼展現數據了。github

5. 結果展現

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

2、爬蟲經常使用庫介紹

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是我本身寫的一個數組排序方法的工具。能夠根據一個或者多個屬性進行排序,支持嵌套的屬性。並且能夠再每一個條件中指定排序的方向,並支持傳入比較函數。

3、頁面結構分析

先屢一下咱們爬取的思路。立馬理財線上的產品主要是按期和立馬金庫(最新上線的光大銀行理財產品由於手續比較麻煩,並且起投金額高,基本沒人買,這裏不統計)。按期咱們能夠爬取理財頁的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條,數據就會有遺漏。這是沒有辦法的,因此立馬金庫的統計數據會比真實的偏少。

4、爬蟲代碼分析

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。

5、前端展現

一、總體思路

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

二、前端用到的組件介紹

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

  • DataTables Datatables是一款jquery表格插件。它是一個高度靈活的工具,能夠將任何HTML表格添加高級的交互功能。功能很是強大,有豐富的API,你們能夠去官網學習。

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

三、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地址 | 個人博客

相關文章
相關標籤/搜索