摘要:可以通過傳入待刪除數組元素組成的數組進行一次性刪除。如果后臺返回的為表示登錄的已失效,需要重新執行。等所有的異步執行完畢后,再執行回調函數。回調函數的參數是每個函數返回數據組成的數組。
其實在早之前,就做過立馬理財的銷售額統計,只不過是用前端js寫的,需要在首頁的console調試面板里粘貼一段代碼執行,點擊這里。主要是通過定時爬取https://www.lmlc.com/s/web/home/user_buying異步接口來獲取數據。然后通過一定的排重算法來獲取最終的數據。但是這樣做有以下缺點:
代碼只能在瀏覽器窗口下運行,關閉瀏覽器或者電腦就失效了
只能爬取一個頁面的數據,不能整合其他頁面的數據
爬取的數據無法存儲到本地
上面的異步接口數據會部分過濾,導致我們的排重算法失效
由于最近學習了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. SuperagentSuperagent是個輕量的的http方面的庫,是nodejs里一個非常方便的客戶端請求代理模塊,當我們需要進行get、post、head等網絡請求時,嘗試下它吧。
2. CheerioCheerio大家可以理解成一個 Node.js 版的 jquery,用來從網頁中以 css selector 取數據,使用方式跟 jquery 一模一樣。
3. AsyncAsync是一個流程控制工具包,提供了直接而強大的異步功能mapLimit(arr, limit, iterator, callback),我們主要用到這個方法,大家可以去看看官網的API。
4. arr-delarr-del是我自己寫的一個刪除數組元素方法的工具。可以通過傳入待刪除數組元素index組成的數組進行一次性刪除。
5. arr-sortarr-sort是我自己寫的一個數組排序方法的工具。可以根據一個或者多個屬性進行排序,支持嵌套的屬性。而且可以再每個條件中指定排序的方向,并支持傳入比較函數。
三、頁面結構分析先屢一下我們爬取的思路。立馬理財線上的產品主要是定期和立馬金庫(最新上線的光大銀行理財產品因為手續比較麻煩,而且起投金額高,基本沒人買,這里不統計)。定期我們可以爬取理財頁的ajax接口:https://www.lmlc.com/web/product/product_list?pageSize=10&pageNo=1&type=0。(update: 定期近期沒貨,可能看不到數據,可以看1月19號以前的)數據如下圖所示:
這里包含了所有線上正在銷售的定期產品,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代碼很長,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=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; iuser.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= max){ delArr1.push(j); } } sort.delArrByIndex(lmlc[i].records, delArr1); } // 刪掉prod.records為空的數據 let delArr2 = []; for(let i=0, len=lmlc.length; i = 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 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(" <----- 請用chrome瀏覽器打開 http://localhost:5000 頁面,并激活livereload插件 -----> "); }); 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地址 | 我的博客
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/107523.html
摘要:剩下的同學,我們繼續了可以看出,作為一個完善的電商網站,尚妝網有著普通電商網站所擁有的主要的元素,包括分類,分頁,主題等等。 系列教程 手把手教你寫電商爬蟲-第一課 找個軟柿子捏捏 如果沒有看過第一課的朋友,請先移步第一課,第一課講了一些基礎性的東西,通過軟柿子切糕王子這個電商網站好好的練了一次手,相信大家都應該對寫爬蟲的流程有了一個大概的了解,那么這課咱們就話不多說,正式上戰場,對壘...
摘要:剩下的同學,我們繼續了可以看出,作為一個完善的電商網站,尚妝網有著普通電商網站所擁有的主要的元素,包括分類,分頁,主題等等。 系列教程 手把手教你寫電商爬蟲-第一課 找個軟柿子捏捏 如果沒有看過第一課的朋友,請先移步第一課,第一課講了一些基礎性的東西,通過軟柿子切糕王子這個電商網站好好的練了一次手,相信大家都應該對寫爬蟲的流程有了一個大概的了解,那么這課咱們就話不多說,正式上戰場,對壘...
摘要:之前寫了一個電商爬蟲系列的文章,簡單的給大家展示了一下爬蟲從入門到進階的路徑,但是作為一個永遠走在時代前沿的科技工作者,我們從來都不能停止。金融數據實在是價值大,維度多,來源廣。由于也是一種,因此通常來說,在中抽取某個元素是通過來做的。 相關教程: 手把手教你寫電商爬蟲-第一課 找個軟柿子捏捏 手把手教你寫電商爬蟲-第二課 實戰尚妝網分頁商品采集爬蟲 手把手教你寫電商爬蟲-第三課 實戰...
摘要:之前寫了一個電商爬蟲系列的文章,簡單的給大家展示了一下爬蟲從入門到進階的路徑,但是作為一個永遠走在時代前沿的科技工作者,我們從來都不能停止。金融數據實在是價值大,維度多,來源廣。由于也是一種,因此通常來說,在中抽取某個元素是通過來做的。 相關教程: 手把手教你寫電商爬蟲-第一課 找個軟柿子捏捏 手把手教你寫電商爬蟲-第二課 實戰尚妝網分頁商品采集爬蟲 手把手教你寫電商爬蟲-第三課 實戰...
摘要:和前面幾節課類似的分析這節課就不做了,對于分頁,請求什么的,大家可以直接參考前面的四節課,這一刻主要特別的是,我們在采集商品的同時,會將京東的商品評價采集下來。 系列教程: 手把手教你寫電商爬蟲-第一課 找個軟柿子捏捏 手把手教你寫電商爬蟲-第二課 實戰尚妝網分頁商品采集爬蟲 手把手教你寫電商爬蟲-第三課 實戰尚妝網AJAX請求處理和內容提取 手把手教你寫電商爬蟲-第四課 淘寶網商品爬...
閱讀 2741·2021-11-24 09:39
閱讀 1650·2021-09-28 09:35
閱讀 1123·2021-09-06 15:02
閱讀 1315·2021-07-25 21:37
閱讀 2731·2019-08-30 15:53
閱讀 3648·2019-08-30 14:07
閱讀 718·2019-08-30 11:07
閱讀 3518·2019-08-29 18:36