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 模块

结尾

献上我的源码一份,望不吝点赞。

posted @ 2020-05-06 12:04  Ever-Lose  阅读(1155)  评论(0编辑  收藏  举报