- 系統版本: CentOS 7.4
- Python版本: Python 3.6.1
在現在的WEB中,為了防止爬蟲類程序提交表單,圖片驗證碼是最常見也是最簡單的應對方法之一。
1.驗證碼圖片的生成
在python中,圖片驗證碼一般用PIL或者Pillow庫實現,下面就是利用Pillow生成圖片驗證碼的代碼:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Author : Yang
# @Time : 2017/11/06 1:04
import random
from PIL import Image, ImageDraw, ImageFont, ImageFilter
_letter_cases = "abcdefghjkmnpqrstuvwxy" # 小寫字母,去除可能干擾的i,l,o,z
_upper_cases = _letter_cases.upper() # 大寫字母
_numbers = ''.join(map(str, range(10))) # 數字
init_chars = ''.join((_letter_cases, _upper_cases, _numbers))
def create_validate_code(size=(120, 30),
chars=init_chars,
img_type="GIF",
mode="RGB",
bg_color=(230, 230, 230),
fg_color=(18, 18, 18),
font_size=20,
font_type=‘/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf’,
length=4,
draw_lines=True,
n_line=(1, 2),
draw_points=True,
point_chance=1):
'''
@todo: 生成驗證碼圖片
@param size: 圖片的大小,格式(寬,高),默認為(120, 30)
@param chars: 允許的字符集合,格式字符串
@param img_type: 圖片保存的格式,默認為GIF,可選的為GIF,JPEG,TIFF,PNG
@param mode: 圖片模式,默認為RGB
@param bg_color: 背景顏色,默認為白色
@param fg_color: 前景色,驗證碼字符顏色,默認為藍色#0000FF
@param font_size: 驗證碼字體大小
@param font_type: 驗證碼字體的詳細路徑,默認為 ae_AlArabiya.ttf
@param length: 驗證碼字符個數
@param draw_lines: 是否划干擾線
@param n_lines: 干擾線的條數范圍,格式元組,默認為(1, 2),只有draw_lines為True時有效
@param draw_points: 是否畫干擾點
@param point_chance: 干擾點出現的概率,大小范圍[0, 100]
@return: [0]: PIL Image實例
@return: [1]: 驗證碼圖片中的字符串
'''
width, height = size # 寬, 高
img = Image.new(mode, size, bg_color) # 創建圖形
draw = ImageDraw.Draw(img) # 創建畫筆
def get_chars():
'''生成給定長度的字符串,返回列表格式'''
return random.sample(chars, length)
def create_lines():
'''繪制干擾線'''
line_num = random.randint(*n_line) # 干擾線條數
for i in range(line_num):
# 起始點
begin = (random.randint(0, size[0]), random.randint(0, size[1]))
# 結束點
end = (random.randint(0, size[0]), random.randint(0, size[1]))
draw.line([begin, end], fill=(0, 0, 0))
def create_points():
'''繪制干擾點'''
chance = min(100, max(0, int(point_chance))) # 大小限制在[0, 100]
for w in range(width):
for h in range(height):
tmp = random.randint(0, 100)
if tmp > 100 - chance:
draw.point((w, h), fill=(0, 0, 0))
def create_strs():
'''繪制驗證碼字符'''
c_chars = get_chars()
strs = ' %s ' % ' '.join(c_chars) # 每個字符前后以空格隔開
font = ImageFont.truetype(font_type, font_size)
font_width, font_height = font.getsize(strs)
draw.text(((width - font_width) / 3, (height - font_height) / 3),
strs, font=font, fill=fg_color)
return ''.join(c_chars)
if draw_lines:
create_lines()
if draw_points:
create_points()
strs = create_strs()
# 圖形扭曲參數
params = [1 - float(random.randint(1, 2)) / 100,
0,
0,
0,
1 - float(random.randint(1, 10)) / 100,
float(random.randint(1, 2)) / 500,
0.001,
float(random.randint(1, 2)) / 500
]
img = img.transform(size, Image.PERSPECTIVE, params) # 創建扭曲
img = img.filter(ImageFilter.EDGE_ENHANCE_MORE) # 濾鏡,邊界加強(閾值更大)
return img, strs
if __name__ == '__main__':
img, str = create_validate_code()
img.save('./test.gif', 'gif')
最后的結果會返回一個元組,第一個返回值為一個Image類的實例,第二個返回值為驗證碼圖片中的字符串,可以用於比對驗證碼是否正確。
生成的驗證碼圖片效果:
但是需要注意一點,以上代碼需要依賴於系統字體,如果 font_type設置不正確,就會拋出 OSError 異常。
對於CenOS系統,字體文件一般在 /usr/share/fonts/dejavu/ 下, 如CentOS 7.4:
從中隨意選取一個即可。windows 下同理,只需將 font_type 設置成正確的字體路徑即可, 如 font_type=r"C:\Windows\Fonts\Arial.ttf"
2.如何在網頁中顯示驗證碼
在上述代碼中,驗證碼都是以文件的方式保存。如果要在web中使用驗證碼,不可能每次都先生成驗證碼圖片,先保存到磁盤,再返回給前端 web。這樣會增加磁盤的開銷,另外頻繁產生的驗證碼也會占用大量的磁盤空間。這時,可以使用 BytesIO 模塊,使驗證碼圖片的讀寫直接在內存中進行,並直接返回給前端。同時將正確驗證碼字符串存在session中,當用戶提交表單時,就可以和session中的正確字符串作比較了。
以Flask為例,以下為在Flask中使用驗證碼的完整 Demo:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Author : Yang
# @Time : 2017/11/08 15:35
import random
from PIL import Image, ImageDraw, ImageFont, ImageFilter
from io import BytesIO
from flask import Flask, session, request
_letter_cases = "abcdefghjkmnpqrstuvwxy" # 小寫字母,去除可能干擾的i,l,o,z
_upper_cases = _letter_cases.upper() # 大寫字母
_numbers = ''.join(map(str, range(10))) # 數字
init_chars = ''.join((_letter_cases, _upper_cases, _numbers))
def create_validate_code(size=(120, 30),
chars=init_chars,
img_type="GIF",
mode="RGB",
bg_color=(230, 230, 230),
fg_color=(18, 18, 18),
font_size=20,
font_type='/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf',
length=4,
draw_lines=True,
n_line=(1, 2),
draw_points=True,
point_chance=1):
'''
@todo: 生成驗證碼圖片
@param size: 圖片的大小,格式(寬,高),默認為(120, 30)
@param chars: 允許的字符集合,格式字符串
@param img_type: 圖片保存的格式,默認為GIF,可選的為GIF,JPEG,TIFF,PNG
@param mode: 圖片模式,默認為RGB
@param bg_color: 背景顏色,默認為白色
@param fg_color: 前景色,驗證碼字符顏色,默認為藍色#0000FF
@param font_size: 驗證碼字體大小
@param font_type: 驗證碼字體的詳細路徑,默認為 ae_AlArabiya.ttf
@param length: 驗證碼字符個數
@param draw_lines: 是否划干擾線
@param n_lines: 干擾線的條數范圍,格式元組,默認為(1, 2),只有draw_lines為True時有效
@param draw_points: 是否畫干擾點
@param point_chance: 干擾點出現的概率,大小范圍[0, 100]
@return: [0]: PIL Image實例
@return: [1]: 驗證碼圖片中的字符串
'''
width, height = size # 寬, 高
img = Image.new(mode, size, bg_color) # 創建圖形
draw = ImageDraw.Draw(img) # 創建畫筆
def get_chars():
'''生成給定長度的字符串,返回列表格式'''
return random.sample(chars, length)
def create_lines():
'''繪制干擾線'''
line_num = random.randint(*n_line) # 干擾線條數
for i in range(line_num):
# 起始點
begin = (random.randint(0, size[0]), random.randint(0, size[1]))
# 結束點
end = (random.randint(0, size[0]), random.randint(0, size[1]))
draw.line([begin, end], fill=(0, 0, 0))
def create_points():
'''繪制干擾點'''
chance = min(100, max(0, int(point_chance))) # 大小限制在[0, 100]
for w in range(width):
for h in range(height):
tmp = random.randint(0, 100)
if tmp > 100 - chance:
draw.point((w, h), fill=(0, 0, 0))
def create_strs():
'''繪制驗證碼字符'''
c_chars = get_chars()
strs = ' %s ' % ' '.join(c_chars) # 每個字符前后以空格隔開
font = ImageFont.truetype(font_type, font_size)
font_width, font_height = font.getsize(strs)
draw.text(((width - font_width) / 3, (height - font_height) / 3),
strs, font=font, fill=fg_color)
return ''.join(c_chars)
if draw_lines:
create_lines()
if draw_points:
create_points()
strs = create_strs()
# 圖形扭曲參數
params = [1 - float(random.randint(1, 2)) / 100,
0,
0,
0,
1 - float(random.randint(1, 10)) / 100,
float(random.randint(1, 2)) / 500,
0.001,
float(random.randint(1, 2)) / 500
]
img = img.transform(size, Image.PERSPECTIVE, params) # 創建扭曲
img = img.filter(ImageFilter.EDGE_ENHANCE_MORE) # 濾鏡,邊界加強(閾值更大)
return img, strs
app = Flask(__name__)
app.config.update(
DEBUG=True,
SECRET_KEY='...'
)
@app.route('/')
def index():
return 'test'
@app.route('/code')
def get_code():
# 把strs發給前端,或者在后台使用session保存
code_img, strs = create_validate_code()
buf = BytesIO()
code_img.save(buf, 'jpeg')
buf_str = buf.getvalue()
response = app.make_response(buf_str)
response.headers['Content-Type'] = 'image/gif'
session['img'] = strs.upper()
return response
@app.route("/login", methods=["POST", "GET"])
def login():
if request.method == 'POST':
if session.get('img') == request.form.get('img').upper():
return 'OK'
return 'Error'
return """
<form action="" method="post">
<p>Name:<input type=text name=username>
<p>Password:<input type=text name=password>
<p>CAPTCHA:<input type=text name=img>
<img id="verficode" src="./code" onclick="this.src='./code?'+Math.random()"> # onclick事件用於每次點擊時獲取一個新的驗證碼
<p><input type=submit value=Login>
</form>
"""
if __name__ == "__main__":
app.run(host="0.0.0.0", port=18888, debug=True)
最終效果: