41

写在前面

现在,很多网站采取各种各样的措施来反爬虫,其中之一就是使用验证码。当我们访问网页时,必须先通过验证码才能够访问页面。下面我们便来讲2种验证码的识别方式和一些思路。当然我们也可以直接使用付费的打码平台,那样可以增加识别的准确度,毕竟出了钱的嘛。哈哈!

PIL库

其实,验证码识别归根到底还是对各种各样图片的识别和操作,python中有很对图像处理的库,其中PIL就是其中之一。 所以在处理验证码识别之前,必须先了解PIL库和tesserocr。 下面附上其API源码地址,以及对应的学习博客。
源码地址:https://pillow-cn.readthedocs.io/zh_CN/latest/reference/index.html
参考博客:https://blog.csdn.net/louishao/article/details/69879981
下面我们就开始验证码识别之路了。

图形验证码

以中国知网为例:
图片描述

首先,我们先拿到上图中绿线标记的验证码,下载到本地项目文件中,
然后,编写如下代码:

import tesserocr
from PIL import Image

image = Image.open('image.png')
res = tesserocr.image_to_text(image)
print(res)  # F8BS

输出结果为:F8BS, 可是实际图片为F8B8,这是因为验证码内多余线条干扰了图片的识别,像这类情况,还需要做出额外的处理,比如转灰度,二值化等。当然,实际处理中并不是这样,一般我们会先对模糊图片进行灰度处理后,再设定二值化的阈值,实际处理如下:

import tesserocr
from PIL import Image

image = Image.open('code.jpg')  # 创建image对象

image = image.convert('L')
threshold = 150  # 指定二值化阈值
table = []
for i in range(256):
    if i < threshold:
        table.append(0)
    else:
        table.append(1)

image = image.point(table, '1')
image.show()
res = tesserocr.image_to_text(image)
print(res) 

输出结果:F8B8
进行识别时,先设定好二值化阈值threshold,进行适当调试,直到图片能正常识别为止。

滑动验证码

过程分析:

滑动验证码主要的验证方式是拖动滑块,拼合图像;如图象完全拼合,则验证成功,即表单提交成功,否则需要重新验证。
如图:
图片描述

下面,我们就以极验的验证码为例,来讲诉一下识别方法。
因为极验的验证码在拖动验证码后会生成一个加密的表单提交到后台,所有为了避免麻烦我们直接用selenium模拟浏览器行为来完成验证。
登陆网站:极验官网

目标站点:https://account.geetest.com/l...

图片描述

首先,我们发现登陆界面有个智能按钮,一般来说,在输入邮箱之后,点击按钮就会弹出滑动验证窗口,然后我们在拖动验证码完成图像拼接,完成验证。
图片描述

所以,滑块验证识别需要完成以下步骤:

  1. 模拟点击验证按钮
  2. 识别滑块的缺口位置
  3. 模拟拖动滑块

如何实现以上步骤呢?我们先需要将任务进行分解,看似只有三大步骤,其实里面坑还有很多的,稍后会做解释。

第一步,输入账号,获取智能按钮,使用selenium模拟点击,获取带有缺口的图片。

第二步,获取上面缺口图片中的完整图片。这里有个地方要注意,正常情况下我们在网页源代码里是找不到完整图的,因为它被隐藏了,必须执行javascript语句才能出现完整图。

图片描述

我们将display参数改为block,opacity参数改为1,然后进行截图,就可以拿到完整的验证码图片了。

第三步,对比两张图片的所有RGB像素点,得到缺口位置。

第四步,模拟人的拖动习惯,这里也有坑,极验的验证码增加了机器轨迹识别,匀速移动,或者随机速度移动滑块都不能通过验证,所以我们将需要拖动的总位移分成一段一段小的轨迹,先匀加速拖动后匀减速拖动。

第五步,按照规定轨迹进行拖动,完成验证。

第六步,完成账号登陆。

过程分析完了,下面我们就来写代码试一下:
首先,我们先将整个代码的一个逻辑思路做一个大致的概括吧。

def main():
    """主函数"""

    # 获取带缺口验证码图片image1, 传入的参数后缀为: .png
    image1 = get_unFull_captcha('unfull_captcha.png')
    # print(image1.load()[12,25])
    # 获取完整验证码图片image2
    image2 = get_full_captcha('full_captcha.png')
    # 对比上述图片像素点,获取缺口位置,得到偏移距离
    distance = get_quekou_distance(image1, image2)
    print('缺口偏移量:', distance)
    # 获取滑块的移动轨迹
    track = get_track(distance)
    # 模拟人的行为,拖动滑块,完成验证
    slider = get_slider()
    move(slider, track)
    success = wait.until(EC.text_to_be_present_in_element((By.CLASS_NAME, 'geetest_success_radar_tip_content'), '验证成功'))
    print(success)
    if success:
        login()
    else:
        main()

接下来,我们便来逐一完成main函数里要实现的功能了。

代码示例:
通过以上代码我们便拿到了完整的验证码和带有缺口的验证码。
缺口图片:

def get_unFull_captcha(name):
    """
    获取带缺口验证码图片
    :return: unfull captcha
    """
    top, bottom, left, right = get_captcha_position('geetest_canvas_slice')
    print('验证码1位置:', top, bottom, left, right)
    screenshot = get_screenshot()
    unfull_captcha = screenshot.crop((left, top, right, bottom)) # 按图片位置裁剪
    unfull_captcha.save(name)     # 这里传入的name要以xxx.png命名
    return unfull_captcha

完整图片:

def get_full_captcha(name):
    """
    获取完整验证码图片
    :return: full_captcha
    """
    # 这里要执行JavaScript脚本才能拿到完整图片的截图
    show_Full_img1= "document.getElementsByClassName('geetest_canvas_fullbg')[0].style.display='block'"
    browser.execute_script(show_Full_img1)
    show_Full_img2 = "document.getElementsByClassName('geetest_canvas_fullbg')[0].style.opacity=1"
    browser.execute_script(show_Full_img2)
    # 等待完整图片加载
    time.sleep(2)
    top, bottom, left, right = get_captcha_position('geetest_canvas_fullbg')
    print('验证码2位置:', top, bottom, left, right)
    screenshot = get_screenshot()
    full_captcha = screenshot.crop((left, top, right, bottom))  # 同上
    full_captcha.save(name)
    return full_captcha

这里我在调试的时候碰到一个坑,因为chrome中,location方法不滚动,直接返回相对整个html的坐标,我的电脑是15.6寸的,显示设置上布局的缩放大小被放大到1.25倍,导致location返回的坐标与验证码的坐标有误差。修改布局为100%后就解决了。 下面便是对比图片找出缺口位置。这里我们需要遍历图片的坐标点,获取像素点的RGB数据。

代码示例

def get_quekou_distance(image1, image2):
    """
    对比像素点,获取缺口位置
    :param image1: 缺口图片
    :param image2: 完整图片
    :return: 缺口的偏移距离
    """
    # 缺口在滑块右侧,设定遍历初始横坐标left为59
    left = 60
    # 像素对比阈值
    threshold = 60

    for i in range(left, image2.size[0]):
        for j in range(image2.size[1]):
            rgb1 = image1.load()[i, j]
            rgb2 = image2.load()[i, j]

            res1 = abs(rgb2[0] - rgb1[0])
            res2 = abs(rgb2[1] - rgb1[1])
            res3 = abs(rgb2[2] - rgb1[2])
            if not (res1 < threshold and res2 < threshold and res3 < threshold):
                return i-7 # 返回缺口偏移距离,这里需测试几次

接下来就是获取滑块的移动路径和模拟拖动行为了。

执行代码:

def get_track(distance):
    """
    获取移动路径
    :param distance: 偏移量
    :return: track:移动轨迹
    """
    # 存放移动轨迹
    track = []
    # 当前位置
    current = 0
    # 设定加速段和减速段临界点为路径的3/4处
    mid = distance*4/5
    # 时间间隔time, 取0.2~0.3之间随机数,避免被网站识别出来
    t = random.randint(2, 3)/10
    # 初速度
    v = 0

    while current < distance:
        if current < mid:
            # 匀加速移动,加速度a
            a = 2
        else:
            a = -3
        # 初速度
        v0 = v
        # 当前速度
        v = v0 + a*t
        # 移动距离
        s = v0*t + 1/2 * a * t*t
        # 当前位移
        current += s
        # 加入到移动轨迹
        track.append(round(s))
    return track

def move(slider, track):
    """
    模拟鼠标操作,点击,移动滑块按钮
    :param: 滑块
    :param: 轨迹
    :return:
    """
    ActionChains(browser).click_and_hold(slider).perform()
    # 操作鼠标按轨迹移动
    for x in track:
        ActionChains(browser).move_by_offset(xoffset=x, yoffset=0).perform()
    time.sleep(0.3)
    # 松开
    ActionChains(browser).release().perform()

最后终于成功了,踩了这么多坑,终于完成了滑块验证码的破解。。。现在已经实现功能,因为还可能出现其他情况,接下来我们还需要完善一下代码,其实也可封装成一个类,测试的时候我们会发现,图片会弹出小怪兽被吃了,那是因为系统识别我们是机器行为,所以不通过,这里我们需要修改加速度参数,再增加一个回调。

这样我们就成功破解验证码,并登陆到网页界面了。。。忙活了一上午,吃饭去了。
好像现在极验官网改了, 但是滑动验证码思路基本上就是这样的...

源码地址:https://github.com/appleguardu/spider_projects/tree/master/Captcha


a_dodo
2.4k 声望1k 粉丝

天下事有难易乎?