简介
爬虫在抓网站数据时,不可避免要和验证码做长久斗争。当然能绕过最好,但是总有绕不过的验证码,此时,对于简单的可以尝试破解,有难度的对接打码平台。现在验证码多种多样,点选,滑动,英文字母组合等,接下来简单的聊一聊英文字母组合中的这两种验证码的破解。
流程
识别英文字母组合验证码的一般步骤通常是:加载图片,灰度化,二值化,去除噪点(包括干扰线),字符分割,训练模型,识别。其中的难点一般在去燥和字符分割这两步,比如搜狗微信的验证码
干扰线比字符还要粗,去燥不可避免会伤及字符,比如百度验证码
粘连,扭曲,变形。这种验证码不需要去除噪点,单恰恰是特别难的,分割特别麻烦,当然要是你能生成各种类型的验证码,那验证码的破解就非常easy了,只需要将带有标签的大批量验证码通过神经网络CNN训练出模型,可以高精度识别。
验证码处理一般需要使用PIL,opencv这两个模块。这里我们主要使用PIL库,验证码的处理主要是对图片的像素点进行操作,彩色图像中的每个像素的颜色有R、G、B三个分量决定,而灰度图像是R、G、B三个分量相同的一种特殊的彩色图像,将图片进行灰度化可以通过这两种方式实现。
像素点的R、G、B三色求平均值
2:R * 0.3+ G * 0.59 +B * 0.11
二值化是通过阀值将像素点转化成非白(255)即黑(0)的值
1》、具体代码实现:
# 加载图片
img = Image.open('0.jpg')
# 图片转化为灰色图片
img = img.convert("L")
# 图片灰度化
pixdata = img.load()
for y in range(img.size[1]):
for x in range(img.size[0]):
if x <= 5 or x >= 195 or y <= 5 or y >= 47:
pixdata[x, y] = 225
if pixdata[x, y] < 100:
pixdata[x, y] = 0
else:
pixdata[x, y] = 255
这两步处理之后图片变为,
图片去除噪点有很多种方法,滤波法,邻域法,轮廓法等,这里通过8邻域法实现,主要是控制好阀值避免误伤,当一次效果理想时,可以适当增加降噪次数。
def denoising(im):
"""图片去除噪点"""
pixdata = im.load()
w, h = im.size
for j in range(1, h - 1):
for i in range(1, w - 1):
count = 0
l = pixdata[i, j]
if l == pixdata[i, j - 1]:
count = count + 1
if l == pixdata[i, j + 1]:
count = count + 1
if l == pixdata[i + 1, j - 1]:
count = count + 1
if l == pixdata[i + 1, j + 1]:
count = count + 1
if l == pixdata[i + 1, j]:
count = count + 1
if l == pixdata[i - 1, j + 1]:
count = count + 1
if l == pixdata[i - 1, j - 1]:
count = count + 1
if l == pixdata[i - 1, j]:
count = count + 1
if count < 4:
pixdata[i, j] = 255
return im
去燥之后的图片已经基本不影响分割和识别了
验证码分割一样有很多中针对特定情形的方法。图形基本不粘连的垂直阴影法,cfs通道法,以及稍稍粘连使用滴水算法切割等
效果都特别好。博主对这种明显不粘连的验证码才用的是垂直阴影法切割
# 垂直阴影法分割
def get_projection_x(image, invert=False):
p_x = [0 for x in range(image.size[0])]
for w in range(image.size[1]):
for h in range(image.size[0]):
if invert:
if image.getpixel((h, w)) >= 200:
p_x[h] += 1
continue
else:
if image.getpixel((h, w)) <= 5:
p_x[h] += 1
continue
# 判断边界
l, r = 0, 0
flag = False
cuts = []
for i, int in enumerate(p_x):
# 阈值这里为2
if flag is False and int > 3:
l = i
flag = True
if flag and int <= 3:
r = i - 1
flag = False
cuts.append((l, r))
return cuts
分割之后得到的单个字符,虽然祛噪点并不是特别完美,但是对于模型识别没有特别大的影响。
2》、第二种验证码和上面的去噪点方式基本相同,处理后的图片为
因为图片存在扭曲现象而又不相互粘连,才用cfs通道法进行的分割,网上有很多现成代码,结合业务使用即可。
def cfs(img):
"""传入二值化后的图片进行连通域分割"""
pixdata = img.load()
w, h = img.size
visited = set()
q = queue.Queue()
offset = [(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1)]
cuts = []
for x in range(w):
for y in range(h):
x_axis = []
# y_axis = []
if pixdata[x, y] == 0 and (x, y) not in visited:
q.put((x, y))
visited.add((x, y))
while not q.empty():
x_p, y_p = q.get()
for x_offset, y_offset in offset:
x_c, y_c = x_p + x_offset, y_p + y_offset
if (x_c, y_c) in visited:
continue
visited.add((x_c, y_c))
try:
if pixdata[x_c, y_c] == 0:
q.put((x_c, y_c))
x_axis.append(x_c)
# y_axis.append(y_c)
except:
pass
if x_axis:
min_x, max_x = min(x_axis), max(x_axis)
if max_x - min_x > 4:
# 宽度小于3的认为是噪点,根据需要修改
cuts.append((min_x, max_x))
return cuts
对于有干扰,宽度较大的单个字符需要做二次切割处理,得到图片
此时,若要增加识别准确度,可能需要使用旋转卡壳算法,原理很简单,但是不太好实现,参考一些大牛的代码结合自己的实际。具体代码如下,如果有更好的方式请告知博主,很愿意学习,如有需要请自行研究。
# -*- coding: utf-8 -*-
from PIL import Image
import cv2
import numpy as np
def rotate_bound_white_bg(image, angle):
(h, w) = image.shape[:2]
print(image.shape)
(cX, cY) = (w // 2, h // 2)
M = cv2.getRotationMatrix2D((cX, cY), angle, 1)
cos = np.abs(M[0, 0])
sin = np.abs(M[0, 1])
nW = int((h * sin) + (w * cos)) - w
nH = int((h * cos) + (w * sin))
M[0, 2] += (nW / 2) - cX
M[1, 2] += (nH / 2) - cY
return cv2.warpAffine(image, M, (nW, nH), borderValue=(255, 255, 255))
def getcrop(region):
frame = region.getdata() # 返回图像内容的像素值序列,不过,这个返回值是 PIL 内部的数据类型,只支持确切的序列操作符,包括迭代器和基本序列方法
# print(list(frame))
(w, h) = region.size
pts = []
ptsi = []
for i in range(h):
for j in range(w):
if frame[i * w + j] != 255: # 黑色像素点所在的位置
pts.append((i, j))
ptsi.append((j, i))
if pts == []:
return [0, 0, 1, 1]
pp1 = min(pts)
pp2 = max(pts)
pp3 = min(ptsi)
pp4 = max(ptsi)
return [pp3[0], pp1[0], pp4[0] + 1, pp2[0] + 1] # 图像内容所在的最大坐标点位置
def docrop(region):
croppos = getcrop(region)
newregion = region.crop(croppos) # 切割内容点
return newregion
def density(region):
frame = region.getdata()
(w, h) = region.size
area_all = w * h
area = 0
for i in range(h):
for j in range(w):
if frame[i * w + j] != 255:
area += 1
return 1.0 * area / area_all
def dorotate(region):
deg = 0
maxdens = 0
for i in range(-30, 30):
area = docrop(region.rotate(i))
dens = density(area)
if dens > maxdens:
deg = i
maxdens = dens
return deg
def imdiv(im):
'''div and return pieces of pics'''
frame = im.load()
(w, h) = im.size
horis = []
for i in range(w):
for j in range(h):
if frame[i, j] != 255:
horis.append(i)
break
horis2 = [max(horis[0] - 2, 0)]
for i in range(1, len(horis) - 1):
if horis[i] != horis[i + 1] - 1:
horis2.append((horis[i] + horis[i + 1]) / 2)
horis2.append(min(horis[-1] + 3, w))
boxes = []
for i in range(len(horis2) - 1):
boxes.append([horis2[i], 0, horis2[i + 1], h])
for k in range(len(boxes)):
verts = []
for j in range(h):
for i in range(boxes[k][0], boxes[k][2]):
if frame[i, j] != 255:
verts.append(j)
boxes[k][1] = max(verts[0] - 2, 0)
boxes[k][3] = min(verts[-1] + 3, h)
regions = []
for box in boxes:
regions.append(im.crop(box))
return regions
def normalize(im):
regions = imdiv(im)
angle = dorotate(regions[0])
return angle
image = Image.open('cut3.jpg')
angle = normalize(image)
image = np.array(image)
image = rotate_bound_white_bg(image, angle)
image = Image.fromarray(image)
image = image.resize((29, 51))
pixdata = image.load()
for y in range(image.size[1]):
for x in range(image.size[0]):
if pixdata[x, y] <= 80:
pixdata[x, y] = 0
else:
pixdata[x, y] = 255
image.save('0cut2.jpg', 'JPEG')
最终得到的字符如下