其实在早之前,就做过立马理财的销售额统计,只不过是用前端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数据只有产品本身相关的信息,比如产品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);