一、前言
最近一直在搞滑塊驗證碼,發現它比之前的極驗驗證碼又提升了一個檔次。驗證碼只提供兩張拼圖,不提供原圖。所以通過對比兩張圖片來尋找缺口的方法已經不適用了!所以要用一些圖像處理和計算機視覺相關的方法,比如openCV。但是這個東西太深奧了,又和python的另一個第三方庫:numpy緊密結合,所以一時半會是學不完的。咱畢竟是搞數據的又不是搞圖像的,我就在git上找了一些大佬的項目,然后拿過來分析一下,做了下簡單的修改,加了一些注釋。測試了一下,除了個別幾張圖片,大多還是能正確識別的。
二、分析
文件名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已經模擬鼠標按住並拖動了,這時候你把鼠標移到瀏覽器里面,而你又沒有按住,那它就會人為鼠標已經松開,釋放滑塊!