光是用户名和密码的登陆验证方式还是很难阻止爬虫的步伐,毕竟cookie或session等会话机制的存在,使得爬虫在自动登陆后变得一劳永逸。因为不借助外力爬虫本身是很难去识别图片信息的,所以如果能结合图片验证码,会使得反爬变得更容易一点。这一篇文章我们就一起来学习下如何自己绘制出图片验证码,以及如何在登陆中应用它。
我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。
操作环境
先总结下我的操作环境:
- Centos 7
- Python 3.7
- Pycharm 2019.3
- Django 2.2
- Pillow 5.3
因为Django长期支持版本2.2LTS和前一个长期支持版本1.11LTS有许多地方不一样,需要小心区分。
Pillow绘图基本操作
虽然说有很多插件能帮助我们完成验证码这个事情,但是知其然还得知其所以然,我们通过Pillow原生绘图来理清验证码绘制和验证的原理有助于我们处理更多的绘图相关的需求。
工欲善其事必先利其器,首先来看一下Pillow这个工具画图的基本操作。
准备工具
有三个对象要准备好:
- Image对象,可以理解为一块画布
- ImageDraw对象,可以理解为一块画笔,是绘图时的主要操作对象,需要和Image对象绑定
- ImageFont对象,字体对象
首先从Pillow导入这三个类,并生成实例,需要注意的是因为历史遗留问题,Python3里面虽然安装的是Pillow,但是导入的时候还是从PIL进行导入。
from PIL import Image, ImageDraw, ImageFont
生成画布命令如下
Image.new(mode,size,color=0)
mode
可以是彩色图片用到的RGB模式,也可以是计算机处理常用的灰度图片的L模式。如果是RGB模式,所有的颜色都是三元素元组,如果是L模式,所有的颜色都是单值size
是一个二元元组,分别是pixel为单位的宽和高。需要注意计算机中的坐标都是以左上角为原点,左边为x,下面为ycolor
是画布的背景色,根据前面的mode
进行设置
例如生成一块白色画布
mode = 'RGB'
bg_size = (300, 100)
bg_color = (255, 255, 255)
img = Image.new(mode, bg_size, bg_color)
img.show()
之后显示如下
生成画笔命令如下
ImageDraw.Draw(im, mode=None)
im
是一个Image对象,画笔必须要和画布绑定才能操作mode
填一个和画布一样的模式即可
生成一个画笔,绑定上面的画布
draw = ImageDraw.Draw(img, mode)
生成字体命令如下
ImageFont.truetype(
['font=None', 'size=10', 'index=0', "encoding=''", 'layout_engine=None'],
)
通常指定字体和大小即可,而且实际赋值的时候不用写为list
font
是一个文件或类文件对象,格式为TrueType Font(.ttf)。如果是centos7,字体保存在/usr/share/fonts/
目录下size
是字体的大小
生成一个字体如下
font = ImageFont.truetype('/usr/share/fonts/google-crosextra-caladea/Caladea-Bold.ttf', 100)
工具准备妥当,下面开始正式绘制。
添加文字
添加文字命令如下,其中draw
是上面的画笔对象
draw.text(xy,text,fill=None,font=None)
xy
是一个二元元组,是文字的左上角坐标text
是文字内容,字符串对象fill
是文字颜色,根据画笔的mode
设定,如果是RGB就是三元元组font
就是ImageFont对象
按照如下设定写一段文字
xy = (0,0)
fill = (209, 115, 76)
draw.text(xy,'xiaofu',fill,font)
img.show()
结果如下
添加色彩
添加色彩只能一个pixel接一个pixel的去填充,格式如下,其中draw
是上面的画笔对象
draw.point(xy,fill=None)
xy
是一个二元元组,是像素点的位置fill
是像素点的颜色,根据画笔的mode
设定,如果是RGB就是三元元组
给背景添加一些随机颜色
for x in range(300):
for y in range(100):
fill = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
draw.point((x, y), fill=fill)
img.show()
结果如下
学习了文字和色彩的绘制,下面就可以结合两者来绘制验证码了。
绘制验证码
在项目中创建一个目录utils
专门放置一些和具体业务没啥关系的小工具,在其中新建verification_code.py
用来放置生成验证码相关的代码。
完整代码如下
import random
from PIL import Image, ImageFont, ImageDraw
def get_code():
mode = 'RGB'
bg_width = 300
bg_height = 100
bg_size = (bg_width, bg_height)
bg_color = (255, 255, 255)
ttf_path = '/usr/share/fonts/google-crosextra-caladea/Caladea-Bold.ttf'
img = Image.new(mode, bg_size, bg_color)
draw = ImageDraw.Draw(img, mode)
font = ImageFont.truetype(ttf_path, 80)
# generate text
letters = get_letters()
for index, text in enumerate(letters):
x = 75*index+10
y = 0
draw.text((x,y),text,get_rdmcolor(),font)
# blur the background
for i in range(20000):
x = random.randint(0,bg_width)
y = random.randint(0,bg_height)
fill = get_rdmcolor()
draw.point((x,y),fill)
return img,letters
def get_letters():
base = '1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM'
result = []
for i in range(4):
result.append(random.choice(base))
return result
def get_rdmcolor():
return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)
其中get_letters
用来生成一个包含四个字符的list,用来显示在验证码中,get_rdmcolor
用来生成一个随机元素的三元元组表示RGB值,画图的主要逻辑是在get_code
里面。
首先根据get_letters
返回的list,每个字符间隔一段距离排放在画布上。然后在画布上的随机20000个点涂上随机的颜色。不仅要返回图像文件,还要同时返回四个字符用来做验证码的验证。
然后创建路由和view函数来获取一下这个验证码
path('verification_code/', views.verification_code, name='verification_code'),
def verification_code(request):
img, letters = get_code()
fp = BytesIO()
img.save(fp, 'png')
print(letters)
return HttpResponse(fp.getvalue(), content_type='image/png')
没必要将验证码这种临时的文件保存在服务器的磁盘中,所以这里使用了BytesIO这个类文件对象用来保存图像,类文件对象的操作和磁盘上的文件差不多,不过是保存在内存中的,更详细的说明可以参考我的另一篇博客《python3中StringIO和BytesIO使用方法和使用场景详解》。
类似于上传文件,返回给前端的时候是Bytes类型数据,所以也需要指定下MIME类型方便浏览器用对应程序来打开。
关于上传文件的操作可以参考《【Django 020】Django2.2多文件同时上传和文件MIME判定以及数据模型中FileField和ImageField的使用详解》
最后效果如下,每刷新一次出现一个新的验证码
利用验证码登录
先说一下验证码登录的验证逻辑。
- 前端在显示登陆页面的时候会显示一张图片,
src
是前面的verification_code/
- 同时会有个
input
标签将用户输入的验证码传递给后端 - 后端在生成验证码的时候会将验证码同步保存到session中
- 如果用户输入的和session内存储的一致,成功登录
下面把这个验证码添加到登陆用的login.html
中,注意img
标签中的src
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<form action="{% url 'app:login' %}" method="post">
{% csrf_token %}
<label for="name">Name: </label><input type="text" id="name" name="name" placeholder="Your name"><br>
<label for="verification_code">Verification Code:</label><input type="text" id="verification_code"
name="verification_code" placeholder="Please type below code"><br>
<img src="{% url 'app:verification_code' %}" alt="verification code"><br>
<input type="submit">
</form>
</body>
</html>
修改下上面生成验证码的view函数如下,将letters元素连接成字符串存储在session中
def verification_code(request):
img, letters = get_code()
request.session['verification_code']=''.join(letters)
fp = BytesIO()
img.save(fp, 'png')
return HttpResponse(fp.getvalue(), content_type='image/png')
创建登录的路由和view函数如下
path('login/', views.login, name='login'),
def login(request):
if request.method == 'GET':
name = request.session.get('username', 0)
if name:
response = HttpResponseRedirect(reverse('app:homepage'))
else:
response = render(request, 'login.html')
return response
elif request.method == 'POST':
name = request.POST.get('name', 0)
code = request.POST.get('verification_code','')
if name and code == request.session.get('verification_code',''):
response = HttpResponseRedirect(reverse('app:homepage'))
request.session['username'] = name
else:
response = HttpResponseRedirect(reverse('app:login'))
return response
此时如果请求http://127.0.0.1:8000/app/login/
,结果如下
成功输入验证码之后跳转到用户homepage
如果验证码输入失败会重新跳转到登陆页面
点击更换验证码
随着验证码的马赛克技术越来越魔性,这东西看不清楚感觉都见怪不怪了,所以势必要给用户加一个点击替换的功能。
其逻辑如下,通过jQeuery给验证码图片添加一个点击事件,每次点击就刷新一下验证码图片的src
属性。但是浏览器因为自带缓存功能,当资源的路径不发生改变默认是不会主动去服务器重新获取的,所以后面还要加一个不影响功能的随机数。
在项目中创建静态文件夹static/js
,新建文件login.js
,内容如下
$(function () {
$('img').click(function () {
$(this).attr('src', '/app/verification_code/?t=' + Math.random())
})
})
这里的?t=Math.random()
就是为了增加一个随机数使得url不同,促使浏览器去掉缓存主动去服务端查询。
然后只需要去h5页面中添加jQuery和刚才的js脚本即可
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n"
crossorigin="anonymous"></script>
<script type="application/javascript" src="{% static 'js/login.js' %}"></script>
</head>
之后每次点击验证码图片都会更换一张新的图,而且图片的url也会跟着换
总结
几个重要的知识点总结一下
- Pillow这个库因为存在历史遗留问题,导入还是通过PIL
- 将小工具统一放在utils目录下,减低代码的耦合性
- 画图过程中的配置,例如长宽颜色等都可以单独做为变量来设定,提高代码的灵活度
- 通过BytesIO将不必保存在磁盘中的内容放在内存中做为类文件来处理
- Bytes传递到前端记得在HttpResponse中设定MIME类型,不然浏览器无法打开
- 通过session记录用户请求过程中和用户绑定的中间变量,例如验证码
- 浏览器的缓存原因,url不变的资源不会去主动请求,通过添加不影响请求的查询参数来解决