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

結尾

獻上我的源碼一份,望不吝點贊。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM