Python selenium破解騰訊滑塊驗證碼,Github開源項目分析


一、前言

最近一直在搞滑塊驗證碼,發現它比之前的極驗驗證碼又提升了一個檔次。驗證碼只提供兩張拼圖,不提供原圖。所以通過對比兩張圖片來尋找缺口的方法已經不適用了!所以要用一些圖像處理和計算機視覺相關的方法,比如openCV。但是這個東西太深奧了,又和python的另一個第三方庫:numpy緊密結合,所以一時半會是學不完的。咱畢竟是搞數據的又不是搞圖像的,我就在git上找了一些大佬的項目,然后拿過來分析一下,做了下簡單的修改,加了一些注釋。測試了一下,除了個別幾張圖片,大多還是能正確識別的。

Github鏈接----<<<<

二、分析

文件名login,類名Tencent

相關依賴

from selenium.webdriver.support import expected_conditions as EC 
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver import ActionChains
from selenium import webdriver
import cv2 as cv
import requests
import random
import time

安裝第三方庫openCV:pip install opencv-python
在這里插入圖片描述

構造函數

    def __init__(self, url, username, password):
        """
        初始化瀏覽器配置,聲明變量

        :param url: 要登錄的網站地址
        :param username: 賬號
        :param password: 密碼
        """
        # profile = webdriver.FirefoxOptions()  # 配置無頭
        # profile.add_argument('-headless')
        self.browser = webdriver.Chrome()
        self.wait = WebDriverWait(self.browser, 20)
        self.url = url  # 目標url
        self.username = username  # 用戶名
        self.password = password  # 密碼

注釋掉的代碼是大佬的,因為我要看一下滑塊滑動的過程,所以沒有配置無頭模式。

程序退出后的操作

    def end(self):
        """
        結束后退出,可選

        :return:
        """
        self.browser.quit()

大佬沒有寫析構函數,大概是因為end()可選吧

填寫個人信息

    def set_info(self):
        """
        填寫個人信息,在子類中完成

        """
        pass

這個是我后面加上去的,通過后期子類的繼承,可以定制不同網站的登錄。感覺在父類里面寫的話太亂了,於是寫個空的放在這里。
(強迫症)

保存圖片

    @staticmethod
    def save_img(bk_block):
        """
        保存圖片

        :param bk_block: 圖片url
        :return: bool類型,是否被保存
        """
        try:
            img = requests.get(bk_block).content
            with open('bg.jpeg', 'wb') as f:
                f.write(img)
            return True
        except:
            return False

缺口識別

@staticmethod
    def get_pos():
        """
        識別缺口
        注意:網頁上顯示的圖片為縮放圖片,縮放 50% 所以識別坐標需要 0.5

        :return: 缺口位置
        """
        image = cv.imread('bg.jpeg')
        # 高斯濾波
        blurred = cv.GaussianBlur(image, (5, 5), 0)
        # 邊緣檢測
        canny = cv.Canny(blurred, 200, 400)
        # 輪廓檢測
        contours, hierarchy = cv.findContours(
            canny, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
        for i, contour in enumerate(contours):
            m = cv.moments(contour)
            if m['m00'] == 0:
                cx = cy = 0
            else:
                cx, cy = m['m10'] / m['m00'], m['m01'] / m['m00']
            if 6000 < cv.contourArea(contour) < 8000 and 370 < cv.arcLength(contour, True) < 390:
                if cx < 400:
                    continue
                x, y, w, h = cv.boundingRect(contour)  # 外接矩形
                cv.rectangle(image, (x, y), (x + w, y + h), (0, 0, 255), 2)
                # cv.imshow('image', image)  # 顯示識別結果
                print('【缺口識別】 {x}px'.format(x=x / 2))
                return x / 2
        return 0

重點難點來了,openCV的高級應用。邊緣檢測和輪廓檢測看似簡單,但其中的參數很難把控,到了下面的for循環,我就暈了,大概就是找出輪廓的具體位置。
關於openCV目前我是不會再研究下去了,感興趣的朋友可以看看CSDN上的一個大佬的博客專欄:分享鏈接----<<<<

模擬滑塊軌跡

    @staticmethod
    def get_track(distance):
        """
        軌跡方程

        :param distance: 距缺口的距離
        :return: 位移列表
        """
        distance -= 75  # 初始位置
        # 初速度
        v = 0
        # 單位時間為0.2s來統計軌跡,軌跡即0.2內的位移
        t = 0.2
        # 位移/軌跡列表,列表內的一個元素代表0.2s的位移
        tracks = []
        # 當前的位移
        current = 0
        # 到達mid值開始減速
        mid = distance * 4 / 5

        distance += 10  # 先滑過一點,最后再反着滑動回來
        # a = random.randint(1,3)
        while current < distance:
            if current < mid:
                # 加速度越小,單位時間的位移越小,模擬的軌跡就越多越詳細
                # a = random.randint(2, 4)  # 加速運動
                a = 3
            else:
                # a = -random.randint(3, 5)  # 減速運動
                a = -2
            # 初速度
            v0 = v
            # 0.2秒時間內的位移
            s = v0 * t + 0.5 * a * (t ** 2)
            # 當前的位置
            current += s
            # 添加到軌跡列表
            tracks.append(round(s))

            # 速度已經達到v,該速度作為下次的初速度
            v = v0 + a * t

        # 反着滑動到大概准確位置
        for i in range(4):
            tracks.append(-random.randint(2, 3))
        for i in range(4):
            tracks.append(-random.randint(1, 3))
        return tracks

因為人們在滑動滑塊的時候大多是先加速后減速,有時還會倒退,所以不可以做勻速直線運動,要做變速,變加速直線運動。
高中物理知識:
初速度:v0、位移:x、時間:t、加速度:a,滿足如下公式:
x = v0 * t + 1/2 * a * t^2
滑塊的缺口位置一開始是-37的,但我經過調試后發現老是會滑到最右端超出了界限,所以改成了-75。mid為減速閾值,到了mid距離即開始減速。另外為了提高通過率,大佬還模仿了后退的操作。

主要實現函數

    def tx_code(self):
        """
        主要部分,函數入口

        :return: bool值,是否識別成功
        """
        self.set_info()

        WebDriverWait(self.browser, 20, 0.5).until(
            EC.presence_of_element_located((By.ID, 'tcaptcha_iframe')))  # 等待 iframe
        self.browser.switch_to.frame(
            self.browser.find_element_by_id('tcaptcha_iframe'))  # 加載 iframe
        time.sleep(0.5)
        bk_block = self.browser.find_element_by_xpath(
            '//img[@id="slideBg"]').get_attribute('src')
        print(bk_block)
        if self.save_img(bk_block):
            dex = self.get_pos()
            if dex:
                track_list = self.get_track(dex)
                time.sleep(0.5)
                slid_ing = self.browser.find_element_by_xpath(
                    '//div[@id="tcaptcha_drag_thumb"]')  # 滑塊定位
                ActionChains(self.browser).click_and_hold(
                    on_element=slid_ing).perform()  # 鼠標按下
                time.sleep(0.2)
                print('軌跡', track_list)
                for track in track_list:
                    ActionChains(self.browser).move_by_offset(
                        xoffset=track, yoffset=0).perform()  # 鼠標移動到距離當前位置(x,y)
                time.sleep(1)
                ActionChains(self.browser).release(
                    on_element=slid_ing).perform()  # print('第三步,釋放鼠標')
                time.sleep(1)
                # 識別圖片
                return True
            else:
                self.re_start()
        else:
            print('缺口圖片捕獲失敗')
            return False

這個就還是定位元素的老套路了,不多做解釋。

准備開始

    def re_start(self):
        """
        准備開始

        :return: None
        """
        self.tx_code()
        # self.end()

三、完整代碼及如何使用

基於PyCarm、python3.8、selenium3.141.0、opencv-python4.3.0.36

完整代碼

# -*- coding: utf-8 -*-
"""
@author:Pineapple

@contact:cppjavapython@foxmail.com

@time:2020/8/2 17:29

@file:login.py

@desc: login with Tencen .
"""

from selenium.webdriver.support import expected_conditions as EC  # 顯性等待
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver import ActionChains
from selenium import webdriver
import cv2 as cv
import requests
import random
import time


class Tencent:
    """
    識別騰訊驗證碼
    """

    def __init__(self, url, username, password):
        """
        初始化瀏覽器配置,聲明變量

        :param url: 要登錄的網站地址
        :param username: 賬號
        :param password: 密碼
        """
        # profile = webdriver.FirefoxOptions()  # 配置無頭
        # profile.add_argument('-headless')
        self.browser = webdriver.Chrome()
        self.wait = WebDriverWait(self.browser, 20)
        self.url = url  # 目標url
        self.username = username  # 用戶名
        self.password = password  # 密碼

    def end(self):
        """
        結束后退出,可選

        :return:
        """
        self.browser.quit()

    def set_info(self):
        """
        填寫個人信息,在子類中完成

        """
        pass

    def tx_code(self):
        """
        主要部分,函數入口

        :return:
        """
        self.set_info()

        WebDriverWait(self.browser, 20, 0.5).until(
            EC.presence_of_element_located((By.ID, 'tcaptcha_iframe')))  # 等待 iframe
        self.browser.switch_to.frame(
            self.browser.find_element_by_id('tcaptcha_iframe'))  # 加載 iframe
        time.sleep(0.5)
        bk_block = self.browser.find_element_by_xpath(
            '//img[@id="slideBg"]').get_attribute('src')
        print(bk_block)
        if self.save_img(bk_block):
            dex = self.get_pos()
            if dex:
                track_list = self.get_track(dex)
                time.sleep(0.5)
                slid_ing = self.browser.find_element_by_xpath(
                    '//div[@id="tcaptcha_drag_thumb"]')  # 滑塊定位
                ActionChains(self.browser).click_and_hold(
                    on_element=slid_ing).perform()  # 鼠標按下
                time.sleep(0.2)
                print('軌跡', track_list)
                for track in track_list:
                    ActionChains(self.browser).move_by_offset(
                        xoffset=track, yoffset=0).perform()  # 鼠標移動到距離當前位置(x,y)
                time.sleep(1)
                ActionChains(self.browser).release(
                    on_element=slid_ing).perform()  # print('第三步,釋放鼠標')
                time.sleep(1)
                # 識別圖片
                return True
            else:
                self.re_start()
        else:
            print('缺口圖片捕獲失敗')
            return False

    @staticmethod
    def save_img(bk_block):
        """
        保存圖片

        :param bk_block: 圖片url
        :return: bool類型,是否被保存
        """
        try:
            img = requests.get(bk_block).content
            with open('bg.jpeg', 'wb') as f:
                f.write(img)
            return True
        except:
            return False

    @staticmethod
    def get_pos():
        """
        識別缺口
        注意:網頁上顯示的圖片為縮放圖片,縮放 50% 所以識別坐標需要 0.5

        :return: 缺口位置
        """
        image = cv.imread('bg.jpeg')
        # 高斯濾波
        blurred = cv.GaussianBlur(image, (5, 5), 0)
        # 邊緣檢測
        canny = cv.Canny(blurred, 200, 400)
        # 輪廓檢測
        contours, hierarchy = cv.findContours(
            canny, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
        for i, contour in enumerate(contours):
            m = cv.moments(contour)
            if m['m00'] == 0:
                cx = cy = 0
            else:
                cx, cy = m['m10'] / m['m00'], m['m01'] / m['m00']
            if 6000 < cv.contourArea(contour) < 8000 and 370 < cv.arcLength(contour, True) < 390:
                if cx < 400:
                    continue
                x, y, w, h = cv.boundingRect(contour)  # 外接矩形
                cv.rectangle(image, (x, y), (x + w, y + h), (0, 0, 255), 2)
                # cv.imshow('image', image)  # 顯示識別結果
                print('【缺口識別】 {x}px'.format(x=x / 2))
                return x / 2
        return 0

    @staticmethod
    def get_track(distance):
        """
        軌跡方程

        :param distance: 距缺口的距離
        :return: 位移列表
        """
        distance -= 75  # 初始位置
        # 初速度
        v = 0
        # 單位時間為0.2s來統計軌跡,軌跡即0.2內的位移
        t = 0.2
        # 位移/軌跡列表,列表內的一個元素代表0.2s的位移
        tracks = []
        # 當前的位移
        current = 0
        # 到達mid值開始減速
        mid = distance * 4 / 5

        distance += 10  # 先滑過一點,最后再反着滑動回來
        # a = random.randint(1,3)
        while current < distance:
            if current < mid:
                # 加速度越小,單位時間的位移越小,模擬的軌跡就越多越詳細
                # a = random.randint(2, 4)  # 加速運動
                a = 3
            else:
                # a = -random.randint(3, 5)  # 減速運動
                a = -2
            # 初速度
            v0 = v
            # 0.2秒時間內的位移
            s = v0 * t + 0.5 * a * (t ** 2)
            # 當前的位置
            current += s
            # 添加到軌跡列表
            tracks.append(round(s))

            # 速度已經達到v,該速度作為下次的初速度
            v = v0 + a * t

        # 反着滑動到大概准確位置
        for i in range(4):
            tracks.append(-random.randint(2, 3))
        for i in range(4):
            tracks.append(-random.randint(1, 3))
        return tracks

    def move_to(self, index):
        """
        移動滑塊

        :param index:
        :return:
        """
        pass

    def re_start(self):
        """
        准備開始

        :return: None
        """
        self.tx_code()
        # self.end()

如何使用

前面說過了,或者代碼注釋里也有,要寫一個子類繼承它,並完成set_info()函數。
#text.py 和 login.py在同一目錄下

# -*- coding: utf-8 -*-
"""
@author:Pineapple

@contact:cppjavapython@foxmail.com

@time:2020/7/29 9:00

@file:#test.py

@desc: 測試-登錄騰訊網
"""

from selenium.common.exceptions import TimeoutException
from login import Tencent
from login import EC
from login import By
import time

class Tencent_net(Tencent):
    """
    Tencent的子類,完成set_info()函數
    """
    def set_info(self):
        """
        填寫表單信息

        :return: None
        """
        self.browser.get(url=self.url)
        try:
            # 首頁登錄按鈕
            login_button = self.wait.until(EC.element_to_be_clickable((
                By.CSS_SELECTOR, 'div#top-login > div.item.item-login.fl > a.l-login'
            )))
            login_button.click()
            # 登錄子頁面
            self.wait.until(EC.presence_of_all_elements_located((
                By.ID, 'ptlogin_iframe'
            )))
            # 進入登錄子頁面
            self.browser.switch_to.frame(
                self.browser.find_element_by_id('ptlogin_iframe')
            )
            # qq賬號登錄
            qq_login = self.wait.until(EC.element_to_be_clickable((
                By.ID, 'switcher_plogin'
            )))
            qq_login.click()
            # 用戶名
            input_username = self.wait.until(EC.presence_of_element_located((
                By.ID, 'u'
            )))
            # 密碼
            input_password = self.wait.until(EC.presence_of_element_located((
                By.ID, 'p'
            )))
            input_username.send_keys(self.username)
            input_password.send_keys(self.password)
            time.sleep(1)
            qq_login_button = self.wait.until(EC.element_to_be_clickable((
                By.ID, 'login_button'
            )))
            qq_login_button.click()
        except TimeoutException as e:
            print('Error:', e.args)
            self.set_info()

if __name__ == '__main__':
    url=input('url:')
    username=input('賬號:')
    password=input('密碼:')
    tencent=Tencent_net(url,username,password)
    tencent.re_start()

填坑1

在寫子類的過程中還是遇到了一些問題,就比如點擊賬號密碼登錄這個鏈接:
在這里插入圖片描述又是JavaScript:void(0)鏈接!!!和之前微博登錄的那個還不一樣,這個人為點擊是可以的,可就是模擬瀏覽器狂點無效。讓我一度誤認為這種鏈接不可點擊。
后來我把檢查元素的窗口放在右邊后才發現了問題:
在這里插入圖片描述原來登錄部分的頁面是個子頁面,之前被我蓋住了沒看到!selenium打開頁面后默認是在父級Frame里操作的,如果還有子Frame,它是不能獲取到子Frame的結點的。這時候就要用swith_to.frame()方法來切換Frame。

填坑2

如過你取消了配置無頭模式,在打開瀏覽器的時候:
不要把鼠標放在瀏覽器內!!!
不要把鼠標放在瀏覽器內!!!
不要把鼠標放在瀏覽器內!!!

重要的事情說三遍,因為識別到缺口后,selenium已經模擬鼠標按住並拖動了,這時候你把鼠標移到瀏覽器里面,而你又沒有按住,那它就會人為鼠標已經松開,釋放滑塊!

四、結果展示

在這里插入圖片描述

在這里插入圖片描述


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM