cnblog爬虫练习

目标

入口为http://www.cnblogs.com, 博客园文章列表页每页有20篇文章,最多可以翻到200页。异步去爬取4000篇文章的具体内容,拿到一些关键数据。
列表页

爬虫流程

抓取

最重要的步骤就是如何把想要的页面抓取回来。并且能兼顾时间效率,能够并发的同时爬取多个页面。
同时,要获取目标内容,需要我们分析页面结构,因为 ajax 的盛行,许多页面内容并非是一个url就能请求的的回来的,通常一个页面的内容是经过多次请求异步生成的。所以这就要求我们能够利用抓包工具分析页面结构。
如果深入做下去,你会发现要面对不同的网页要求,比如有认证的,不同文件格式、编码处理,各种奇怪的url合规化处理、重复抓取问题、cookies 跟随问题、多线程多进程抓取、多节点抓取、抓取调度、资源压缩等一系列问题。
所以第一步就是拉网页回来,慢慢你会发现各种问题待你优化。

存储

当把页面内容抓回来后,一般不会直接分析,而是用一定策略存下来,个人觉得更好的架构应该是把分析和抓取分离,更加松散,每个环节出了问题能够隔离另外一个环节可能出现的问题,好排查也好更新发布。
那么存文件系统、SQL or NOSQL 数据库、内存数据库,如何去存就是这个环节的重点。

分析

对网页进行文本分析,提取链接也好,提取正文也好,总之看你的需求,但是一定要做的就是分析链接了。通常分析与存储会交替进行。可以用你认为最快最优的办法,比如正则表达式。然后将分析后的结果应用与其他环节。

展示

更好的展示出来

编写爬虫代码

Step1 页面分析

目标是爬取博客园第1页至第200页内的4000篇文章,获取其中的作者信息,并保存分析。
列表页
article
共4000篇文章,所以首先我们要获得这个4000篇文章的入口,然后再异步并发的去请求4000篇文章的内容。但是这个4000篇文章的入口 URL 分布在200个页面中。所以我们要做的第一步是 从这个200个页面当中,提取出4000个 URL 。并且是通过异步并发的方式,当收集完4000个 URL 再进行下一步。

Step2 获取4000个文章入口URL

intoarticle
要获取这么多 URL ,首先还是得从分析单页面开始,F12 打开。很容易发现文章入口链接保存在 class 为 titlelnk 的 \ 标签中,所以4000个 URL 就需要我们轮询 200个列表页 ,将每页的20个 链接保存起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//一些依赖
var http = require("http"),
url = require("url"),
superagent = require("superagent"),
cheerio = require("cheerio"),
async = require("async"),
eventproxy = require('eventproxy');

var ep = new eventproxy(),
urlsArray = [], //存放爬取网址
pageUrls = [], //存放收集文章页面网站
pageNum = 200; //要爬取文章的页数

for(var i=1 ; i<= 200 ; i++){
pageUrls.push('http://www.cnblogs.com/#p'+i);
}

// 主start程序
function start(){
function onRequest(req, res){
// 轮询 所有文章列表页
pageUrls.forEach(function(pageUrl){
superagent.get(pageUrl)
.end(function(err,pres){
// pres.text 里面存储着请求返回的 html 内容,将它传给 cheerio.load 之后
// 就可以得到一个实现了 jquery 接口的变量,我们习惯性地将它命名为 `$`
// 剩下就都是利用$ 使用 jquery 的语法了
var $ = cheerio.load(pres.text);
var curPageUrls = $('.titlelnk');

for(var i = 0 ; i < curPageUrls.length ; i++){
var articleUrl = curPageUrls.eq(i).attr('href');
urlsArray.push(articleUrl);
// 相当于一个计数器
ep.emit('BlogArticleHtml', articleUrl);
}
});
});

ep.after('BlogArticleHtml', pageUrls.length*20 ,function(articleUrls){
// 当所有 'BlogArticleHtml' 事件完成后的回调触发下面事件
// ...
});
}
http.createServer(onRequest).listen(3000);
}
exports.start= start;

这里我们用到了三个库,superagent 、 cheerio 、 eventproxy。
分别简单介绍一下:
superagent

superagent(http://visionmedia.github.io/superagent/ ) 是个轻量的的 http 方面的库,是nodejs里一个非常方便的客户端请求代理模块,当我们需要进行 get 、 post 、 head 等网络请求时,可以尝试使用。

cheerio

cheerio(https://github.com/cheeriojs/cheerio ) 大家可以理解成一个 Node.js 版的 jquery,用来从网页中以 css selector 取数据,使用方式跟 jquery 一样一样的。

eventproxy

eventproxy(https://github.com/JacksonTian/eventproxy ) 非常轻量的工具,但是能够带来一种事件式编程的思维变化。

如果你要并发异步获取两三个地址的数据,并且要在获取到数据之后,对这些数据一起进行利用的话,常规的写法是自己维护一个计数器。

先定义一个 var count = 0,然后每次抓取成功以后,就 count++。如果你是要抓取三个源的数据,由于你根本不知道这些异步操作到底谁先完成,那么每次当抓取成功的时候,就判断一下count === 3。当值为真时,使用另一个函数继续完成操作。

而 eventproxy 就起到了这个计数器的作用,它来帮你管理到底这些异步操作是否完成,完成之后,它会自动调用你提供的处理函数,并将抓取到的数据当参数传过来。

Step3 爬取具体页面内容 使用 async 控制异步并发数量

获取到4000个 URL ,并且回调入口也有了,接下来我们只需要在回调函数里继续爬取4000个具体页面,并收集我们想要的信息就好了。其实刚刚我们已经经历了第一轮爬虫爬取,只是有一点做的不好的地方是我们刚刚并没有限制并发的数量,这也是我发现 cnblog 可以改善的一点,不然很容易被单IP的巨量 URL 请求攻击到崩溃。为了做一个好公民,也为了减轻网站的压力(其实为了不被封IP),这4000个URL 我限制了同时并发量最高为5。这里用到了另一个非常强大的库 async ,让我们控制并发量变得十分轻松,简单的介绍如下。

async

async(https://github.com/caolan/async#queueworker-concurrency),async是一个流程控制工具包,提供了直接而强大的异步功能mapLimit(arr, limit, iterator, callback)。

这次我们要介绍的是 async 的 mapLimit(arr, limit, iterator, callback) 接口。另外,还有个常用的控制并发连接数的接口是 queue(worker, concurrency) ,大家可以去看看它的API。

继续我们的爬虫,进到具体的文章页面,发现我们想获取的信息也不在直接请求而来的 html 页面中,而是如下这个 ajax 请求异步生成的,不过庆幸的是我们上一步收集的 URL 包含了这个请求所需要的参数,所以我们仅仅需要多做一层处理,将这个参数从 URL 中取出来再重新拼接成一个ajax URL 请求。
intoarticle
下面,贴出代码,在我们刚刚的回调函数中,继续我们4000个页面的爬取,并且控制并发数为5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

ep.after('BlogArticleHtml',pageUrls.length*20,function(articleUrls){
// 当所有 'BlogArticleHtml' 事件完成后的回调触发下面事件
// 控制并发数
var curCount = 0;
var reptileMove = function(url,callback){
//延迟毫秒数
var delay = parseInt((Math.random() * 30000000) % 1000, 10);
curCount++;
console.log('现在的并发数是', curCount, ',正在抓取的是', url, ',耗时' + delay + '毫秒');

superagent.get(url)
.end(function(err,sres){
// sres.text 里面存储着请求返回的 html 内容
var $ = cheerio.load(sres.text);
// 收集数据
// 拼接URL
var currentBlogApp = url.split('/p/')[0].split('/')[3],
appUrl = "http://www.cnblogs.com/mvc/blog/news.aspx?blogApp="+ currentBlogApp;
// 具体收集函数
personInfo(appUrl);
});

setTimeout(function() {
curCount--;
callback(null,url +'Call back content');
}, delay);
};

// 使用async控制异步抓取
// mapLimit(arr, limit, iterator, [callback])
// 异步回调
async.mapLimit(articleUrls, 5 ,function (url, callback) {
reptileMove(url, callback);
}, function (err,result) {
// 4000 个 URL 访问完成的回调函数
// ...
});
});

根据重新拼接而来的 URL ,再写一个具体的 personInfo(URL) 函数,具体获取我们要的昵称、园龄、粉丝数等信息。

这样,我们把抓取回来的信息以 JSON 串的形式存储在 catchDate 这个数组当中,

node index.js 运行一下程序,将结果打印出来.

intoarticle
intoarticle
intoarticle

至此,第三步就完成了,我们也收集到了4000条我们想要的原始数据。