文档章节

如何给网站加入优雅的实时反爬虫策略

小水熊
 小水熊
发布于 2014/08/20 18:35
字数 2198
阅读 9149
收藏 185

你的网站内容很有价值,希望被google,百度等正规搜索引擎爬虫收录,却不想让那些无节操的山寨爬虫把你的数据扒走坐享其成。本文将探讨如何在网站中加入优雅的反爬虫策略。

【思路】

反爬虫策略要考虑以下几点:

  1. 能被google、百度等正规搜索引擎爬虫抓取,不限流量和并发数;

  2. 阻止山寨爬虫的抓取;

  3. 反爬虫策略应该是实时检测的,而不是通过一段时间后的访问统计分析得出;

  4. 误判后的人性化处理(优雅之所在);


大部分的爬虫不是以浏览器方式来访问页面的,爬虫只下载网页的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函数是检验用户输入的验证码。


© 著作权归作者所有

小水熊

小水熊

粉丝 67
博文 61
码字总数 41498
作品 1
静安
架构师
私信 提问
加载中

评论(15)

杨万
杨万
楼主你好问一下,如何判断请求的url是否存在反爬虫
小水熊
小水熊

引用来自“Seminar”的评论

大神你好,我是小白...最近一直在看你写的“如何给网站加入优雅的实时反爬虫策略”,有些问题想向大神请教
1.kickdoor和leavehair两个函数分别实现了什么功能...按照文章讲述应该是计数,判断一定时间内并发数量是否超过阀值,但两个函数下感觉都在自增...不太明白
2.可以分别告诉一下 IP_RECORD_EXPIRE,IP_LOCK_EXPIRE
, IP_HAIR_EXPIRE , DOOR_THRESHOLD , HAIR_THRESHOLD的意思吗?我觉得我理解的可能不太对

谢谢大神!
kickdoor是给IP做一个增加的计数, 发生在页面本身的请求时刻。close_door的时候将这个计数减一,发生在页面尾部的一个js请求时。 达到一个目的:只要是浏览器请求的, 这个计数是先加一, 再减一, 结果应该一直保持0。 非浏览器请求的话这个数只加不减, 会越来越大, 在一定时间内连续增长一定程度大于DOOR_THRESHOLD就可以判定它是爬虫了。leavehair则是只要有用户访问就增加计数, 但是这个计数在一定时间内会过期,每增加一次延续一次额外的存活时间, 是用来判断在连续的一段时间内ip访问有多高。 结合前面的是否浏览器的判断, 就可以比较准确的判断出爬虫了。超过一定并发数的就出现验证码。
Seminar
Seminar
大神你好,我是小白...最近一直在看你写的“如何给网站加入优雅的实时反爬虫策略”,有些问题想向大神请教
1.kickdoor和leavehair两个函数分别实现了什么功能...按照文章讲述应该是计数,判断一定时间内并发数量是否超过阀值,但两个函数下感觉都在自增...不太明白
2.可以分别告诉一下 IP_RECORD_EXPIRE,IP_LOCK_EXPIRE
, IP_HAIR_EXPIRE , DOOR_THRESHOLD , HAIR_THRESHOLD的意思吗?我觉得我理解的可能不太对

谢谢大神!
开源中国首席吃不饱
开源中国首席吃不饱

引用来自“国栋”的评论

爬虫以浏览器载入的方式,降低并发频度,间隔时间随机化,你还能识别出来吗?

引用来自“水熊宝宝”的评论

间隔时间长、或者使用大量代理连接都是可以的,这些策略在我的开源爬虫neocrawler(http://git.oschina.net/dreamidea/neocrawler)都有实现,但是这样做以后爬虫抓取的效率会很低。

引用来自“国栋”的评论

问题是我不单单抓取你一个网页呀,我有一系列的网站列表用于轮询,我的爬虫还是满负荷在运转的
这个跟正常用户一样了吧...
yan_li
yan_li
西门飞
西门飞
nodejs的代码好丑 要是nodejs写大型应用简直就是灾难
国栋
国栋

引用来自“国栋”的评论

爬虫以浏览器载入的方式,降低并发频度,间隔时间随机化,你还能识别出来吗?

引用来自“水熊宝宝”的评论

间隔时间长、或者使用大量代理连接都是可以的,这些策略在我的开源爬虫neocrawler(http://git.oschina.net/dreamidea/neocrawler)都有实现,但是这样做以后爬虫抓取的效率会很低。
问题是我不单单抓取你一个网页呀,我有一系列的网站列表用于轮询,我的爬虫还是满负荷在运转的
小水熊
小水熊

引用来自“国栋”的评论

爬虫以浏览器载入的方式,降低并发频度,间隔时间随机化,你还能识别出来吗?
间隔时间长、或者使用大量代理连接都是可以的,这些策略在我的开源爬虫neocrawler(http://git.oschina.net/dreamidea/neocrawler)都有实现,但是这样做以后爬虫抓取的效率会很低。
国栋
国栋
爬虫以浏览器载入的方式,降低并发频度,间隔时间随机化,你还能识别出来吗?
justanick
justanick
不错,思路很好,下次面临这种问题的时候借鉴一下。
关于反爬虫和恶意攻击的一些策略和思路

前段时间Guang.com经常受到恶意spider攻击,疯狂抓取网站内容,一系列机器人spam发广告,对网站性能有较大影响。 下面我说说一些反恶意spider和spam的策略和思路。 1. 通过日志分析来识别恶意...

Cnlouds
2014/03/09
0
3
[转] 互联网网站的反爬虫策略浅析

因为搜索引擎的流行,网络爬虫已经成了很普及网络技术,除了专门做搜索的Google,Yahoo,微软,百度以外,几乎每个大型门户网站都有自己的搜索引擎,大大小小叫得出来名字得就几十种,还有各...

鉴客
2010/11/24
626
0
Python 编写知乎爬虫实践

1、爬虫的基本流程 网络爬虫的基本工作流程如下: 首先选取一部分精心挑选的种子 URL 将种子 URL 加入任务队列 从待抓取 URL 队列中取出待抓取的 URL,解析 DNS,并且得到主机的 ip,并将 UR...

大数据之路
2012/06/16
0
0
2018上半年互联网恶意爬虫分析:从全景视角看爬虫与反爬虫

  导语:互联网最激烈的对抗战场,除了安全专家与黑客之间,大概就是爬虫与反爬虫领域了。据统计,爬虫流量早已超过了人类真实访问请求流量。互联网充斥着形形色色的爬虫,云上、传统行业都...

FreeBuf
2018/07/20
0
0
2个月精通Python爬虫——3大爬虫框架+6场实战+分布式爬虫,包教包会

阿里云大学在线工作坊上线,原理精讲+实操演练,让你真正掌握云计算、大数据技能。 在第一批上线的课程中,有一个Python爬虫的课程,畅销书《精通Python网络爬虫》作者韦玮,带你两个月从入门...

云木西
2018/06/27
0
0

没有更多内容

加载失败,请刷新页面

加载更多

二进制位操作

单片机,或者一些模块的设置操作,都是由一个字节数据来完成,每位各有定义。就需进行位操作来组合需要的数字结果。 以JavaScript为例,编写位操作。 我们期望得到这样一个二进制数:0101101...

format
33分钟前
3
0
聊聊中国的通信行业:从“七国八制”到“中华”脊梁

本期文章和大家一起来聊一聊我曾经从事过的通信行业吧。最近各方面信息的泛滥,包括和华为的同学聊天,自己确实也感慨颇多。想想我自己本科主修通信工程,研究生再修信息与通信工程,从本科开...

CodeSheep
今天
7
0
MDK:ARM M451M:exceed the range of code meory, continue to erase or not?

问题: 代码空间超限 几天前就遇到:exceed the range of code meory, continue to erase or not? 如下所示: 解决过程 开始以为中MDK软件的128KB限制,如是就不能生成HEX文件,应该链接时有提...

SamXIAO
今天
1
1
OSChina 周六乱弹 —— 因违反《中华人民共和国治安管理处罚法》第四十四条之规定

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @xiaoshiyue :#今日歌曲推荐# 惊艳分享谷微的单曲《安守本份》(@网易云音乐) 《安守本份》- 谷微 手机党少年们想听歌,请使劲儿戳(这里) ...

小小编辑
今天
595
12
Angular 英雄编辑器

应用程序现在有了基本的标题。 接下来你要创建一个新的组件来显示英雄信息并且把这个组件放到应用程序的外壳里去。 创建英雄组件 使用 Angular CLI 创建一个名为 heroes 的新组件。 ng gener...

honeymoose
今天
9
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部