Python爬虫 | 滑动验证码破解

极验验证码:需要手动拼合滑块来完成的验证,相对图形验证码识别难度上升了几个等级。下面用程序识别并通过极验验证码的验证,其中有分析识别思路、识别缺口位置、生成滑块拖动、模拟实现滑块拼合通过验证等步骤。需要用到Chrome 浏览器,并配置 ChromeDriver ,要用到的 Python 库是 Selenium。

1、 对极验验证码了解
  极验验证码官网:http://www.geetest.com/。一个专注提供验证安全的系统,主要验证方式是拖动滑块拼命图像。若图像完全拼合,则验证成功,即表单提交,否则需要重新验证。

现在极验验证码使用的企业很多,每天有超过几亿次的响应。极验验证码广泛用于直播视频、金融服务、电子商务、游戏娱乐、政府企业等各大类网站。

2、 极验验证码特点
  识别难度大。首先要点击按钮进行智能验证,如果验证不通过,则会弹出滑动窗口,拖动滑块拼合图像进行验证。之后三个加密参数会生成,通过表单提交到后台,后台还会进行一次验证。

极验验证码增加了机器学习的方法来识别手动轨迹。其官方网站的安全防护有下面几点说明:

  • 三角防护之防模拟:恶意程序模仿人类行为轨迹对验证码进行识别。利用机器学习和神经网络,构建线上线下的多重静态、动态防御模型,识别模拟轨迹,界定人机边界。

  • 三角防护之防伪造:恶意程序通过伪造设备浏览器环境对验证码进行识别。利用设备基因技术,深度分析浏览器的实际性能来辨识伪造信息,同时根据依靠事件不断更新黑名单来提高防依靠能力。

  • 三角防护之防暴力:恶意程序在短时间内进行密集的攻击,对验证码进行暴力识别。为了防暴力,极验验证码有多种形态,每种形态都用神经网络生成海量图库储备,每一张图片都是独一无二的,且图库不断更新,极大程度提高防暴力识别成本。

3、 识别思路
  对于极验验证码,直接模拟表单提交,加密参数构造较困难,需要分析其加密和校验逻辑,相对烦琐。所以直接采用模拟浏览器动作的方式来完成验证。使用 Python 的 Selenium 库模拟人的行为方式来完成验证,成本要相对去识别加密算法少很多。如图所示。

!极验验证码实例](https://img2018.cnblogs.com/blog/1501522/201905/1501522-20190520165616663-50859391.png)

先找一个带有极验证的网站,这里以博客园的登录为例进行验证。极验的官方登录网站是 https://account.geetest.com/login,也可以在其官网上做测试。在博客园的登录页面点击登录后会出现一个极验验证按钮。如图2-2所示。

Python爬虫 | 滑动验证码破解_第1张图片
极验验证按钮

该按钮是智能验证按钮。一般来说,如果是同一个会话,一段时间内第二次点击会直接通过验证。如果智能识别不通过,则会弹出滑动验证窗口,需要拖动滑块图像完成二步验证。验证成功后,验证按钮会提示验证成功。接下来就是提交表单。

经过上述分析,极验验证需要完成下面三步:
(1)模拟点击验证按钮。
(2)识别滑动缺口的位置。
(3)模拟手动滑块。

第(1)步操作相对简单,可使用 Selenium 模拟点击按钮。
第(2)步操作识别缺口的位置很关键,需要用到图像的相关处理方法。首先观察缺口的样子,如图所示。

Python爬虫 | 滑动验证码破解_第2张图片
图2-3 缺口示例

缺口的四周边缘有明显的断裂边缘,边缘与边缘周围有明显的区别。可用边缘检测算法来找出缺口的位置。对于极验验证码,可以利用和原图对比检测方式识别缺口的位置,通常在没有拖动滑块前,缺口没有呈现。

可以同时获取两张图片,设定一个对比阈值,然后遍历两张图片,找出相同位置像素 RGB 差距超过此阈值的像素点,此像素点的位置就是缺口的位置。

第(3)步中要注意的是,极验验证码增加了机器轨迹识别,匀速移动、随机速度移动等方法都不能通过验证,只有完全模拟人的移动轨迹才可以通过验证。人的移动轨迹一般是先加速后减速,要对这个过程模拟才能成功。

要包括这几个步骤。

第一步,初始化,在这里我们先初始化 一些selenium的 配置及一些参数的配置。

第二步,就是模拟点击了,这里主要是利用selenium模块模拟浏览器对网页进行操作。

第三步,就该识别缺口的位置了。首先获取前后两张图片,得到其所在位置和宽高,然后获取整个网页的截图,图片裁切下来即可。

最后一步,模拟拖动,经过多次试验,得出一个结论,那就是完全模拟加速减速的过程通过了验证。前段作匀加速,后段作匀减速运动,利用物理学的加速度公式即可完成验证。

位移移动

需要的基础知识

位移移动相当于匀变速直线运动,类似于小汽车从起点开始运行到终点的过程(首先为匀加速,然后再匀减速)。

Python爬虫 | 滑动验证码破解_第3张图片

其中a为加速度,且为恒量(即单位时间内的加速度是不变的),t为时间

Python爬虫 | 滑动验证码破解_第4张图片
Python爬虫 | 滑动验证码破解_第5张图片

教程一:博客园

误区一:

下面整个button区域对应着整个登录框,可以直接使用id里的标签进行定位,而不是第1个span标签

button = self.wait.until(EC.presence_of_element_located((By.ID,"submitBtn")))

误区二:注意关于图片的一些知识点

img1 = Image.open("E:/python27/pic/1.jpg")
img1.size
r, g, b = img1.split()

打开图片:Image.open(fp, mode='r')
保存图片:Image.save(fp, format=None, **params):
展示图片:Image.show(title=None, command=None):
location = img.location
# location属性可以返回该图片对象(既这张图片)在浏览器中的位置,以字典的形式返回,{‘x’:30,‘y’:30} 这里我们图片的位置是(30,30)。坐标轴是以屏幕左上角为原点,x轴向右递增,y轴像下递增。(相对整个html的坐标)



size = img.size  
# 通过size获取属性大小,size属性同样返回一个字典,{‘height’:30,‘width’:30 } 即图片对象的高度,宽度。



four_corner =(location['x'],
        location['y'],
        location['x']+size['width'],
        location['y']+size['height'])        
# 通过location和size获取上下左右,注意是小写的x和y



captcha = screenshot.crop((top, bottom, left, right))  
# Image.crop() 从图像中提取出某个矩形大小的图像。它接收一个四元素的元组作为参数,各元素为(left, upper, right, lower),坐标系统的原点(0, 0)是左上角。# 因为上面截图,截取到的是整个页面,现在只把验证码图片取到


image.size属性是一个列表,下标0是横坐标(宽,图片的最右边),下标1是纵坐标
image.size[0]
image.size[1]

完整代码

from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver import ActionChains
from selenium import webdriver
# import PIL.Image as image
from PIL import Image
from io import BytesIO
import time

EMAIL =
PASSWORD =
BODER = 6
INIT_LEFT = 60

class CrackGeetest():

    def __init__(self):
        self.url = 'http://www.sf-express.com/cn/sc/dynamic_function/waybill'
        self.browser = webdriver.Chrome('D:\\chromedriver.exe')
        self.wait = WebDriverWait(self.browser, 100)
        self.email = EMAIL
        self.keyword = PASSWORD
        self.BORDER = 6


    def open(self):
        """
        打开浏览器,并输入用户名和密码
        :return:None
        """
        self.browser.get(self.url)
        email = self.wait.until(EC.presence_of_element_located((By.ID, 'email')))
        password = self.wait.until(EC.presence_of_element_located((By.ID, 'password')))
        email.send_keys(self.email)
        password.send_keys(self.password)

    def get_geetest_button(self):
        """
        点击按钮,弹出没有缺口的图片
        :return: 返回按钮对象
        """
        button = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'geetest_radar_tip')))
        return button

    def __del__(self):
        self.browser.close()


    def get_position(self):
        """
        获取验证码位置
        :return:验证码位置元组
        """
        img = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'geetest_canvas_img')))
        time.sleep(2)

        # location属性可以返回该图片对象(既这张图片)在浏览器中的位置,以字典的形式返回,{‘x’:30,‘y’:30} 这里我们图片的位置是(30,30)。坐标轴是以屏幕左上角为原点,x轴向右递增,y轴像下递增。(相对整个html的坐标)
        location = img.location

        # size属性同样返回一个字典,{‘height’:30,‘width’:30 } 即图片对象的高度,宽度。
        size = img.size                 # 通过sizes属性获取大小
        top,bottom,left,right = location['y'],location['y']+size['height'],location['x'],location['x']+size['width']    # 通过location和size获取上下左右
        return (top,bottom,left,right)


    def get_screenshot(self):
        """
        获取网页截图
        :return: 截图对象
        """
        pic.png = self.browser.get_screenshot_as_png()
        screenshot = Image.open(BytesIO(pic.png))            # 转换为字节对象

            '''
            img1 = Image.open("E:/python27/pic/1.jpg")
            img1.size
            r, g, b = img1.split()
            
            打开图片:Image.open(fp, mode='r')
            保存图片:Image.save(fp, format=None, **params):
            展示图片:Image.show(title=None, command=None):
            '''

    def get_geetest_image(self, name='captcha.jpg'):
        """
        获取验证码图片
        :return: 图片对象
        """
        top, bottom, left, right = self.get_position()                  # 定位到这个位置之后,再截图
        print('验证码位置',top, bottom, left, right)
        screenshot = self.get_screenshot()                              # 获取到截图

        # Image.crop() 从图像中提取出某个矩形大小的图像。它接收一个四元素的元组作为参数,各元素为(left, upper, right, lower),坐标系统的原点(0, 0)是左上角。
        captcha = screenshot.crop((top, bottom, left, right))           # 因为上面截图,截取到的是整个页面,现在只把验证码图片取到
        captcha.save(name)                                              # 把图片存储起来,name是传进来的变量
        return captcha


    def get_slider(self):
        """
        获取滑块
        :return: 滑块对象
        """
        slider = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'geetest_slider_button')))
        return slider


    def is_pixel_equal(self, img1, img2, x, y):
        """
        判断两个像素是否相同
        :param image1: 图片1
        :param image2: 图片2
        :param x: 位置x
        :param y: 位置y
        :return: 像素是否相同
        """
        # 取两个图片的像素点 (img.load()[x, y]获取某一点的像素值)
        pixel1 = img1.load()[x, y]      # 上面的 i 和 j
        pixel2 = img2.load()[x, y]
        threshold = 60                  # 控制误差
        if (abs(pixel1[0] - pixel2[0] < threshold) and abs(pixel1[1] - pixel2[1] < threshold)           # 0 1 2 是RGB的三种颜色
        and abs(pixel1[2] - pixel2[2] < threshold)):
            return True
        else:                           # 有一个超过阈值,则认为2个像素点不一致
            return False


    def get_gap(self, img1, img2):
        """
        获取缺口偏移量
        :param img1: 不带缺口图片
        :param img2: 带缺口图片
        :return:
        """
        left = 60                                   # 从图片的60处开始往右,这样就可以把被拖动滑块跳过去,因为被拖动滑块处像素也不一样

        # 相当于从60开始,一列列做对比
        for i in range(left, img1.size[0]):         # i 相当于横坐标      size属性是一个列表,下标0是横坐标(宽,图片的最右边),下标1是纵坐标
            for j in range(img1.size[1]):           # j 相当于纵坐标      从0开始
                if not self.is_pixel_equal(img1, img2, i, j):
                    left = i                                            # 如果相同位置的像素不一致,则替换为i
                    return left
        return left


    def get_track(self, distance):
        """
        根据偏移量获取移动轨迹
        :param distance: 偏移量
        :return: 移动轨迹
        """
        # 移动轨迹
        track = []
        # 当前位移
        current = 0
        # 减速阈值
        mid = distance * 4 / 5
        # 计算间隔
        t = 0.2
        # 初速度
        v = 0

        while current < distance:                   # 所以 track是不会大于总长度的
            if current < mid:
                # 加速度为正2
                a = 2
            else:
                # 加速度为负3
                a = -3
            # 初速度v0
            v0 = v
            # 移动距离x = v0t + 1/2 * a * t^2,现做了加速运动
            move = v0 * t + 1 / 2 * a * t * t
            # 当前速度v = v0 + at  速度已经达到v,该速度作为下次的初速度
            v = v0 + a * t
            # 当前位移
            current += move
            # 加入轨迹
            track.append(round(move))                   # track 就是最终鼠标在 X 轴移动的轨迹
        return track


    def move_to_gap(self, slider, track):
        """
        拖动滑块到缺口处
        :param slider: 滑块
        :param track: 轨迹
        :return:
        """
        ActionChains(self.browser).click_and_hold(slider).perform()         # 利用动作链,获取slider,perform是

        for x in track:
            ActionChains(self.browser).move_by_offset(xoffset=x, yoffset=0).perform()       # xoffset横坐标,yoffset纵坐标。使得鼠标向前推进
        time.sleep(0.5)                                     # 推动到合适位置之后,暂停一会
        ActionChains(self.browser).release().perform()      # 抬起鼠标左键


    def login(self):
        """
        登陆
        :return: None
        """
        submit = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME,'login-btn')))
        submit.click()                      # 登陆按钮
        time.sleep(10)
        print('登陆成功')


    def crack(self):
        # 打开浏览器,输入用户名和密码
        self.open()

        # 点击按钮,弹出没有缺口的图片
        button = self.get_geetest_button()
        button.click()
        time.sleep(0.5)

        # 获取验证码图片
        image1 = self.get_geetest_image('captcha1.png')

        #点按呼出滑块
        slider = self.get_slider()
        slider.click()                              # 一般的验证码,只有点完slide按钮之后,才会出现缺口

        # 获取带缺口的验证码图片
        image2 = self.get_geetest_image('captcha2.png')

        # 获取缺口位置
        gap = self.get_gap(image1, image2)
        print('缺口位置', gap)

        # 减去缺口的位移(因为,滑块移动img1到img2的时候,允许有误差范围。在范围内,即使有几个像素差,也是可以接受的
        gap -= BODER

        # 获取移动轨迹
        track = self.get_track(gap)
        print('滑动轨迹',track)

        # 拖动滑块到缺口处。模拟人的行为习惯(先匀加速拖动后匀减速拖动),把需要拖动的总距离分成一段一段小的轨迹
        self.move_to_gap(slider, track)

        success = self.wait.until(
            EC.text_to_be_present_in_element((By.CLASS_NAME,'geetest_sucess_radar_tip_constant'),'验证成功')        # 证明极验成功
        )
        print(success)

        # 失败后重试
        if not success:
            self.crack()
        else:
            self.login()


if __name__ == '__main__':
    print('开始验证')
    crack = CrackGeetest()
    crack.crack()
    print('验证成功')

你可能感兴趣的:(Python爬虫 | 滑动验证码破解)