有次帮某人发spam,找人来填调查。写了个自动评论的脚本,但是发出一定数量的评论之后就会遭遇验证码,于是决定破解之。
思路也是一般的转化切割比对,成功率不是很高,不过重试几次也是可以用的。
虽然已经控制好了频率,不过最后还是被管理员发现了,直接封了帐号(好在是临时注册的),再注册再封,后来干脆封IP,
于是不得不给我的vps换了个ip(也好在是免费的),杯具。
闲话休说,言归正题。
首先是需要取得验证码的样本,以作训练特征之用。而要取得验证码,首先要模拟登录的请求:
usr = 'xx' psw = 'oo'
resp = urllib2.urlopen('https://login.sina.com.cn/sso/login.php?username=%s&password=%s&returntype=TEXT' %
( usr, psw)) cookie = Cookie.SimpleCookie(resp.headers['set-cookie']) headers = { 'Referer': 'http://t.sina.com.cn', 'Cookie': cookie_header(cookie),
'User-Agent': 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.3pre) Gecko/20100405 Firefox/3.6.3plugin1', } def cookie_header(cookie):
ret = '' for v in cookie.values(): ret += "%s=%s; " % (v.key, v.value) return ret
headers就是后续的请求中,需要传递的参数了。
取回一些验证码样本:
for i in xrange(100):
req_img = urllib2.Request('http://t.sina.com.cn/pincode/pin.php?lang=zh&r=%d&rule' % int(time() * 1000),
headers = headers) res_img = urllib2.urlopen(req_img) f = open('xinlang_pincode/%d.png' % i, 'wb') f.write(res_img.read()) f.close()
有一些验证码的回答是中文,中国首都什么的,这些不处理,直接返回失败。因为可以重复获取重新识别,不成问题的。下面是处理算术问题验证码的方法:
先进行变换处理:
from PIL import Image, ImageFilter, ImageEnhance file = 'xinlang_pincode/0.png' im = Image.open(file) im = im.convert()
enhancer = ImageEnhance.Brightness(im) im = enhancer.enhance(2.0) #加亮,效果见图1 enhancer = ImageEnhance.Contrast(im) im = enhancer.enhance(4)
#提高对比度,效果见图2 im = im.convert('1') #二值化,效果见图3 im = im.filter(ImageFilter.MedianFilter) #中值去噪,效果见图4 im.show()
#调用xv命令来显示图片,方便debug
图1:
图2:
图3:
图4:
这样处理过之后,图片背景中的色块被过滤掉,杂点也被过滤掉,而数字的形状也没有太大的损失。
下面是分解字符,也就是将每一个数字或者+-*等符号分解出来:
imim = im.load() WIDTH = 250 HEIGHT = 50 i = 0 has_start = False chars = [] while i < WIDTH: all_none = True for j in xrange(HEIGHT): if imim[i, j] != 255:
all_none = False if all_none: if has_start: end_x = i has_start = False char = im.crop((start_x, 0, end_x, HEIGHT))
char.show() #到这一步的效果见图5 charchar = char.load() width = end_x - start_x y1 = 0 y2 = HEIGHT - 1 all_none = True
while all_none: for ii in xrange(width): if charchar[ii, y1] != 255: all_none = False y1 += 1 all_none = True while all_none:
for ii in xrange(width): if charchar[ii, y2] != 255: all_none = False y2 -= 1 char = char.crop((0, y1 - 1, width, y2 + 2))
char = char.resize((20, 20)) #将图片缩放到统一的大小 char.show() #到这一步的效果见图6 chars.append(char) else: if not has_start:
start_x = i has_start = True i += 1
图5:字符被独立分割开
图6:字符上下两边的空白被去掉,且缩放到同一大小
这一步得到的chars是下面要用到的。
然后是训练,也就是形成特征库。特征库规模越大,识别率也越高。不过训练起来也挺累的,有几十上百条也就好了。至少0到9和+-*=等几个字符的特征都要有:
file = open('xinlang.img', 'a') for c in chars: nstr = '' im_loaded = c.load() for x in range(20): for y in range(20): if im_loaded[x, y] == 255:
nstr += '0' else: nstr += '1' c.show() n = raw_input('? ') file.write(nstr+':'+n+'\n') file.close()
这里的特征,就是直接把每一个像素的信息,用0和1组成的字符串表示。
训练的结果是一个文本文件,记录了对应的特征和字符,用于下面的比对。
比对函数:
pattern = [] for l in open('xinlang.img', 'r').read().split('\n'): pattern.append(l.split(':')) del pattern[-1] def what(img): im = img.load() nstr = ''
for x in xrange(20): #生成目标图像的特征字符串 for y in xrange(20): if im[x, y] == 255: nstr += '0' else: nstr += '1' minmin = 400
res = None for p in pattern: cur = 0 for i in xrange(400):
if nstr[i] != p[0][i]: #比对每一个像素,如果不相同,则增加差异值 cur += 1 if cur < = minmin: #记录下差异值最小时所对应的字符
minmin = cur res = p[1] return res
最后测试一下:
for c in chars: print what(c),
结果:
可以看到18+18这些字符可以成功识别。那为什么=和?识别不了呢?因为我没有训练这两个字符,而=和?都和数字2的特征最接近 -_,-|
这个验证码还是挺好破解的,因为字符之间间距很大,而且没有旋转,没有扭曲,不需要多少变换就能得到可用的结果。像google的那种,就完全没法可想了。