更新:立马金库在4月23日就中止在平台上售卖,因此以后的是没有数据的css
其实在早以前,就作过立马理财的销售额统计,只不过是用前端js写的,须要在首页的console调试面板里粘贴一段代码执行,点击这里。主要是经过定时爬取https://www.lmlc.com/s/web/home/user_buying
异步接口来获取数据。而后经过必定的排重算法来获取最终的数据。可是这样作有如下缺点:html
因为最近学习了node爬虫相关知识,咱们能够在后台本身模拟请求,爬取页面数据。而且我开通了阿里云服务器,能够把代码放到云端跑。这样,一、二、3均可以解决。4是由于以前不知道这个ajax接口是每三分钟更新一次,这样咱们能够根据这个来排重,确保数据不会重复。说到爬虫,你们想到的比较多的仍是python,确实python有Scrapy等成熟的框架,能够实现很强大的爬取功能。可是node也有自身的优势,凭借强大的异步特性,能够很轻松的实现高效的异步并发请求,节省cpu的开销。其实node爬虫仍是比较简单的,下面咱们就来分析整个爬虫爬取的流程和最终如何展现数据的。前端
线上地址node
咱们最终的目标是实现爬取立马理财每日的销售额,并知道卖了哪些产品,每一个产品又被哪些用户在什么时间点买的。首先,介绍下爬虫爬取的主要步骤:python
咱们要爬取页面的数据,第一步固然是要先分析清楚页面结构,要爬哪些页面,页面的结构是怎样的,需不须要登陆;有没有ajax接口,返回什么样的数据等。jquery
分析清楚要爬取哪些页面和ajax,就要去抓取数据了。现在的网页的数据,大致分为同步页面和ajax接口。同步页面数据的抓取就须要咱们先分析网页的结构,python抓取数据通常是经过正则表达式匹配来获取须要的数据;node有一个cheerio的工具,能够将获取的页面内容转换成jquery对象,而后就能够用jquery强大的dom API来获取节点相关数据, 其实你们看源码,这些API本质也就是正则匹配。ajax接口数据通常都是json格式的,处理起来仍是比较简单的。git
抓取的数据后,会作简单的筛选,而后将须要的数据先保存起来,以便后续的分析处理。固然咱们能够用MySQL和Mongodb等数据库存储数据。这里,咱们为了方便,直接采用文件存储。es6
由于咱们最终是要展现数据的,因此咱们要将原始的数据按照必定维度去处理分析,而后返回给客户端。这个过程能够在存储的时候去处理,也能够在展现的时候,前端发送请求,后台取出存储的数据再处理。这个看咱们要怎么展现数据了。github
作了这么多工做,一点展现输出都没有,怎么甘心呢?这又回到了咱们的老本行,前端展现页面你们应该都很熟悉了。将数据展现出来才更直观,方便咱们分析统计。web
Superagent是个轻量的的http方面的库,是nodejs里一个很是方便的客户端请求代理模块,当咱们须要进行get、post、head等网络请求时,尝试下它吧。
Cheerio你们能够理解成一个 Node.js 版的 jquery,用来从网页中以 css selector 取数据,使用方式跟 jquery 如出一辙。
Async是一个流程控制工具包,提供了直接而强大的异步功能mapLimit(arr, limit, iterator, callback),咱们主要用到这个方法,你们能够去看看官网的API。
arr-del是我本身写的一个删除数组元素方法的工具。能够经过传入待删除数组元素index组成的数组进行一次性删除。
arr-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条,数据就会有遗漏。这是没有办法的,因此立马金库的统计数据会比真实的偏少。
由于产品详情页须要登陆,因此咱们要先拿到登陆的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方法。
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属性记录着售卖信息。
咱们再看下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。
先上代码:
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,那么几乎能够确定是旧数据。若是同一我的正好在三分钟后购买同一个产品相同的金额那我也没辙了,哈哈。
天天零点咱们须要整理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。
前端总共就两个页面,首页和详情页,首页主要展现实时销售额、某一时间段内的销售状况、具体某天的销售状况。详情页展现某天的具体某一产品销售状况。页面有两个入口,并且比较简单,这里咱们采用gulp来打包压缩构建前端工程。后台用express搭建的,匹配到路由,从data文件夹里取到数据再分析处理再返回给前端。
Echarts Echarts是一个绘图利器,百度公司不可多得的良心之做。能方便的绘制各类图形,官网已经更新到4.0了,功能更增强大。咱们这里主要用到的是直方图。
DataTables Datatables是一款jquery表格插件。它是一个高度灵活的工具,能够将任何HTML表格添加高级的交互功能。功能很是强大,有丰富的API,你们能够去官网学习。
Datepicker Datepicker是一款基于jquery的日期选择器,须要的功能基本都有,主要样式比较好看,比jqueryUI官网的Datepicker好看太多。
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插件,在开发环境下,文件改动会自动刷新页面。
至此,一个完整的爬虫就完成了。其实我以为最须要花时间的是在分析页面结构,处理数据还有解决各类问题,好比如何保持一直在登陆状态等。
本爬虫代码只作研究学习用处,禁止用做任何商业分析。再说,统计的数据也不许确。
由于代码开源,但愿你们照着代码去爬取其余网站,若是都拿立马理财来爬,估计服务器会承受不了的额。