如何給網站加入優雅的實時反爬蟲策略

你的網站內容很有價值,希望被google,百度等正規搜索引擎爬蟲收錄,卻不想讓那些無節操的山寨爬蟲把你的數據扒走坐享其成。本文將探討如何在網站中加入優雅的反爬蟲策略。

思路

反爬蟲策略要考慮以下幾點:

能被google、百度等正規搜索引擎爬蟲抓取,不限流量和并發數;

阻止山寨爬蟲的抓??;

反爬蟲策略應該是實時檢測的,而不是通過一段時間后的訪問統計分析得出;

誤判后的人性化處理(優雅之所在);

大部分的爬蟲不是以瀏覽器方式來訪問頁面的,爬蟲只下載網頁的html源代碼,不加載包含在頁面中的js/css/圖片,這是區分爬蟲與否的一個關鍵。一個請求被識別出來不是瀏覽器訪問,一定是爬蟲,為了滿足上面所說的第1點和第2點,進一步對http頭agent進行驗證,是否標記為google、百度的spider,嚴格一點的話應該判別來源IP是否為google、baidu的爬蟲IP,這些IP在網上都可以找到。校驗出來IP不在白名單就可以阻止訪問內容。

當然,有一部分爬蟲是以瀏覽器載入的方式來抓取內容的,所以,即使被識別出來是瀏覽器訪問的來源ip。還要檢測這個個ip在一個時間片內的并發數,超過一定閥值,可以認為是爬蟲,阻止訪問內容。

由于我們的反爬蟲策略是基于IP的,會出現誤判,尤其是并發量限制的判別。我們需要一種友好的方式來阻止訪問。直接返回50x/40x空白或者錯誤頁面是很粗魯的,當真正的用戶被誤判阻止訪問時能夠手動解鎖繼續訪問才是比較優雅的方式,大家不約而同的會想到驗證碼,對!讓用戶輸入圖形中的驗證碼解鎖,可是我們平常見到的驗證碼都還是野蠻的,驗證碼技術從一開始的簡單的數字,發展今天有輸入漢字的、輸入數學計算結果的等等五花八門,不僅以復雜的驗證碼刁難用戶,還要加上各種干擾字符,美其名曰提高安全性,實際上是開發工程師腦殘扎堆鉆牛角尖的產物,用戶是怨聲載道。驗證碼的目的是區分人工和機器,要做到機器無法自動操作,同時讓人工操作很方便、優雅。在本文的案例中,我們采用了一種比較有趣的驗證碼,讓人識別物體,在驗證碼系統中預存了大量的事物,包括動物、植物、家具等等日常遇到的東西,驗證用戶的過程就是系統從這些事物中隨機選出少量圖形,并要求用戶選中預設答案中的某一個即可解鎖。

回到識別爬蟲的步驟,我們用流程圖理一下:

識別爬蟲流程圖

實現

我們用nodejs(express)和redis來實現反爬蟲系統,redis用來存放一些計數。

1、判別是否為瀏覽器訪問

返回頁面請求時,在redis中給該IP的頁面訪問計數+1。在每個頁面中會引入一個js,當請求這個js文件時在redis中給該IP頁面訪問計數-1,這樣,如果不是瀏覽器的請求,redis中的頁面計數會不斷增大,如果是瀏覽器請求,下載頁面源代碼時增1,隨后瀏覽器加載js文件時減1,redis中的頁面計數會歸零。我們只需要判斷頁面計數是否為0來區分是否為瀏覽器訪問,我們還可以給頁面下載完了但是js沒有加載這種特殊情況留點余地,設定一個閥值,例如:5,頁面計數大于5就判別出該IP內有爬蟲訪問。

2、爬蟲白名單識別

如果上一步被識別為爬蟲訪問,則進一步檢測用戶http頭的user-agent、ip,判斷是否在預設的白名單內。如果不在則阻止訪問顯示驗證碼。這個步驟很簡單,不用多說。

3、瀏覽器訪問下的并發量限制

同樣在 redis下給每個IP做計數,和上面不同的是利用redis key的過期機制,每次計數累加時將key設定在一定的時間內過期,比如5秒,這個相當于一個時間片,如果5秒內有另外一個請求,會計數增加1,過期時間會延長5秒,如果在一個5秒內沒有其他請求,這個key就會消失。此后一個請求進來計數從1開始。我們可以設定一個閥值,比如20,任意5秒內有20個請求進來為超限,阻止訪問顯示驗證碼。

4、優雅的驗證碼

系統預設了很多圖片,每個圖片是一個動物、植物、家具等,每個圖片有一個ID值。從這些圖片中任意抽出3個,并且選中其中一個為標準答案,注意這個過程都是程序后臺進行,將標準答案ID放在session中。前臺頁面顯示了這3幅圖片,并根據預設的答案要求用戶選擇其中一個,用戶只要選中對應的圖片,將表單提交到后臺,系統將提交的ID與session中ID比較判別是否正確。當然,每個圖片都有一個固定的ID值有被窮舉的漏洞,有很多改進的余地,此處僅討論原型不做過多探討。

效果如圖:

校驗

好了,接下來我會貼出一些實現的代碼,如果你想看看實現后的效果,可以訪問碰頭吧(http://www.pengtouba.com/)試驗一下,首頁沒有加反爬蟲策略。打開微信廣場http://www.pengtouba.com/weixin/cast-c1p1s1.html 然后用F5強暴刷新你就會看到效果了。

代碼

攔截請求(其他語言類似,例如java可以用攔截器)

app.get('/weixin/*', antiCrawler.openDoor);//需要保護的目錄
app.get('/helper/close-door.js', antiCrawler.closeDoor); //偽js文件

antiCrawler.js

/**
 * anti crawler
 * Created by Cherokee on 14-7-13.
 */
var settings = require("../settings.json");
var redis = require("redis");
var cache = require("../lib/cache.js");
var vcode = require('../lib/vcode.js');
var ac_redis_cli = redis.createClient(settings['anti_crawler_redis_db']['port'],settings['anti_crawler_redis_db']['host']);
var IP_RECORD_EXPIRE = settings['anti_crawler_redis_db']['ip_record_expire'];
var IP_LOCK_EXPIRE = settings['anti_crawler_redis_db']['ip_lock_expire'];
var IP_HAIR_EXPIRE = settings['anti_crawler_redis_db']['ip_hair_expire'];
var DOOR_THRESHOLD = settings['anti_crawler_redis_db']['door_threshold'];
var HAIR_THRESHOLD = settings['anti_crawler_redis_db']['hair_threshold'];


ac_redis_cli.on('ready',function(){
    console.log('redis for anti-crawler is ready');
});

ac_redis_cli.on('error',function(err){
    console.error('redis for anti-crawler error'+err);
});

ac_redis_cli.on('end',function(){
    console.error('redis for anti-crawler closed');
});

ac_redis_cli.select(settings['anti_crawler_redis_db']['db'],function(err){
    if(err)throw err;
    else {
        cache.set('ac_redis_cli',ac_redis_cli,77760000);
        console.log('redis for anti-crawler switch db :'+settings['anti_crawler_redis_db']['db']);
    }
});

exports.openDoor = function(req, res, next) {
    var ua = req.get('User-Agent');
    var ip = req.ip;
    var url = req.url;

    if(/\/weixin\//.test(url)){
        ac_redis_cli.exists('lock:'+ip,function(err,bol){
            if(bol){
                send421(req,res);
            }else{
                ac_redis_cli.get('door:'+ip,function(err,d_num){
                    if(d_num>DOOR_THRESHOLD){//some one didn't use browser
                        if(isTrustSpider(ua,ip)){//it's trusted spider
                            kickDoor(ip,function(val){
                                leaveHair(ip,function(val){
                                    next();
                                });
                            });
                        }else{
                            blockIt(req,res);
                        }
                    }else{//perhaps using simulated browser to crawl pages
                        ac_redis_cli.get('hair:'+ip,function(err,h_num){
                            if(h_num>HAIR_THRESHOLD){//suspicious
                                blockIt(req,res);
                            }else {
                                kickDoor(ip,function(val){
                                    leaveHair(ip,function(val){
                                        next();
                                    });
                                });
                            }
                        });
                    }
                });
            }
        });
    }
};

exports.closeDoor = function(req,res){
    ac_redis_cli.multi()
        .decr('door:'+req.ip)
        .expire('door:'+req.ip,IP_RECORD_EXPIRE)
        .exec(function(err, replies){
            if(replies&&parseInt(replies[0])<0){
                ac_redis_cli.set('door:'+req.ip,0,function(err){
                    res.set('Content-Type', 'application/x-javascript');
                    res.send(200,'{"zeroize":true}');
                });
            }else{
                res.set('Content-Type', 'application/x-javascript');
                res.send(200,'{"zeroize":false}');
            }
        });
}

exports.verify = function(req,res){
    var vcode = req.body.vcode;
    var origin_url = req.body.origin_url;
    if(req.session.vcode&&vcode==req.session.vcode){
        req.session.vcode = null;
        ac_redis_cli.multi()
            .del('lock:'+req.ip)
            .del('door:'+req.ip)
            .del('hair:'+req.ip)
            .exec(function(err, replies){
                res.redirect(origin_url);
            });
    }else send421(req,res,origin_url);

}

var blockIt = function(req,res){
    ac_redis_cli.multi()
        .set('lock:'+req.ip,1)
        .expire('lock:'+req.ip,IP_LOCK_EXPIRE)
        .exec(function(err, replies){
            send421(req,res);
        });
}

var send421 = function(req,res,origin_url){
    var code_map = {};
    var code_arr = [];

    while(code_arr.length<3){
        var rindex = Math.ceil(Math.random() * vcode.list.length) - 1;
        if(!code_map[rindex]){
            code_map[rindex] = true;
            code_arr.push(rindex);
        }
    }
    var answer = code_arr[Math.ceil(Math.random() * 3) - 1];
    req.session.vcode = answer;
    res.status(421).render('weixin/421',{'code_list':code_arr,'code_label':vcode.list[answer],'origin_url':origin_url||req.url});
}

var isTrustSpider = function(ua,ip){
    var trustBots  = [
        /Baiduspider/ig,
        /Googlebot/ig,
        /Slurp/ig,
        /Yahoo/ig,
        /iaskspider/ig,
        /Sogou/ig,
        /YodaoBot/ig,
        /msnbot/ig,
        /360Spider/ig
    ];
    for(var i=0;i<trustBots.length;i++){
        if(trustBots[i].test(ua))return true;
    }
    return false;
}

var kickDoor = function(ip,callback){
    ac_redis_cli.multi()
        .incr('door:'+ip)
        .expire('door:'+ip,IP_RECORD_EXPIRE)
        .exec(function(err, replies){
            if(callback)callback(replies?replies[0]:null);
        });
}

var leaveHair = function(ip,callback){
    ac_redis_cli.multi()
        .incr('hair:'+ip)
        .expire('hair:'+ip,IP_HAIR_EXPIRE)
        .exec(function(err, replies){
            if(callback)callback(replies?replies[0]:null);
        });
}

實際應用中不僅要檢測User-agent,還要有IP白名單檢測,以上代碼并沒有包含 IP白名單。

send421函數就是顯示驗證碼的步驟,verify函數是檢驗用戶輸入的驗證碼。

大发快3