大部分程序员可能都有过“爬虫”的经历,使用python+scrapy可以很快的实现一个爬虫程序,爬取各种心仪的数据。但做为网站或数据提供方,则是极度讨厌这种“不劳而获”的行为的,公司辛辛苦苦积累的数据,几行代码就拿走了,因此会配置各种各样的防爬虫策略。因此现实中,上演着一场又一场的攻防大战。
防爬虫功能的本质就是区分是否为正常请求,常见的几种防爬虫手段有:
- User-Agent请求代理限制;
- IP访问频率限制;
- 添加Cookie校验;
- 基于JS校验;
- 基于验证码/滑块等校验.
其中前第4、5种方法需要前端配合才能实现,我们主要讨论如何对后端进行改造,实现前3种防爬虫功能。
开发环境及软件包依赖
mac os 10.14.2
redis
python 3.7
python tornado 5.1.1
python redis 3.2.1
1. User-Agent代理限制
User Agent中文名为用户代理,简称 UA,它是一个特殊字符串头,使得服务器能够识别客户使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等。对用户代理的限制也是一种常见的手段,主要是屏蔽一些低级别爬虫,比如用python的requests库直接发请求,或者调用curl发请求等等。有两种方式可以实现该策略:
1. 在server端控制
对于tornado而言,我们可以通过直接获取User_Agent参数,因此用正则表达式进行匹配即可,示例代码如下:
user_agent = self.request.headers["User-Agent"]
if re.match("(curl|python|scrapy|http|requests)+", user_agent):
self.send_error(status_code=403)
2. 通过nginx控制
一般服务会利用nginx来做一层反向代理或负载均衡,因此也可以在nginx入口进行User_Agent的控制,我们只需修改nginx.conf文件即可,在server配置添加User_Agent的限制信息即可:
server {
listen 8005;
server_name localhost;
#charset koi8-r;
# 添加的user_agent限制
if ($http_user_agent ~* (Scrapy|Curl|HttpClient)) {
return 403;
}
#access_log logs/host.access.log main;
location / {
proxy_pass http://127.0.0.1:8002/;
}
}
2. IP访问频率控制
IP访问频率控制的思路为:设定一个记录IP在一段时间内的访问次数,如果该数值超过特定阈值,则返回403页面,禁止该IP继续访问网站。实践中,利用redis可以很方便的实现这一功能。逻辑如下:
定义访问控制时间 EXPIRE_TIME
定义访问次数阈值 LIMIT
# 判断逻辑
1. 用户首次发起正常请求
2. server端记录用户IP,检查redis中是否存在键IP:access_count;
3. 如果存在该键IP:access_count,则值加1,即incr(IP:access_count)
4. 判断IP:access_count<=LIMIT,如果为True,正常访问,否则返回错误界面
5. 如果该键不存在,在redis中创建特定键: IP:access_count,设定访问次数为1,并设定该键的超时时间为 EXPIRE_TIME
该流程中存在一个特殊配置,即键的超时时间,作用为:键IP:access_count创立后,经过一段时间(EXPIRE_TIME)后,该键自动被redis删除。即IP的访问次数会被重置,因此如果某一IP超过访问阈值后,等到键被删除后,可以继续重新访问,实现了访问频率控制以及临时封禁的功能。
思考: 如何实现IP黑名单功能?
我们直接看下这段逻辑对应的代码,这里我预设了超时时间为2分钟,访问次数限制为10次,即2分钟内单IP最多访问10次。如果某一用户已经超多10次了,不好意思,稍等一会就可以再访问了。
def frequency_count(remote_ip):
redis_key = remote_ip + ":test:access_count"
check = redis_client.exists(redis_key)
check_result = True
if check:
redis_client.incr(redis_key)
count = int(redis_client.get(redis_key))
if count > 10:
check_result = False
print "Ban Ip", remote_ip
else:
redis_client.set(redis_key, 1)
redis_client.expire(redis_key, 120)
return check_result
3. cookie添加
cookie认证也是一种防爬虫手段,思路为:对于特定页面需要传入cookie,server端进行cookie校验,通过可正常访问,否则返回错误页面。cookie认证是IP访问频率限制的一种补充。tornado中封装了可以直接调用的API,具体如下:
class CookieHandler(tornado.web.RequestHandler):
def get(self):
if not self.get_cookie("mycookie"):
self.set_cookie("mycookie", "myvalue")
self.write("Your cookie was not set yet!")
else:
self.write("Your cookie was set!")
进一步,我们可以对cookie进行加密,并设定过期时间,此时cookie需要隔一段时间进行更新,tornado的实现代码如下:
class MainHandler(tornado.web.RequestHandler):
def get(self):
if not self.get_secure_cookie("mycookie"):
self.set_secure_cookie("mycookie", "myvalue")
self.write("Your cookie was not set yet!")
else:
self.write("Your cookie was set!")
# 可以在初始化构造Application时设定秘钥
application = tornado.web.Application([
(r"/", MainHandler),
], cookie_secret="test")
思考:本小节我们只是给出了基于tornado进行cookie添加的方法,并未给出如何用cookie实现防爬虫,大家可以思考一下?
总结
在本文中,我们主要给出了User_Agent和IP访问频率限制两种防爬虫策略,并留了两处思考,我们将在下一篇文章中继续讨论,最后给出一个完整的tornado后端server,包含了以上三种方法。
# -*- coding:utf-8 -*-
import redis
import re
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import tornado.httpclient
import time
from tornado.options import define, options
define("port", default=8002, help="run on the given port", type=int)
redis_client = redis.Redis("127.0.0.1", 6379, 3, "f66sU9iP")
class IndexHandler(tornado.web.RequestHandler):
def get(self):
remote_ip = self.request.remote_ip
user_agent = self.request.headers["User-Agent"]
if re.match("([curl]|[python]|[scrapy]|[http]|[requests])+", user_agent):
self.send_error(status_code=403)
else:
flag = self.frequency_count(remote_ip)
if flag:
cookie_value = self.get_secure_cookie("cookie_value")
if not cookie_value:
self.set_secure_cookie("cookie_value", value=str(time.time()), expires_days=1)
self.write("Error access!")
self.request.finish()
else:
self.write("Your are valid user")
self.request.finish()
else:
self.send_error(status_code=403)
@staticmethod
def frequency_count(remote_ip):
redis_key = remote_ip + ":test:access_count"
check = redis_client.exists(redis_key)
check_result = True
if check:
redis_client.incr(redis_key)
count = int(redis_client.get(redis_key))
if count > 10:
check_result = False
print "Ban Ip", remote_ip
else:
redis_client.set(redis_key, 1)
redis_client.expire(redis_key, 120)
return check_result
def make_app():
tornado.options.parse_command_line()
app = tornado.web.Application(handlers=[(r"/get_cookie", IndexHandler)],
cookie_secret="test123")
http_server = tornado.httpserver.HTTPServer(app, xheaders=True)
http_server.listen(options.port)
tornado.ioloop.IOLoop.current().start()
if __name__ == "__main__":
make_app()