如何对付网络爬虫 - JavaEye和网络爬虫斗争之路

WDhongquan 2011-09-25

由于搜索引擎的泛滥,网络爬虫如今已经成为全球互联网的一大公害。除了专门做搜索的Google,Yahoo,微软,百度以外,几乎每个大型门户网站都有自己的搜索引擎,搜狐,腾讯,网易。再加上十分流氓的社区搜索奇虎等等,国内大大小小叫得出来名字得就几十家,还有各种不知名的几千几万家,另外还有国外各种奇奇怪怪的搜索引擎。只要你做的网站是内容丰富的网站,就避免不了被几千几万个爬虫每天爬来爬去。

大的搜索引擎如Google的爬取网页十分智能,爬取频率和爬取压力都没有那么高,对网站资源消耗还比较少,最怕各种各样弱智的爬虫,对网页内容的分析能力很差,经常并发几十上百个请求循环重复抓取,对网站往往是毁灭性打击。

我随便举几个例子:网易有道搜索曾经在一个上午的时间就访问了JavaEye网站60多万次请求,把网站访问拖得很慢,被我们立刻封杀。还比方说雅虎爬虫的爬行也十分弱智,经常循环爬取,爬行频率非常高,也被我们封杀掉了。然而最可怕的还是奇虎的爬虫,他托管在河北廊坊机房的服务器上面的爬虫,经常并发上百个请求同时爬取,我有次解除了对该机房的封锁,几秒钟之内,JavaEye网站就彻底无法访问,观察webservr上面堵塞了几百个来自奇虎爬虫的请求。

除了这些叫得出来名字的爬虫之外,还有很多程序员自己写的山寨爬虫,特别是一些菜鸟程序员,完全没有编写爬虫的经验,写出来的爬虫破坏力极强。曾经有一次我在JavaEye的日志里面发现一个User-Agent是Java的爬虫一天之内爬行了将近100万次动态请求。毫无疑问是个利用JDK类库编写的简单爬网页程序,由于JavaEye网站内部链接构成了回环导致该程序陷入了爬行死循环,而程序没有相应的处理代码,导致网站资源被大量消耗。

对于一个原创内容丰富,URL结构合理易于爬取的网站来说,简直就是各种爬虫的盘中大餐,很多网站的访问流量构成当中,爬虫带来的流量要远远超过真实用户访问流量,甚至爬虫流量要高出真实流量一个数量级。即使像JavaEye这样一向严厉封杀爬虫的网站,只要稍微松懈一段时间,爬虫流量就能轻易超过真实访问流量的2倍以上。对于大型互联网网站来说,有足够的硬件资源来应付爬虫带来的庞大访问压力,也有足够的资源和能力去解决这个问题。但是对于中小型互联网网站来说,爬虫带来的就是毁灭性打击了。

JavaEye网站也一直被网络爬虫问题所困扰,并且不断采用一些新的手段对付网络爬虫,网站和爬虫之间的战争就像此消彼长的拉锯战一样。

一、野蛮型爬虫

在2006年的时候,JavaEye遭遇的网络爬虫基本上都是比较野蛮的爬虫,动不动上百个并发请求一起过来,网站立刻被拖慢或者干脆无法访问,例如奇虎的爬虫就是这样(百度的爬虫早期也是如此,现在已经斯文多了)。这种爬虫是很容易识别出来的,通过netstat信息查看,或者webserver提供的并发连接信息,比方说lighttpd的mod_status就可以非常直观的观察到当前每个并发连接的状态,请求的地址和IP,以及连接时间。

对付这种野蛮的爬虫其实没有什么太好的办法,只有一种办法,就是直接封杀。然而爬虫往往并不分布在一台服务器上,而是很多台服务器上面,因此你封掉一个ip地址根本不解决问题,所以我们采取的办法就是直接封杀爬虫所在的C网段地址,例如:

C代码

iptables-AINPUT-ieth0-jDROP-ptcp--dport80-s84.80.46.0/24

iptables-AINPUT-ieth0-jDROP-ptcp--dport80-s84.80.46.0/24

除此之外还可以采取一些辅助的解决办法,比方说在webserver上面限制每IP并发连接数量,如果超过一定的并发连接数量,就直接返回拒绝请求的页面。例如lighttpd可以这样配置:

C代码

$HTTP["url"]=~"^/topics/download/"{

evasive.max-conns-per-ip=2

}

$HTTP["url"]=~"^/topics/download/"{

evasive.max-conns-per-ip=2

}

限定每IP只能并发一个线程下载。

总的来说,这种蛮不讲理的爬虫相对比较稀少,碰到一个封杀一个C段地址基本可以解决此类爬虫。现在JavaEye已经很少遇到这种爬虫了。

这里要特别说明一点:有很多人提出一种极度脑残的观点,说我要惩罚这些爬虫。我专门在网页里面设计不消耗资源的静态循环链接页面,让爬虫掉进陷阱,死循环爬不出来。能出这种弱智点子的人一看就知道纸上谈兵。根本用不着你设置陷阱,弱智爬虫对正常网页自己就爬不出来,你这样做多此一举不说,而且会让真正的搜索引擎降低你的网页排名。

而且运行一个爬虫根本不消耗什么机器资源,我在自己的笔记本电脑上面跑个Java程序,发起上百个线程,就算死循环了,也消耗不了多少CPU,根本不消耗我什么。相反,真正宝贵的是你的服务器CPU资源和服务器带宽,谁消耗谁阿?做程序员最可怕的不是弱智,而是自己不知道自己弱智,总以为自己很明智。

二、爬虫的海量抓取和海量的各种小爬虫

有很多智能程度比较低的爬虫,比方说雅虎和网易有道的爬虫,它虽然并不会以很高的并发连接爬取你的网站,但是它会以较低的频率持续不间断爬取网站,一天下来至少爬取几十万页面,极大消耗了服务器资源,拖慢服务器的响应速度。而且由于它爬取的并发不高,一般不容易暴露自己,特别是雅虎的爬虫,分布很广,来自大约二十几个C段地址,狡兔n窟,你很难找全它所有的C段地址,因此通过简单的封杀IP地址段,对这种爬虫基本无效。

另外还有很多各种各样的小爬虫,特别是以国外的各式各样稀奇古怪的搜索引擎为主,它们都在尝试Google以外创新的搜索方式,每个爬虫每天爬取你几万的网页,几十个爬虫加起来每天就能消耗掉你上百万动态请求的资源。由于每个小爬虫单独的爬取量都很低,所以你很难把它从每天海量的访问IP地址当中把它准确的挖出来,因此也没有办法通过直接封杀IP的方式对付它们。

怎么解决这个问题呢?其实这些爬虫都有一个共同的特点,在爬取网页的时候,会声明自己的User-Agent信息。我们知道每个浏览器都有自己独一无二的User-Agent信息,比较正规的爬虫,特别是来自国外的爬虫都比较规矩,会声明自己的User-Agent信息,因此我们就可以通过记录和分析User-Agent信息来挖掘和封杀这些爬虫。

首先我们需要记录每个请求的User-Agent信息,对于用rails开发的JavaEye网站来说这很简单,我们在app/controllers/application.rb里面添加一个全局的before_filter,来记录每个请求的User-Agent信息:

Ruby代码

logger.info"HTTP_USER_AGENT#{request.env["HTTP_USER_AGENT"]}"

logger.info"HTTP_USER_AGENT#{request.env["HTTP_USER_AGENT"]}"

这样就会把每个请求的User-Agent信息记录到production.log里面去。

然后我们统计每天的production.log,抽取User-Agent信息,找出访问量最大的那些User-Agent。但是这里要注意的是我们只关注那些爬虫的User-Agent信息,而不是真正浏览器User-Agent,所以我们还要排除掉浏览器User-Agent,最后我们就可以得到一个访问量最多的爬虫列表。要做到这一点仅仅需要一行shell:

C代码

grepHTTP_USER_AGENTproduction.log|grep-v-E"MSIE|Firefox|Chrome|Opera|Safari|Gecko"|sort|uniq-c|sort-r-n|head-n100>bot.log

grepHTTP_USER_AGENTproduction.log|grep-v-E"MSIE|Firefox|Chrome|Opera|Safari|Gecko"|sort|uniq-c|sort-r-n|head-n100>bot.log

这行shell命令从production.log里面抽取包含User-Agent的日志,然后排除真实浏览器的User-Agent,再统计访问量,然后按照访问量从大到小排序,最后挑选排名前100的记录到日志文件里面去。或者你也可以直接把输出内容发送到你的邮箱里面去。

最终的爬虫统计结果类似下面这样:

C代码

57335HTTP_USER_AGENTBaiduspider+(+http://www.baidu.com/search/spider.htm)

56639HTTP_USER_AGENTMozilla/5.0(compatible;Googlebot/2.1;+http://www.google.com/bot.html)

42610HTTP_USER_AGENTMediapartners-Google

19131HTTP_USER_AGENTmsnbot/2.0b(+http://search.msn.com/msnbot.htm)

8980HTTP_USER_AGENTMozilla/5.0(compatible;YoudaoFeedFetcher/1.0;http://www.youdao.com/help/reader/faq/topic006/;1subscriber;)

8034HTTP_USER_AGENTSosoblogspider+(+http://help.soso.com/soso-blog-spider.htm)

7847HTTP_USER_AGENTmsnbot/1.1(+http://search.msn.com/msnbot.htm)

4342HTTP_USER_AGENTMozilla/5.0(compatible;GoogleDesktop)

3183HTTP_USER_AGENT

3115HTTP_USER_AGENTMozilla/4.0

2900HTTP_USER_AGENTWordPress/2.7

2096HTTP_USER_AGENTApple-PubSub/65.11

1891HTTP_USER_AGENTZhuaxia.com1Subscribers

1201HTTP_USER_AGENTApple-PubSub/65

1154HTTP_USER_AGENTMozilla/5.0(compatible;YoudaoFeedFetcher/1.0;http://www.youdao.com/help/reader/faq/topic006/;2subscribers;)

1059HTTP_USER_AGENTFeedBurner/1.0(http://www.FeedBurner.com)

57335HTTP_USER_AGENTBaiduspider+(+http://www.baidu.com/search/spider.htm)

56639HTTP_USER_AGENTMozilla/5.0(compatible;Googlebot/2.1;+http://www.google.com/bot.html)

42610HTTP_USER_AGENTMediapartners-Google

19131HTTP_USER_AGENTmsnbot/2.0b(+http://search.msn.com/msnbot.htm)

8980HTTP_USER_AGENTMozilla/5.0(compatible;YoudaoFeedFetcher/1.0;http://www.youdao.com/help/reader/faq/topic006/;1subscriber;)

8034HTTP_USER_AGENTSosoblogspider+(+http://help.soso.com/soso-blog-spider.htm)

7847HTTP_USER_AGENTmsnbot/1.1(+http://search.msn.com/msnbot.htm)

4342HTTP_USER_AGENTMozilla/5.0(compatible;GoogleDesktop)

3183HTTP_USER_AGENT

3115HTTP_USER_AGENTMozilla/4.0

2900HTTP_USER_AGENTWordPress/2.7

2096HTTP_USER_AGENTApple-PubSub/65.11

1891HTTP_USER_AGENTZhuaxia.com1Subscribers

1201HTTP_USER_AGENTApple-PubSub/65

1154HTTP_USER_AGENTMozilla/5.0(compatible;YoudaoFeedFetcher/1.0;http://www.youdao.com/help/reader/faq/topic006/;2subscribers;)

1059HTTP_USER_AGENTFeedBurner/1.0(http://www.FeedBurner.com)

从日志就可以直观的看出主要是Google,baidu,微软msn,网易有道和腾讯搜搜的爬虫,以及每个爬虫爬取的请求次数。通过这个简单的办法,你就可以有效的窥视每个爬虫的动向,如果哪个爬虫不老实,胆敢疯狂爬取的话,你就可以一眼把它挑出来。

要根据User-Agent信息来封杀爬虫是件很容易的事情,主流的WebServer都支持针对User-Agent信息的设置,JavaEye使用的是lighttpd,因此用以下的lighttpd配置来封杀爬虫:

C代码

$HTTP["useragent"]=~"qihoobot|^Java|Commons-HttpClient|Wget|^PHP|Ruby|Python"{

url.rewrite=("^/(.*)"=>"/crawler.html")

}

$HTTP["useragent"]=~"qihoobot|^Java|Commons-HttpClient|Wget|^PHP|Ruby|Python"{

url.rewrite=("^/(.*)"=>"/crawler.html")

}

使用这种方式来封杀爬虫简单而有效,JavaEye已经根据User-Agent信息封杀了70多种网络爬虫。而且这种方式可以有效的挡住那些程序员菜鸟编写的垃圾爬虫,因为他们一般情况下并不知道声明User-Agent信息,所以爬虫会直接把编程语言的名称或者库的名称作为User-Agent发送过来,我们只要封杀常用编程语言和网络库的名称就OK了。

通过以上的两种方式已经可以基本解决网络爬虫造成的困扰,特别是可以解决掉绝大部分并不专门针对你的网站进行爬取的网络爬虫。但是如果一个爬虫有针对性的非要爬取你的网站不可,它也可以通过修改User-Agent信息的方式伪装自己,从而避开你的封杀。为了尽最大可能性不暴露自己,它甚至会寻找几十上百个代理服务器采取分布式爬取的策略,在这种情况下,我们上面的封杀策略就失效了,必须采取新的手段。

三、善于伪装的爬虫

在上面提到,如果爬虫伪装自己的User-Agent信息,就必须寻找新的办法来封杀爬虫了。事实上对网站来说,最大的挑战就是如何准确的甄别一个IP发起的请求,究竟是真实用户访问还是爬虫访问呢?

先说点题外话,在很多年以前(2000年),我就做过网站流量统计系统。主流的网站流量统计系统不外乎两种策略:一种策略是在网页里面嵌入一段js,这段js会向特定的统计服务器发送请求的方式记录访问量;另一种策略是直接分析服务器日志,来统计网站访问量。

在理想的情况下,嵌入js的方式统计的网站流量应该高于分析服务器日志,这是因为用户浏览器会有缓存,不一定每次真实用户访问都会触发服务器的处理。但实际情况是,分析服务器日志得到的网站访问量远远高于嵌入js方式,极端情况下,甚至要高出10倍以上。

现在很多网站喜欢采用awstats来分析服务器日志,来计算网站的访问量,但是当他们一旦采用GoogleAnalytics来统计网站流量的时候,却发现GA统计的流量远远低于awstats,所以开始怀疑GA的准确性。其实GA的统计确实会略低于真实的用户访问量,但数据的真实性比较靠谱,不会偏差特别大。之所以略低是因为GA的服务器有时候用户访问不到,还有一种情况是访问JavaEye的用户所在公司使用了白名单,他能访问JavaEye却无法访问GA服务器,此外也有可能用户还没有等到GA加载就跳转到下一页了,所以统计量没有被GA计算。

那么为什么GA和awstats统计会有这么大差异呢?罪魁祸首就是把自己伪装成浏览器的网络爬虫。一些网络爬虫为了避免被网站以识别User-Agent的方式封杀,就修改了自己的User-Agent信息,通常伪装成WindowsXP上的IE6浏览器,也有伪装成Firefox浏览器的。这种情况下awstats无法有效的识别了,所以awstats的统计数据会虚高。不过说句公道话,这也怪不了awstats,只怪爬虫太狡猾,不但awstats无法有效识别,就算我们肉眼去查看日志,也往往无法有效识别。

因此作为一个网站来说,如果希望了解自己的网站真实访问量,希望精确了解网站每个频道的访问量和访问用户,开发自己的网站流量统计系统就显得非常有必要性。JavaEye网站就开发了自己的网站流量统计系统,采用在页面里面嵌入js的方式来实现网站流量统计。因此我们可以精确的掌握登录用户和非登录用户的比例,不同的访问偏好,JavaEye每个频道精确的流量和比例,真实的用户数量和分布等GA无法提供的有价值的信息。

JavaEye自己的流量统计系统尽管并不是为了甄别爬虫而编写的,但是他可以帮助甄别网络爬虫。我们知道只有用户使用浏览器访问页面的时候才会执行js,而爬虫并不会执行页面里面的js,所以rails的production.log里面出现的IP地址,却并没有相应的流量统计系统记录这个IP地址,我们可以99%的断定这个IP是个爬虫。如果爬虫编写者专门伪装真实IP向流量统计服务器发起请求的话,流量统计系统也有自己的防范作¥弊的机制,以及数据分析机制来甄别异常的访问请求,这点就不展开讨论了。

总之有了JavaEye流量统计系统提供的真实IP作为参考标准,我们就可以拿日志里面出现的IP地址进行比较,如果日志里面某个IP发起了大量的请求,在流量统计系统里面却根本找不到,或者即使找得到,可访问量却只有寥寥几个,那么这无疑就是一个网络爬虫,我们可以直接用iptables封杀该C段地址了。

根据这种策略,我们可以重新调整封杀方案。首先统计production.log,统计访问最多的200个C段地址,这仅仅需要一条shell命令:

C代码

grepProcessingproduction.log|awk"{print$4}"|awk-F".""{print$1"."$2"."$3".0"}"|sort|uniq-c|sort-r-n|head-n200>stat_ip.log

grepProcessingproduction.log|awk"{print$4}"|awk-F".""{print$1"."$2"."$3".0"}"|sort|uniq-c|sort-r-n|head-n200>stat_ip.log

这200个C段地址就是我们需要重点考察的对象,网络爬虫就混迹在这200个C段地址之内。它的格式大致如下,显示访问请求最多的C段IP地址和请求次数:

C代码

99650203.208.60.0

55813123.125.66.0

21131221.235.58.0

1836072.14.199.0

14632121.0.29.0

11789202.165.185.0

1053961.135.216.0

1015365.55.106.0

700165.55.211.0

424065.55.207.0

3789219.133.0.0

3721194.8.74.0

99650203.208.60.0

55813123.125.66.0

21131221.235.58.0

1836072.14.199.0

14632121.0.29.0

11789202.165.185.0

1053961.135.216.0

1015365.55.106.0

700165.55.211.0

424065.55.207.0

3789219.133.0.0

3721194.8.74.0

然后我们还需要流量统计系统提供的真实IP地址的C段地址作为参考,这已经由流量统计系统提供给我们了。

接着我们还需要准备一个白名单列表,比方说Google和百度的爬虫IP地址段,对于这些爬虫,我们给予放行,究竟放行哪些爬虫,就需要完全根据自己网站的情况而定了。例如JavaEye现在的白名单(还在不断添加中):

C代码

60.28.204.0抓虾

61.135.163.0百度

61.135.216.0有道

65.55.106.0微软

65.55.207.0微软

65.55.211.0微软

66.249.66.0Google

72.14.199.0Google

121.0.29.0阿里巴巴

123.125.66.0百度

124.115.10.0腾讯搜搜

124.115.11.0腾讯搜搜

124.115.12.0腾讯搜搜

203.208.60.0Google

209.85.238.0Google

219.239.34.0鲜果

220.181.50.0百度

220.181.61.0搜狗

60.28.204.0抓虾

61.135.163.0百度

61.135.216.0有道

65.55.106.0微软

65.55.207.0微软

65.55.211.0微软

66.249.66.0Google

72.14.199.0Google

121.0.29.0阿里巴巴

123.125.66.0百度

124.115.10.0腾讯搜搜

124.115.11.0腾讯搜搜

124.115.12.0腾讯搜搜

203.208.60.0Google

209.85.238.0Google

219.239.34.0鲜果

220.181.50.0百度

220.181.61.0搜狗

最后我们还需要准备一个IP地址库,对于那些被我们揪出来的爬虫,我们还需要甄别一下他的身份,它究竟是一个恶意的爬虫,还是一个未被我们放入白名单的合法爬虫呢?IP地址库很容易从互联网下载一份,所以也不展开讨论了。总之有了这些素材,我们要甄别网络爬虫就十分简单了,仅仅十几行ruby代码就搞定了:

Ruby代码

whitelist=[]

IO.foreach("#{RAILS_ROOT}/lib/whitelist.txt"){|line|whitelist<<line.split[0].stripifline}

realiplist=[]

IO.foreach("#{RAILS_ROOT}/log/visit_ip.log"){|line|realiplist<<line.stripifline}

iplist=[]

IO.foreach("#{RAILS_ROOT}/log/stat_ip.log")do|line|

ip=line.split[1].strip

iplist<<ipifline.split[0].to_i>3000&&!whitelist.include?(ip)&&!realiplist.include?(ip)

end

Report.deliver_crawler(iplist)

whitelist=[]

IO.foreach("#{RAILS_ROOT}/lib/whitelist.txt"){|line|whitelist<<line.split[0].stripifline}

realiplist=[]

IO.foreach("#{RAILS_ROOT}/log/visit_ip.log"){|line|realiplist<<line.stripifline}

iplist=[]

IO.foreach("#{RAILS_ROOT}/log/stat_ip.log")do|line|

ip=line.split[1].strip

iplist<<ipifline.split[0].to_i>3000&&!whitelist.include?(ip)&&!realiplist.include?(ip)

end

Report.deliver_crawler(iplist)

代码的实现很简单,就是读入访问请求次数超过3000次的C段地址,根据经验来说,超过3000次的访问请求已经非常可疑了。然后去掉白名单里面的C段地址,再去掉出现在真实访问列表中的IP地址段,最后剩下来的就是高度可疑的C段地址了。对于这些地址查询IP地址数据库信息,再格式化成报告自动给我发送电子邮件。

最后需要人肉的简单识别,比方说某地址的来源信息显示为“Google公司总部”,那么我就知道这个地址需要添加到白名单里面。除去这些可以肉眼识别的地址,剩下来的就可以统统干掉了。

另外,对于这个简单的程序还需要进一步完善,比方说不是简单的根据realiplist进行比对和排除,而是给realiplist也建立一个ip段的统计信息,即使该段地址有真实访问量,仍然需要进一步甄别,用该地址的请求数量除以realiplist里面的访问数量,如果倍数大于一个阀值比方说1000,就可以断定仍然是网络爬虫。

四、使用浏览器内核驱动的网络爬虫

有人在文章后面的评论里面提到一种新的爬虫的爬取方式,就是不用程序去爬取,而是编程控制一个真正的浏览器内核去爬取网站,由于浏览器内核可以真正执行js,所以会被识别为真实用户访问,从而避开网站的检查机制。这种爬虫是最难以甄别的爬虫,如果精心编写,甚至可以欺骗Google的服务器。由于Safari的webkit浏览器内核和Firefox的Gecko浏览器内核都是开源的,因此一个水平比较高的程序员自己动手编写程序驱动一个真实的浏览器内核作为爬虫并不是非常困难的事情。

实际上这种爬虫我们也遇到过几次,但也并不难以甄别,只是需要一定的手工甄别机制,难以用程序全部自动化甄别。我们知道一个网站的真实用户访问量如果没有短期的市场推广活动,那么会保持一个比较稳定的水平,如果网站的访问量出现一个比较大的跳跃,或者网站频道之间的访问比例出现突变,就可以99%断定有此类爬虫出现了。

那么要甄别它也很简单,对真实访问IP进行统计和排序,挑选出来前200名C段IP地址中每天访问量超过3000次的IP段地址,然后去除白名单,最后再用IP地址数据库去比对。根据经验来说,一个C段地址每天超过3000次访问已经肯定是一个大公司在访问JavaEye了,可如果该来源C段并非出自像阿里巴巴,IBM中国公司,搜狐,腾讯这样的公司地址,就可以99%断定是网络爬虫,直接用iptables干掉该C段地址。

总之,通过这种方式目前已经可以有效甄别伪装的网络爬虫,以及通过n多国外代理服务器的分布式网络爬虫,不过网站和爬虫之间的战争永远不会结束,我们可以通过每天的日志报告来检测网站的运行状况,一旦发现数据报告异常,就知道有新的爬虫出现,那么就可以通过日志分析寻找封杀它的新办法。

相关推荐