种子爬取心得

简介

今天分享的主题是“使用nodeJs爬取磁力链接”,本文以探讨严肃的学术研究为目的。本文共分几块内容:

  1. 明确需求
  2. 程序结构
  3. 细节补完
  4. 程序优化
  5. 避免被反爬虫

一、明确需求

知己知彼百战百胜,想要爬种子必须要明确:希望得到什么样的资源?以及去什么地方寻找资源?

希望得到什么样的资源

资源的番号通常可以代表一个口味系列,比方说笔者喜欢轻口味的,并且无码的片子,不喜欢看凌虐情节,那么应该找SMD系列的资源,SMD意为super model,其番号通常是SMD-xx。 那么也有人会也喜欢轻口味系列的,强烈要求女主必须面容姣好,演戏投入的,可以接受有码,那么应该找IPZ系列、STAR系列、ABP系列的。 也有女同学会说这些资源里的男演员太丑,而且需要一些情节,不希望上来就是直接赤裸肉搏大戏的,看起来完全没有代入感,就可以去找SILK系列的片子,这里男帅女靓。 至于大名鼎鼎的东热公司出品的系列,大多数口味都比较重,并且有码,笔者心生厌恶,这里压下不表。

去什么地方寻找资源

鉴于t66y,jav等知名站点是需要翻墙才能访问,于是乎在国内还得得到资源的,就只剩下诸如torrentkitty这样的种子搜索站点,只要得到番号,就可以当作关键字搜索从而获取目标资源。这里笔者选了一个请求响应速度较快的bt2.bt87.cc这个站点。

二、程序结构

大致流程

举个例子,我们先需要发送下图请求,返回的是一个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语法,改造我们的请求函数和文件操作函数,代码如下

//第一个请求,请求资源列表
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文件里的就是这种格式

四、程序优化

获取多个资源并且过滤其中不合格的

下载过程中会发现有一些链接总是不可用,或者是下载过来的片源质量太差,看着模糊。所以我们希望能够在一个番号里一次性爬出3个磁力链接,并且过滤掉小于700MB的影片和大于4G的影片。所以我们的代码需要改写

getResourceUrl(url);  
.then(function (html) {
    //var href = 第一个ul里的第一个第一个a标签的href属性
    var $ = cheerio.load(html);
    var $body = $('.media-body');
    var reqArr = [];
    $body.each(function (i, elem) {
        if (reqArr.length === 3) {
            return false;
        }
        var $elem = $(elem);
        var size = $elem.find('.label.label-warning').text();
        //小于4G则允许去获取
        if (size && size.indexOf('GB') > -1 && parseInt(size) < 4 ||
            size && size.indexOf('MB') > -1 && parseInt(size) > 700 && parseInt(size) < 4000) {
            var href = $elem.find('.title').attr('href');
            reqArr.push(getMagnet('http://bt2.bt87.cc' + href));
        }
    });
    if (reqArr.length > 0) {
        return Promise.all(reqArr);
    } else {
        //当第二次请求正常或者第一次请求没有匹配到合适的资源,计数+1。
        event.emit('parse_done', null, searchId);
        console.log('can not match suitable resource');
    }
}, function (res) {
    return Promise.reject(res);
})
.then(function (resArr) {
    //注意这里得到的是一个结果数组
    ......

}, function (res) {
    return Promise.reject(res);
})
...

获取的资源数组按有无封面图或者大小为降序排序。

操作过程中我们一般使用第一个资源去下载影片,我们更希望让第一个片子是有封面图的,而且大小尽可能小一些以便于下载。所以需要优化我们的第二个请求,代码如下

getResourceUrl(url)  
.then(function (html) {
    //这里省略过滤规则直接发起第二次请求
}, function (res) {
    return Promise.reject(res);
})
.then(function (resArr) {
    var res = [];
    resArr.forEach(function (v, k) {
        var $ = cheerio.load(v);
        var name = $('.row-fluid.tor-title h2').text();
        var magnet = $('#magnetLink').val();
        var size = $('.tor-detail').html().match(/\d+(\.\d{1,2})?(MB|GB)/)[0];
        var haspic = $('.tor-info').html().match(/(png|jpg)/) ? true : false;
        res.push({
            name: name,
            magnet: magnet,
            size: size,
            haspic: haspic
        });
    });

    //res就是我们得到的结果,以下省略排序,也省略输出到文件的操作
    ...

}, function (res) {
    return Promise.reject(res);
})

最后我们输出的内容变成了这样

5、避免被反爬虫

笔者曾经在爬取妹子图网站上的妹子图片的时候曾经遇到过,爬虫返回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池,在爬虫的时候不停轮换使用。诚可谓道高一尺魔高一丈。

结尾

能看到最后的都是学习热情高涨的同学么,不论是对种子还是对代码,这里我献上我的源码一份,望不吝点赞!荆轲刺秦王。

微信扫描查看或分享
加入我们