python爬虫验证码_(Python)爬虫解析特殊验证码?

(python3.4)

本回答针对该验证码的识别,主要分以下五部分:验证码分析

下载验证码样本

生成初始字符图片样本

识别验证码

测试与总结

涉及的脚本及文件(脚本按照出现的先后顺序排列)

.../captcha_char

.../captcha_example

.../download_captcha_0.py

.../download_captcha_1.py

.../pretreat_image.py

.../cut_all_char.py

.../new_char_example.py

.../load_char_example.py

.../distinguish_captcha.py

验证码分析

下载验证码观察:

# download_captcha_0.py

# python3.4

from urllib import request

host_url = 'http://kmustjwcxk3.kmust.edu.cn/jwweb/'

captcha_url = host_url+'sys/ValidateCode.aspx'

for k in range(10):

request.urlretrieve(captcha_url,'%d.jpg'%k)

0.jpg

1.jpg

2.jpg

观察结论:字符为数字+大写英文字母.后面发现没有数字0,1,字母I,L,O.应该是考虑到人也不易区分这几个字符.字符有两种字体,且都是斜体字.

有五条随机的直线,上方有噪点,有一外框.干扰很小.

一般字体的变形有两类:线性的,非线性的.线性的又可分为:平移,倾斜,旋转,伸缩.很少有对称的.这里只有垂直方向的平移,较易处理.

总之,这是一个很弱的验证码.

(以下涉及两个文件夹captcha_example,captcha_char.放置在python脚本所在路径下.)

下载验证码样本

# download_captcha_1.py

# 手动建立文件夹:captcha_example

from urllib import request

# 该站点速度更快

host_url = 'http://jwmis.hhtc.edu.cn/'

captcha_url = host_url+'sys/ValidateCode.aspx'

for k in range(300):

request.urlretrieve(captcha_url,'captcha_example/%d.jpg'%k)

生成初始字符图片样本

分3步进行:验证码预处理,主要是降噪

分割图片,获取单个字符的图片

生成初始字符图片样本

验证码预处理,主要是降噪

预处理前(原始验证码图片,jpg格式):

预处理后(红色边框内部的是结果,png格式):

原图片的格式为JFIF(jpg).为了不失真,处理后的图片一律保存为png格式

# pretreat_image.py

# 注意:二值图像0/1统一转化为0/255

from PIL import Image,ImageDraw,ImageChops

import os

# 验证码预处理,主要是降噪

# 预处理结束后返回0/255二值图像

# 降噪,参考 http://blog.csdn.net/xinghun_4/article/details/47864949

def pretreat_image(image):

# 将图片转换成灰度图片

image = image.convert("L")

# 二值化,得到0/255二值图片

# 阀值threshold = 180

image = iamge2imbw(image,180)

# 对二值图片进行降噪

# N = 4

clear_noise(image,4)

# 去除外边框

# 原图大小:122*54

# 左上右下,左 <= x < 右

box = ( 8, 10, 118, 50 )

image = image.crop(box)

return image

# 灰度图像二值化,返回0/255二值图像

def iamge2imbw(image,threshold):

# 设置二值化阀值

table = []

for i in range(256):

if i < threshold:

table.append(0)

else:

table.append(1)

# 像素值变为0,1

image = image.point(table,'1')

# 像素值变为0,255

image = image.convert('L')

return image

# 根据一个点A的灰度值(0/255值),与周围的8个点的值比较

# 降噪率N: N=1,2,3,4,5,6,7

# 当A的值与周围8个点的相等数小于N时,此点为噪点

# 如果确认是噪声,用该点的上面一个点的值进行替换

def get_near_pixel(image,x,y,N):

pix = image.getpixel((x,y))

near_dots = 0

if pix == image.getpixel((x - 1,y - 1)):

near_dots += 1

if pix == image.getpixel((x - 1,y)):

near_dots += 1

if pix == image.getpixel((x - 1,y + 1)):

near_dots += 1

if pix == image.getpixel((x,y - 1)):

near_dots += 1

if pix == image.getpixel((x,y + 1)):

near_dots += 1

if pix == image.getpixel((x + 1,y - 1)):

near_dots += 1

if pix == image.getpixel((x + 1,y)):

near_dots += 1

if pix == image.getpixel((x + 1,y + 1)):

near_dots += 1

if near_dots < N:

# 确定是噪声,用上面一个点的值代替

return image.getpixel((x,y-1))

else:

return None

# 降噪处理

def clear_noise(image,N):

draw = ImageDraw.Draw(image)

# 外面一圈变白色

Width,Height=image.size

for x in range(Width):

draw.point((x,0),255)

draw.point((x,Height-1),255)

for y in range(Height):

draw.point((0,y),255)

draw.point((Width-1,y),255)

# 内部降噪

for x in range(1,Width - 1):

for y in range(1,Height - 1):

color = get_near_pixel(image,x,y,N)

if color != None:

draw.point((x,y),color)

if __name__ == '__main__':

image = Image.open('captcha_example/0.jpg')

image = pretreat_image(image)

image.show()

分割图片,获取单个字符的图片

预处理前:

预处理后(红色边框内部的是结果):

分割后(红色边框内部的是结果):

# cut_all_char.py

# 注意:二值图像0/1统一转化为0/255

from PIL import Image,ImageDraw,ImageChops

import os

from pretreat_image import *

# 分割图片,获取单个字符的图片

# 二值图片的分割

def cut_one_char(image):

# 再次降噪

# N = 4

clear_noise(image,4)

CharWidth=25

CharHeight=26

Width,Height=image.size

# 找出image上出现黑点的第一列

x = find_first_column(image)

# 第一行

# 左上右下,左 <= x < 右

box = (x,0,x+CharWidth,Height)

image2 = crop_white(image,box)

y = find_first_row(image2)

# 切割出一个字符

box = (x,y,x+CharWidth,y+CharHeight)

image_char = crop_white(image,box)

# 剩下的图片

if x+CharWidth > Width:

image_residue = None

else:

box = (x+CharWidth,0,Width,Height)

image_residue = crop_white(image,box)

return [image_char,image_residue]

# 没有字符W的情况下,切割的都比较好.

# 出现W的概率为1-(1-1/36)^4≈10.66%

# 这样一来准确率无法超过90%

# 这里处理4个字符的情况

def cut_all_char(image):

image_char1,image = cut_one_char(image)

image_char2,image = cut_one_char(image)

image_char3,image = cut_one_char(image)

image_char4,image = cut_one_char(image)

return [image_char1,image_char2,image_char3,image_char4]

# 如果box超出原图范围,默认会以黑色填充

# 因此为了让图片超出部分以白色填充,进行反色处理,最后再反色回来

def crop_white(image,box):

# 255 - old

image = ImageChops.invert(image)

image = image.crop(box)

return ImageChops.invert(image)

# 找出image上出现黑点的第一列

def find_first_column(image):

Width,Height=image.size

for x in range(Width):

for y in range(Height):

if image.getpixel( (x,y) ) == 0:

return x

# 如果没有黑点,返回第一列

return 0

# 找出image上出现黑点的第一行

def find_first_row(image):

Width,Height=image.size

for y in range(Height):

for x in range(Width):

if image.getpixel( (x,y) ) == 0:

return y

# 如果没有黑点,返回第一行

return 0

if __name__ == '__main__':

image = Image.open('captcha_example/0.jpg')

image = pretreat_image(image)

image_char_list = cut_all_char(image)

image_char_list[0].show()

生成初始字符图片样本

过程示意图1:

在此发现字符0,1,I,L,O并不出现.

过程示意图2:

# new_char_example.py

from PIL import Image,ImageDraw,ImageChops

import os

from pretreat_image import *

from cut_all_char import *

# 生成存放样本的文件夹

def new_char_folder():

# 0-9

for k in range(48,58):

try:

os.mkdir( 'captcha_char/%c' % k )

except:

pass

# A-Z

for k in range(65,91):

try:

os.mkdir( 'captcha_char/%c' % k )

except:

pass

# 生成样本字符,然后手动将对应字符移动到上面生成的文件夹中

# 每个文件夹手动移动10个以上

def new_char_example():

new_char_folder()

for s in range(300):

image = pretreat_image( 'captcha_example/%d.jpg' % s )

image.save( 'captcha_char/%d.png' % s )

image_char_list = cut_all_char(image)

for k in range(4):

image_char_list[k].save( 'captcha_char/%d_%d.png' % (s,k) )

if __name__ == '__main__':

pass

识别验证码

分4步进行(其中前两步与前面生成初始字符图片样本的步骤一样):验证码预处理,主要是降噪

分割图片,获取单个字符的图片

加载字符图片样本

分割的字符图片与字符图片样本进行比对

加载字符图片样本

# load_char_example.py

# 注意:二值图像0/1统一转化为0/255

from PIL import Image,ImageDraw,ImageChops

import os

# 遍历指定目录,显示目录下的所有文件名

def eachfile(filepath):

dir_list = os.listdir(filepath)

all_dir = []

for dir in dir_list:

child = '%s%s' % (filepath, dir)

all_dir.append(child)

return all_dir

# 共31个字符

char_set = [

'2','3','4','5','6','7','8','9',

'A','B','C','D','E','F','G',

'H', 'J','K', 'M','N',

'P','Q','R','S','T',

'U','V','W','X','Y','Z'

]

# 加载字符图片样本

# 将对应字符的样本按照上述 char_set 的顺序加载

def load_char_example():

global char_set

char_example = []

for char in char_set:

char_example.append([])

folder_path = 'captcha_char/%c/'%char

image_name_list = eachfile(folder_path)

for image_name in image_name_list:

image = Image.open(image_name)

# 注意这里读取的数据为灰度图像,像素值为0,255

# 为此将其他地方出现的0/1二值图像统一处理成0/255二值图像

char_example[-1].append(image)

return char_example

if __name__ == '__main__':

load_char_example()

分割的字符图片与字符图片样本进行比对

# distinguish_captcha.py

# 注意:二值图像0/1统一转化为0/255

# 涉及文件夹:captcha_example,captcha_char

from PIL import Image,ImageDraw,ImageChops

import os

from pretreat_image import *

from cut_all_char import *

from load_char_example import *

# 比较两个0/255二值图像

# 计算相似度,公式:相似度 = 相等的像素点数 / 总像素点数

def compare2imbw(imbw1,imbw2):

# out = abs(img1, img2),相同的点变为0,不同的变为255

image = ImageChops.difference(imbw1,imbw2)

# 统计相同的点的个数

# 直方图统计,返回长度为256的list

a = image.histogram()

same_pixel = a[0]

Width,Height=imbw1.size

all_pixel = Width*Height

return same_pixel/all_pixel

# 与样本比较,设置一个评分体系

# 分别取最大的五个相似度相加,再取最大的对应的字符

def distinguish_one_char(char_example,image_char):

global char_set

score_set=[]

for image_list in char_example:

score_set.append([])

for image in image_list:

score_set[-1].append( compare2imbw(image,image_char) )

# 对于score_set[k],取分值最高的5个相加

char_num=len(char_set)

for k in range(char_num):

# 从小到大排序

score_set[k].sort()

# 逆序

score_set[k].reverse()

# 调试打印节点1

# print(char_set[k],score_set[k][0:1])

# 取前5

score_set[k] = score_set[k][0:5]

# 前5相加,保存在score_set[k]中

score_set[k] = sum(score_set[k])

# 获得相似度最大的字符,并返回

# index 返回找到的第一个值的位置

a = score_set.index( max(score_set) )

# 调试打印节点2

# print(char_set[a])

return char_set[a]

def distinguish_all_char(char_example,image_char_list):

s = ""

for image_char in image_char_list:

s += distinguish_one_char(char_example,image_char)

return s

def distinguish_captcha(image):

# 预处理图片,返回0/255二值图像

image = pretreat_image(image)

# 切割二值图像

image_char_list = cut_all_char(image)

# 加载样本数据

char_example = load_char_example()

# 比对识别各个字符

result = distinguish_all_char(char_example,image_char_list)

return result

if __name__ == '__main__':

image = Image.open('captcha_example/0.jpg')

s = distinguish_captcha(image)

print(s)

测试与总结凡是 W 后面一个字符的识别基本上出错,这是由于 W 较宽,导致 W 与其右边一个字符出现粘合,从而切割时出错.如果 W 只出现在最后一个位置,错误率不高.因为切割从左向右进行, W 在最右边时,不会影响到其他字符.

计算理想的准确率,即 W 不出现在前面三个位置的概率.没有 W 的概率

equation?tex=%28%5Cfrac%7B30%7D%7B31%7D%29%5E4 , W 只在最后一个位置出现的概率

equation?tex=%28%5Cfrac%7B30%7D%7B31%7D%29%5E3%5Ctimes%28%5Cfrac%7B1%7D%7B31%7D%29 .理想的准确率为上述两者相加:

equation?tex=%28%5Cfrac%7B30%7D%7B31%7D%29%5E3%5Capprox90.63%5C%25 .

如有必要,需改进分割字符.

进行3次网络测试,每次为100个验证码.正确率为:87%,87%,88%.由此,正确率稳定在87%左右,与90.63%相差的部分应该是由其他误差导致的.

第一次在知乎长文回答,发现代码高亮和 tex 都挺好用的.

20170211

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值