之前在WooYun知识库上写过一篇《初探验证码识别》,主要介绍了一些基本的验证码识别过程,并且给出了一些设计(改进)验证码时值得考虑的地方。正好最近没事在写Python Web玩时注册、登录等操作需要用到验证码,那么就借着这个机会自己写一个吧~
验证码
“验证码”又名“全自动区分计算机和人类的图灵测试”(英语:Completely Automated Public Turing test to tell Computers and Humans Apart,简称CAPTCHA),最早在2002年由卡内基梅隆大学的路易斯·冯·安、Manuel Blum、Nicholas J.Hopper以及IBM的John Langford所提出,是一种区分用户是计算机或人类的公共全自动程序。
利用其区可分人或计算机的特点,在Web应用中,验证码常被用于注册、登录、发布信息等处,以避免计算机程序恶意进行批量注册、暴力破解用户密码、发布大量打击信息等。
验证码的设计与实现
生成一个简单的验证码
首先,我们使用PIL (Python Imaging Library) 这个库来生成一张验证码图片。这里我们随机选择了4个字符并绘制了一张120*40的白色背景图,然后在相应位置绘制了指定字体和字号的字符。
1 2 3 4 5 6 7 8 9 10 11 |
def gen_captcha(self): chars = ''.join(random.sample(self.charset, 4)) image = Image.new('RGBA', (120, 40), 'white') draw = ImageDraw.Draw(image) font = ImageFont.truetype('fonts/eraserdust.ttf', 30) for i in xrange(4): draw.text((30 * i, random.randint(0, 5)), chars[i], font=font, fill='black') return chars, image |
利用上面的代码便可生成一个简单的验证码了。
但是从这个验证码字符分隔明显、前背景易区分且无其他干扰,所以它还是比较容易被识别的。因此我们需要在此基础上进一步加工这个验证码。
添加干扰
我们可以在验证码上绘制一些干扰线,ImageDraw.Draw对象的line函数可以很方便的在我们输入的两个点间绘制一条直线:
1 2 3 4 5 |
def draw_lines(line_cnt): for i in range(line_cnt): begin = (random.randint(0, self.width/2), random.randint(0, self.height)) end = (random.randint(self.width/2, self.width), random.randint(0, self.height)) draw.line([begin, end], fill=self._random_color(*self.line_color_range), width=random.randint(2, 3)) |
除了干扰线外我们还可以绘制一些干扰点,因试验发现干扰点较小很容易被降噪处理去掉,而较大又影响验证码的直观体验,所以这里采用了一些较小的字符来对验证码进行干扰:
1 2 3 4 5 6 7 |
def draw_noisy(noisy_chance): for h in xrange(self.height): for w in xrange(self.width): if random.randint(0, 1000) < noisy_chance: font = ImageFont.truetype(self.bg_font_type, 10) draw.text((w, h), random.choice(self._candidate_chars), font=font, fill=self._random_color(*self.noisy_color_range)) break |
这里以千分比的方式来控制干扰的强度。
随机字符位置
如果每个字符的位置固定的话,可以很容易的通过位置分隔开每个字符,所以字符位置最好是随机的。另外,通过投影或者连通区域的方法分隔字符的时候可以比较容易的将两个不粘连的字符分开,所以这里在随机范围的设定时,允许存在1/3的字符粘连:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
def draw_chars(): font_size = random.randint(*self.font_size) char_cnt = random.randint(*self.char_cnt) chars = random.sample(self.charset, char_cnt) self.value = ''.join(chars) font = ImageFont.truetype(random.choice(self.font_type), font_size) offset = 0 reservation_width = sum(zip(*map(font.getsize, chars))[0]) for char in chars: font_width, font_height = font.getsize(char) pos = random.randint(offset, self.width - reservation_width) x=random.randint(0, self.height - font_height if self.height - font_height > 0 else 0) draw.text( (pos, x), char, font=font, fill=self._random_color(*self.font_color_range) ) offset = pos + int(font_width * (2.0 / 3.0)) reservation_width -= int(font_width * (2.0 / 3.0)) |
颜色设置
在去干扰时,如果颜色上存在字符和干扰(线、字符等)的分界点的话那么所有干扰就失去了意义,所以在颜色设置方面要尽量使其存在交集,在不影响直观感受的情况下,交集越大越不容易被识别。
1 2 3 4 5 6 7 8 |
# 字符颜色范围 font_color_range = (64, 160) # 背景颜色范围 bg_color_range = (144, 255) # 干扰线颜色范围 line_color_range = (48, 176) # 干扰字符颜色范围 noisy_color_range = (144, 224) |
其他考虑
字符选择
因部分字体在没有相应辅助信息(如:对齐、拼写环境等)的条件下类似于:1、I、l,0、o、O,2、z、Z这类字符不易辨别,所以在设计验证码时一般会去掉此类字符以降低用户识别难度。
1 2 3 4 |
_letter_cases = "abcdefghjkmnpqrstuvwxy" _upper_cases = _letter_cases.upper() _numbers = ''.join(map(str, range(3, 10))) _candidate_chars = ''.join((_letter_cases, _upper_cases, _numbers)) |
代码
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
#!/usr/bin/env python # -*- encoding: utf-8 -*- # Author: sundachuan # E-mail: [email protected] import random import StringIO from PIL import Image, ImageDraw, ImageFont class Captcha(object): _letter_cases = "abcdefghjkmnpqrstuvwxy" _upper_cases = _letter_cases.upper() _numbers = ''.join(map(str, range(3, 10))) _candidate_chars = ''.join((_letter_cases, _upper_cases, _numbers)) font_color_range = (64, 160) bg_color_range = (144, 255) line_color_range = (48, 176) noisy_color_range = (144, 224) interfering_line_cnt = (3, 5) noisy_point_chance = (2, 5) bg_font_type = 'fonts/times.ttf' font_types = ['fonts/eraserdust.ttf', 'fonts/Sporkesso.ttf', 'fonts/Typpea.ttf'] def __init__(self, size=(150, 40), charset=_candidate_chars, font_type=font_types, font_size=(30, 40), char_cnt=4, interfering_line=True, noisy=True): self.value = None self.image = None self.image_data = None self.size = size self.width, self.height = size self.charset = charset self.font_size = font_size self.font_type = [font_type] if isinstance(font_type, str) else font_type self.char_cnt = (char_cnt, char_cnt) if isinstance(char_cnt, int) else char_cnt self.interfering_line = interfering_line self.noisy = noisy self.flush() def _random_color(self, beg, end): return (random.randint(beg, end), random.randint(beg, end), random.randint(beg, end)) def flush(self): def draw_lines(line_cnt): for i in range(line_cnt): begin = (random.randint(0, self.width/2), random.randint(0, self.height)) end = (random.randint(self.width/2, self.width), random.randint(0, self.height)) draw.line([begin, end], fill=self._random_color(*self.line_color_range), width=random.randint(2, 3)) def draw_noisy(noisy_chance): for h in xrange(self.height): for w in xrange(self.width): if random.randint(0, 1000) < noisy_chance: font = ImageFont.truetype(self.bg_font_type, 10) draw.text((w, h), random.choice(self._candidate_chars), font=font, fill=self._random_color(*self.noisy_color_range)) break def draw_chars(): font_size = random.randint(*self.font_size) char_cnt = random.randint(*self.char_cnt) chars = random.sample(self.charset, char_cnt) self.value = ''.join(chars) font = ImageFont.truetype(random.choice(self.font_type), font_size) offset = 0 reservation_width = sum(zip(*map(font.getsize, chars))[0]) for char in chars: font_width, font_height = font.getsize(char) pos = random.randint(offset, self.width - reservation_width) x=random.randint(0, self.height - font_height if self.height - font_height > 0 else 0) draw.text( (pos, x), char, font=font, fill=self._random_color(*self.font_color_range) ) offset = pos + int(font_width * (2.0 / 3.0)) reservation_width -= int(font_width * (2.0 / 3.0)) bg_color = self._random_color(*self.bg_color_range) self.image = Image.new('RGBA', self.size, bg_color) draw = ImageDraw.Draw(self.image) line_cnt = random.randint(*self.interfering_line_cnt) if self.interfering_line: draw_lines(line_cnt - line_cnt / 2) if self.noisy: draw_noisy(random.randint(*self.noisy_point_chance)) draw_chars() if self.interfering_line: draw_lines(line_cnt / 2) image_data = StringIO.StringIO() self.image.save(image_data, 'PNG') self.image_data = image_data.getvalue() def check(self, value, ignore=True): if ignore: return True if value.lower() == self.value.lower() else False else: return True if value == self.value else False if __name__ == '__main__': captcha = Captcha() print captcha.value captcha.image.show(captcha.value) print captcha.check(captcha.value) |
点击数:6397