Node 爬虫心得
简介
使用 Node 爬取信息和其他语言几乎步骤相同,都同样是以下几点
- 发起请求
- 解析内容
- 避免反爬虫
- 爬虫策略更新
注意:爬正规网站可能会有法律风险,但是那些小站,甚至自身就有问题的那种,总不怕啥问题。
发起请求
举个例子,笔者随手找了一个种子搜索站。发送下图请求,返回的是一个html页面
接着我们分析页面html代码找到列表第一项的资源的超链接为 '/0AA61E5C1B7B665BC02BCCAF55F3EF7837AFA4F0.html',加上此站域名从而发送下图请求
具体解析页面html代码抓取到想要的文本的方法,可以很粗暴的选择正则表达式。当抓取完毕资源,应该存储到本地,并且开始重新发送请求再来一遍。
Demo 代码如下:
var http = require('http');
// http.request(options, callback);
http.get('http://bt2.bt87.cc/search/SMD31_ctime_1.html', function(res) {
var data = '';
res.setEncoding("utf8");
res.on('data', function(chunk) {
data += chunk;
}).on('end', function() {
console.log(data)
});
});
这里的data就是我们抓去到的html片段大概长这样
第二幅图里的 magnet:xxxxxxxx,这种格式就是我们要的资源链接,迅雷可用。
调整代码如下:
var http = require('http');
var count = 31;
var start = function (id) {
http.get('http://bt2.bt87.cc/search/SMD'+ id + '_ctime_1.html', function(res) {
var data = '';
res.setEncoding("utf8");
res.on('data', function(chunk) {
data += chunk;
}).on('end', function() {
//var href = 第一个ul里的第一个第一个a标签的href属性
http.get('http://bt2.bt87.cc' + href, function(res1) {
var data1 = '';
res1.setEncoding("utf8");
res1.on('data', function(chunk) {
data1 += chunk;
}).on('end', function() {
//var magnet = 正则匹配带有magnet关键字的信息
/* fs.appendFile(path, content, function (err){}) */
//重新开始请求
start(id + 1);
});
});
});
});
};
start(count);
代码优化
上面的代码陷入了回调地狱里,十分难看,并且也不健壮。任何一个环节出差错都会导致后面代码不执行而停止循环请求。
解决办法是,我们可以使用 ES6 的 Promise 语法,毕竟 Node 自 8 后,完全支持 Promise。改造我们的请求函数和文件操作函数。
得到了爬取内容后,就得解析,解析 HTML 可以用 cheerio,类 JQuery 语法。但简单点直接正则吧,代码如下:
//第一个请求,请求资源列表
var getResourceUrl = function (url) {
return new Promise(function (resolve, reject) {
http.get(url, function(response) {
var html = '';
response.on('data', function(data) {
html += data;
});
response.on('end', function() {
var ul = html.match(/<ul class="media-list media-list-set">[\s\S]*<\/ul>/);
if (ul) {
resolve(ul[0]);
} else {
reject('can not match ul dom');
}
});
}).on('error', function() {
reject('getResourceUrl failed');
});
});
};
//第二个请求,请求具体的某个资源
var getMagnet = function (url) {
return new Promise(function (resolve, reject) {
http.get(url, function(response) {
var html = '';
response.on('data', function(data) {
html += data;
});
response.on('end', function() {
var magnet = html.match(/magnet:\??[^"|<]+/);
if (magnet) {
resolve(html);
} else {
reject('can not match magnetReg');
}
});
}).on('error', function (res) {
reject(res);
});
});
};
//追加文件
var appendFile = function (path, content) {
return new Promise(function (resolve, reject) {
fs.appendFile(path, content, {flag:'a'}, function (err) {
if (err) {
reject('append ' + path + ' failed');
} else {
resolve('append ' + path + ' success');
}
});
});
};
然后我们的调用的代码就成了这样
//开始函数
var start = function () {
getResourceUrl(url);
.then(function (html) {
//var href = 第一个ul里的第一个第一个a标签的href属性
return getMagnet('http://bt2.bt87.cc' + href);
}, function (res) {
return Promise.reject(res);
})
.then(function (resArr) {
//var magnet = 正则匹配带有magnet关键字的信息
return appendFile('./SMD.txt', magnet);
}, function (res) {
console.log(res);
return Promise.reject(res);
})
.then(function (resArr) {
console.log('writeFile success!');
start();
}, function (res) {
console.log(res);
start();
});
};
简单又粗暴,而且某个环节掉了链子,比方说第一次请求匹配不到我们要的链接,也能把错误传递到最后的then里而重新 start() 一个请求,不会中断。
内容解析
具体怎么匹配到我们想要的资源,正则是一个王道的办法,比如下面代码
//匹配magnet磁力链接
var magnetReg = /magnet:\??[^"|<]+/;
//匹配ul标签
var ulReg = /<ul class="media-list media-list-set">[\s\S]*<\/ul>/
//匹配a标签
var aReg = /<a class="title".* href="\/\w+\.html")/g;
但是这里可以有更简便的办法,就是利用cheerio库来DOM结构的html文本。
var cheerio = require('cheerio');
...
getResourceUrl(url);
.then(function (html) {
//var href = 第一个ul里的第一个第一个a标签的href属性
var $ = cheerio.load(html);
var $body = $('.media-body');
var href = $body.eq(0).find('.title').attr('href');
return getMagnet(href);
}, function (res) {
return Promise.reject(res);
});
就是这么容易,第二个请求也是如法炮制,最后输出到 SMD.txt 文件里的就是这种格式
避免反爬虫
笔者曾经在爬取妹子图网站上的图片的时候曾经遇到过,爬虫返回 403,这表示网站采用了防爬技术,反爬虫一般会采用比较简单即会检查用户代理(User Agent)信息。再请求头部构造一个User Agent就行了。也可能会检测Referer请求头,还有cookie等。高级的反爬虫会统计一个 ip 在一小时内请求量是否超过限制,达到则封锁 ip,这样的方案就需要加上代理,下面代码演示了一个伪造 User Agent 头并且连代理的最基本例子
var http = require('http');
var opt = {
//代理服务器的ip或者域名,默认localhost
host: '122.228.179.178',
//代理服务器的端口号,默认80
port: 80,
//path是访问的路径
path: 'http://www.163.com',
//希望发送出去的请求头
headers: {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36',
}
};
http.get(opt, function(res) {
var data = '';
res.setEncoding("utf8");
res.on('data', function(chunk) {
data += chunk;
}).on('end', function() {
console.log(data)
});
});
如果目标网站封锁了我方的IP地址的话,我们只要改变options参数里的host就能解决,这个代理ip只要在搜索引擎上输入“免费代理ip”就有了,比方说这个网站。不过不是每个免费代理ip都能用,难免有些失效了,所以狡猾的程序员会事先抓取网站提供的免费代理ip用它发送请求,如果能发送的了则证明ip可用。可用的一堆ip当作ip池,在爬虫的时候不停轮换使用。诚可谓道高一尺魔高一丈。
爬虫策略
加了 IP 能突破多数的反爬设置,但 IP 并非无限的,若短时间发的太多,还是可能被数据投毒,或者直接封禁。故而需要一些策略。
举个简单的例子是爬取一阵,休息一两分钟再继续,并且控制爬取速度。
思考题
源自本人的一次面试,面试官问:如何写一个多线程的爬虫。
提示:Node 里多线程是没办法,但是可以用多进程模式,关注一下 Node cluster 模块。
结尾
献上我的源码一份,望不吝点赞。