简介

爬虫在抓网站数据时,不可避免要和验证码做长久斗争。当然能绕过最好,但是总有绕不过的验证码,此时,对于简单的可以尝试破解,有难度的对接打码平台。现在验证码多种多样,点选,滑动,英文字母组合等,接下来简单的聊一聊英文字母组合中的这两种验证码的破解。

java 识别带干扰线的验证码 验证码干扰线去除算法_验证码

               

java 识别带干扰线的验证码 验证码干扰线去除算法_验证码_02

流程

识别英文字母组合验证码的一般步骤通常是:加载图片,灰度化,二值化,去除噪点(包括干扰线),字符分割,训练模型,识别。其中的难点一般在去燥和字符分割这两步,比如搜狗微信的验证码

java 识别带干扰线的验证码 验证码干扰线去除算法_Python_03

干扰线比字符还要粗,去燥不可避免会伤及字符,比如百度验证码

java 识别带干扰线的验证码 验证码干扰线去除算法_java 识别带干扰线的验证码_04

粘连,扭曲,变形。这种验证码不需要去除噪点,单恰恰是特别难的,分割特别麻烦,当然要是你能生成各种类型的验证码,那验证码的破解就非常easy了,只需要将带有标签的大批量验证码通过神经网络CNN训练出模型,可以高精度识别。

验证码处理一般需要使用PIL,opencv这两个模块。这里我们主要使用PIL库,验证码的处理主要是对图片的像素点进行操作,彩色图像中的每个像素的颜色有RGB三个分量决定,而灰度图像是RGB三个分量相同的一种特殊的彩色图像,将图片进行灰度化可以通过这两种方式实现。

像素点的RGB三色求平均

              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

这两步处理之后图片变为,

java 识别带干扰线的验证码 验证码干扰线去除算法_验证码_05

图片去除噪点有很多种方法,滤波法,邻域法,轮廓法等,这里通过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

去燥之后的图片已经基本不影响分割和识别了

java 识别带干扰线的验证码 验证码干扰线去除算法_Python_06

验证码分割一样有很多中针对特定情形的方法。图形基本不粘连的垂直阴影法,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

分割之后得到的单个字符,虽然祛噪点并不是特别完美,但是对于模型识别没有特别大的影响。

java 识别带干扰线的验证码 验证码干扰线去除算法_验证码_07

java 识别带干扰线的验证码 验证码干扰线去除算法_验证码_08

java 识别带干扰线的验证码 验证码干扰线去除算法_Python_09

java 识别带干扰线的验证码 验证码干扰线去除算法_java 识别带干扰线的验证码_10

java 识别带干扰线的验证码 验证码干扰线去除算法_Image_11

2》、第二种验证码和上面的去噪点方式基本相同,处理后的图片为

java 识别带干扰线的验证码 验证码干扰线去除算法_java 识别带干扰线的验证码_12

因为图片存在扭曲现象而又不相互粘连,才用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

对于有干扰,宽度较大的单个字符需要做二次切割处理,得到图片

java 识别带干扰线的验证码 验证码干扰线去除算法_Python_13

java 识别带干扰线的验证码 验证码干扰线去除算法_java 识别带干扰线的验证码_14

java 识别带干扰线的验证码 验证码干扰线去除算法_像素点_15

java 识别带干扰线的验证码 验证码干扰线去除算法_Image_16

java 识别带干扰线的验证码 验证码干扰线去除算法_Image_17

此时,若要增加识别准确度,可能需要使用旋转卡壳算法,原理很简单,但是不太好实现,参考一些大牛的代码结合自己的实际。具体代码如下,如果有更好的方式请告知博主,很愿意学习,如有需要请自行研究。

# -*- 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')

最终得到的字符如下

java 识别带干扰线的验证码 验证码干扰线去除算法_Image_18

java 识别带干扰线的验证码 验证码干扰线去除算法_Image_19

 

java 识别带干扰线的验证码 验证码干扰线去除算法_Image_20

 

java 识别带干扰线的验证码 验证码干扰线去除算法_Image_21

java 识别带干扰线的验证码 验证码干扰线去除算法_像素点_22