Python破解滑块验证码算法,完美避开人机识别
| 完美是不可能的,加個(gè)震驚!Python破解BiliBili滑塊驗(yàn)證碼,完美避開(kāi)人機(jī)識(shí)別,可以有
準(zhǔn)備工作
- B站登錄頁(yè) https://passport.bilibili.com/login
- python3
- pip install selenium (webdriver框架)
- pip install PIL (圖片處理)
- chrome driver:http://chromedriver.storage.googleapis.com/index.html
- firefox driver:https://github.com/mozilla/geckodriver/releases
B站的滑塊驗(yàn)證碼如上。
這類驗(yàn)證碼可以使用 selenium 操作瀏覽器拖拽滑塊來(lái)進(jìn)行破解,難點(diǎn)兩個(gè),一個(gè)如何確定拖拽到的位置,另一個(gè)是避開(kāi)人機(jī)識(shí)別(反爬蟲(chóng))。
確定滑塊驗(yàn)證碼需要拖拽的位移距離
有三種方式
- 人工智能機(jī)器學(xué)習(xí),確定滑塊位置
- 通過(guò)完整圖片與缺失滑塊的圖片進(jìn)行像素對(duì)比,確定滑塊位置
- 邊緣檢測(cè)算法,確定位置
各有優(yōu)缺點(diǎn)。人工智能機(jī)器學(xué)習(xí),確定滑塊位置,需要進(jìn)行訓(xùn)練,比較麻煩,也可以看是否存在在線api可以調(diào)用。以下介紹其他兩種方式。
對(duì)比完整圖片與缺失滑塊的圖片
| 僅介紹,本文不進(jìn)行實(shí)現(xiàn)。對(duì)于B站來(lái)說(shuō),是準(zhǔn)確率最高的方式(100%),但不能保證未來(lái)B站的滑塊驗(yàn)證升級(jí),導(dǎo)致不可用。
B站的滑塊驗(yàn)證模塊,一共有三張圖片:完整圖、缺失滑塊圖、滑塊圖,都是由畫布繪制出的。類似于:
完整圖:
缺失滑塊圖:
滑塊圖:
HTML代碼類似于:
<div class="geetest_canvas_img geetest_absolute" style="display: block;"> <div class="geetest_slicebg geetest_absolute"><canvas class="geetest_canvas_bg geetest_absolute" height="160" width="260"></canvas><canvas class="geetest_canvas_slice geetest_absolute" width="260" height="160"></canvas> </div> <canvas class="geetest_canvas_fullbg geetest_fade geetest_absolute" height="160" width="260" style="display: none;"></canvas> </div>- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
只需要通過(guò)selenium獲取畫布元素,執(zhí)行js拿到畫布像素,遍歷完整圖和缺失滑塊圖的像素,一旦獲取到差異(需要允許少許像素誤差),像素矩陣x軸方向即是滑塊位置。
另外由于滑塊圖距離畫布坐標(biāo)原點(diǎn)有距離,還需要減去這部分距離。
最后使用 selenium 拖拽即可。
邊緣檢測(cè)算法,確定位置
| 滑塊基本上是個(gè)方形,通過(guò)算法確定方形起始位置即可。
介紹兩種方式
- 滑塊是方形的,存在垂直與水平的邊,該邊在缺失滑塊圖中基本都是灰黑的。遍歷像素找到基本都是灰黑的邊即可。
- 缺失滑塊圖中滑塊位置是灰黑封閉的。通過(guò)算法可以找到封閉區(qū)域,大小與滑塊相近,即是滑塊需要拖拽到的位置。
第二種實(shí)現(xiàn)起來(lái)有些復(fù)雜,不進(jìn)行實(shí)現(xiàn)了。
下面是第一種實(shí)現(xiàn)方式(只實(shí)現(xiàn)了垂直邊的檢測(cè),水平邊檢測(cè)原理一致),會(huì)存在檢測(cè)不出或錯(cuò)誤的情況,使用時(shí)需要換一張驗(yàn)證碼。也可能存在檢測(cè)出的邊是另一條(因?yàn)锽站的滑塊不是長(zhǎng)方形,存在弧形邊),那么需要減去滑塊寬度
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
使用selenium進(jìn)行滑動(dòng)驗(yàn)證(會(huì)失敗)
首先,我們需要從html中獲取滑塊驗(yàn)證的圖片,通過(guò)執(zhí)行js,將畫布像素轉(zhuǎn)為base64,然后python即可獲取,進(jìn)行拖拽處理:
from selenium import webdriver import time import base64 from PIL import Image from io import BytesIO from selenium.webdriver.support.ui import WebDriverWaitdef checkVeriImage(driver): WebDriverWait(driver, 5).until(lambda driver: driver.find_element_by_css_selector('.geetest_canvas_bg.geetest_absolute'))time.sleep(1)im_info = driver.execute_script('return document.getElementsByClassName("geetest_canvas_bg geetest_absolute")[0].toDataURL("image/png");')# 拿到base64編碼的圖片信息im_base64 = im_info.split(',')[1]# 轉(zhuǎn)為bytes類型im_bytes = base64.b64decode(im_base64)with open('./temp_bg.png', 'wb') as f:# 保存圖片到本地,方便查看預(yù)覽f.write(im_bytes)image_data = BytesIO(im_bytes)bgImage = Image.open(image_data)# 滑塊距離左邊有 5~10 像素左右誤差offsetX = VeriImageUtil().getVerticalLineOffsetX(bgImage)eleDrag = driver.find_element_by_css_selector(".geetest_slider_button")action_chains = webdriver.ActionChains(driver)action_chains.drag_and_drop_by_offset(eleDrag,offsetX-10,0).perform()- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
貌似可以了,但實(shí)際上,驗(yàn)證時(shí)會(huì)遇到“拼圖被怪物吃掉了,請(qǐng)重試”,導(dǎo)致失敗。這是因?yàn)楸粰z測(cè)到機(jī)器人(爬蟲(chóng))操作了。
避開(kāi)人機(jī)識(shí)別
| B站滑塊驗(yàn)證碼的人機(jī)識(shí)別,其實(shí)不咋滴,主要靠是否存在停留間隔來(lái)判斷。一開(kāi)始被網(wǎng)上文章誤導(dǎo),弄了什么距離=初速度乘以時(shí)間t + 1/2加速度乘以(時(shí)間平方)模擬拖拽,實(shí)際上是完全不對(duì)路的。
webdriver.ActionChains(driver).drag_and_drop_by_offset(eleDrag,offsetX-10,0).perform()?拖動(dòng)滑塊會(huì)導(dǎo)致驗(yàn)證失敗。在B站中,這是因?yàn)檫@個(gè)動(dòng)作太快了的緣故。
有的同學(xué)就打算直接加?time.sleep(1)?了,這么做是不會(huì)成功的,會(huì)提示拼圖被怪物吃掉了,請(qǐng)重試
實(shí)際上人做滑塊驗(yàn)證的過(guò)程可以歸為:手指快速拖拽驗(yàn)證碼到指定位置,修正誤差,停留一會(huì)兒,釋放滑塊。
簡(jiǎn)單實(shí)現(xiàn)
代碼可以簡(jiǎn)單實(shí)現(xiàn),都不需要模擬人修正拖拽誤差的過(guò)程,普通網(wǎng)站不會(huì)去統(tǒng)計(jì)這個(gè),至少B站不會(huì)。
def simpleSimulateDragX(self, source, targetOffsetX):"""簡(jiǎn)單拖拽模仿人的拖拽:快速沿著X軸拖動(dòng),直接一步到達(dá)正確位置,再暫停一會(huì)兒,然后釋放拖拽動(dòng)作B站是依據(jù)是否有暫停時(shí)間來(lái)分辨人機(jī)的,這個(gè)方法適用。:param source: :param targetOffsetX: :return: None"""#參考`drag_and_drop_by_offset(eleDrag,offsetX-10,0)`的實(shí)現(xiàn),使用move方法action_chains = webdriver.ActionChains(self.driver)# 點(diǎn)擊,準(zhǔn)備拖拽action_chains.click_and_hold(source)action_chains.pause(0.2)action_chains.move_by_offset(targetOffsetX,0)action_chains.pause(0.6)action_chains.release()action_chains.perform()- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
添加修正過(guò)程的實(shí)現(xiàn)
其實(shí)也就最后一段多出了fix的過(guò)程,?action_chains.move_by_offset(10,0)
def fixedSimulateDragX(self, source, targetOffsetX):#參考`drag_and_drop_by_offset(eleDrag,offsetX-10,0)`的實(shí)現(xiàn),使用move方法action_chains = webdriver.ActionChains(self.driver)# 點(diǎn)擊,準(zhǔn)備拖拽action_chains.click_and_hold(source)action_chains.pause(0.2)action_chains.move_by_offset(targetOffsetX-10,0)action_chains.pause(0.6)action_chains.move_by_offset(10,0)action_chains.pause(0.6)action_chains.release()action_chains.perform()- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
終極版實(shí)現(xiàn)
| 為了更像人類操作,可以進(jìn)行拖拽間隔時(shí)間和拖拽次數(shù)、距離的隨機(jī)化。雖然這對(duì)B站沒(méi)什么用,還可能會(huì)導(dǎo)致驗(yàn)證時(shí)間變久一些。
拖拽多次,可以使用循環(huán)遍歷,不過(guò)代碼可能不好理解,直接判斷就行,最多也就兩到3次就完成修正誤差的過(guò)程。
def __getRadomPauseScondes(self):""":return:隨機(jī)的拖動(dòng)暫停時(shí)間"""return random.uniform(0.6, 0.9)def simulateDragX(self, source, targetOffsetX):"""模仿人的拖拽動(dòng)作:快速沿著X軸拖動(dòng)(存在誤差),再暫停,然后修正誤差防止被檢測(cè)為機(jī)器人,出現(xiàn)“圖片被怪物吃掉了”等驗(yàn)證失敗的情況:param source:要拖拽的html元素:param targetOffsetX: 拖拽目標(biāo)x軸距離:return: None"""action_chains = webdriver.ActionChains(self.driver)# 點(diǎn)擊,準(zhǔn)備拖拽action_chains.click_and_hold(source)# 拖動(dòng)次數(shù),二到三次dragCount = random.randint(2, 3)if dragCount == 2:# 總誤差值sumOffsetx = random.randint(-15, 15)action_chains.move_by_offset(targetOffsetX + sumOffsetx, 0)# 暫停一會(huì)action_chains.pause(self.__getRadomPauseScondes())# 修正誤差,防止被檢測(cè)為機(jī)器人,出現(xiàn)圖片被怪物吃掉了等驗(yàn)證失敗的情況action_chains.move_by_offset(-sumOffsetx, 0)elif dragCount == 3:# 總誤差值sumOffsetx = random.randint(-15, 15)action_chains.move_by_offset(targetOffsetX + sumOffsetx, 0)# 暫停一會(huì)action_chains.pause(self.__getRadomPauseScondes())# 已修正誤差的和fixedOffsetX = 0# 第一次修正誤差if sumOffsetx < 0:offsetx = random.randint(sumOffsetx, 0)else:offsetx = random.randint(0, sumOffsetx)fixedOffsetX = fixedOffsetX + offsetxaction_chains.move_by_offset(-offsetx, 0)action_chains.pause(self.__getRadomPauseScondes())# 最后一次修正誤差action_chains.move_by_offset(-sumOffsetx + fixedOffsetX, 0)action_chains.pause(self.__getRadomPauseScondes())else:raise Exception("莫不是系統(tǒng)出現(xiàn)了問(wèn)題?!")# 參考action_chains.drag_and_drop_by_offset()action_chains.release()action_chains.perform()- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
終章(完整代碼)
| 示例代碼和效果圖。完整示例代碼本身只是示例,方便測(cè)試用的,不進(jìn)行成功驗(yàn)證等處理,驗(yàn)證成功后python會(huì)直接異常退出。
本文完整示例代碼如下
# -*- coding: utf-8 -*- # @Date:2020/2/15 2:09 # @Author: Lu # @Description bilibili滑塊驗(yàn)證碼識(shí)別。B站有反爬限制,過(guò)快地拖拽會(huì)提示“怪物吃了拼圖,請(qǐng)重試”。 # 目前B站有三張圖片,只要對(duì)比完整圖和缺失滑塊背景圖的像素,就可以得到偏移圖片y軸距離,減去滑塊空白距離=需要滑動(dòng)的像素距離 # 這里采用邊緣檢測(cè),檢測(cè)缺失滑塊的底圖是否存在一條灰色豎線,即認(rèn)為是滑塊目標(biāo)位置,存在失敗的概率,適用范圍應(yīng)該更大些。from selenium import webdriver import time import base64 from PIL import Image from io import BytesIO from selenium.webdriver.support.ui import WebDriverWait import random import copyclass VeriImageUtil():def __init__(self):self.defaultConfig = {"grayOffset": 20,"opaque": 1,"minVerticalLineCount": 30}self.config = copy.deepcopy(self.defaultConfig)def updateConfig(self, config):# temp = copy.deepcopy(config)for k in self.config:if k in config.keys():self.config[k] = config[k]def getMaxOffset(self, *args):# 計(jì)算偏移平均值最大的數(shù)av = sum(args) / len(args)maxOffset = 0for a in args:offset = abs(av - a)if offset > maxOffset:maxOffset = offsetreturn maxOffsetdef isGrayPx(self, r, g, b):# 是否是灰度像素點(diǎn),允許波動(dòng)offsetreturn self.getMaxOffset(r, g, b) < self.config["grayOffset"]def isDarkStyle(self, r, g, b):# 灰暗風(fēng)格return r < 128 and g < 128 and b < 128def isOpaque(self, px):# 不透明return px[3] >= 255 * self.config["opaque"]def getVerticalLineOffsetX(self, bgImage):# bgImage = Image.open("./image/bg.png")# bgImage.im.mode = 'RGBA'bgBytes = bgImage.load()x = 0while x < bgImage.size[0]:y = 0# 點(diǎn)》》線,灰度線條數(shù)量verticalLineCount = 0while y < bgImage.size[1]:px = bgBytes[x, y]r = px[0]g = px[1]b = px[2]# alph = px[3]# print(px)if self.isDarkStyle(r, g, b) and self.isGrayPx(r, g, b) and self.isOpaque(px):verticalLineCount += 1else:verticalLineCount = 0y += 1continueif verticalLineCount >= self.config["minVerticalLineCount"]:# 連續(xù)多個(gè)像素都是灰度像素,直線,認(rèn)為需要滑動(dòng)這么多# print(x, y)return xy += 1x += 1passclass DragUtil():def __init__(self, driver):self.driver = driverdef __getRadomPauseScondes(self):""":return:隨機(jī)的拖動(dòng)暫停時(shí)間"""return random.uniform(0.6, 0.9)def simulateDragX(self, source, targetOffsetX):"""模仿人的拖拽動(dòng)作:快速沿著X軸拖動(dòng)(存在誤差),再暫停,然后修正誤差防止被檢測(cè)為機(jī)器人,出現(xiàn)“圖片被怪物吃掉了”等驗(yàn)證失敗的情況:param source:要拖拽的html元素:param targetOffsetX: 拖拽目標(biāo)x軸距離:return: None"""action_chains = webdriver.ActionChains(self.driver)# 點(diǎn)擊,準(zhǔn)備拖拽action_chains.click_and_hold(source)# 拖動(dòng)次數(shù),二到三次dragCount = random.randint(2, 3)if dragCount == 2:# 總誤差值sumOffsetx = random.randint(-15, 15)action_chains.move_by_offset(targetOffsetX + sumOffsetx, 0)# 暫停一會(huì)action_chains.pause(self.__getRadomPauseScondes())# 修正誤差,防止被檢測(cè)為機(jī)器人,出現(xiàn)圖片被怪物吃掉了等驗(yàn)證失敗的情況action_chains.move_by_offset(-sumOffsetx, 0)elif dragCount == 3:# 總誤差值sumOffsetx = random.randint(-15, 15)action_chains.move_by_offset(targetOffsetX + sumOffsetx, 0)# 暫停一會(huì)action_chains.pause(self.__getRadomPauseScondes())# 已修正誤差的和fixedOffsetX = 0# 第一次修正誤差if sumOffsetx < 0:offsetx = random.randint(sumOffsetx, 0)else:offsetx = random.randint(0, sumOffsetx)fixedOffsetX = fixedOffsetX + offsetxaction_chains.move_by_offset(-offsetx, 0)action_chains.pause(self.__getRadomPauseScondes())# 最后一次修正誤差action_chains.move_by_offset(-sumOffsetx + fixedOffsetX, 0)action_chains.pause(self.__getRadomPauseScondes())else:raise Exception("莫不是系統(tǒng)出現(xiàn)了問(wèn)題?!")# 參考action_chains.drag_and_drop_by_offset()action_chains.release()action_chains.perform()def simpleSimulateDragX(self, source, targetOffsetX):"""簡(jiǎn)單拖拽模仿人的拖拽:快速沿著X軸拖動(dòng),直接一步到達(dá)正確位置,再暫停一會(huì)兒,然后釋放拖拽動(dòng)作B站是依據(jù)是否有暫停時(shí)間來(lái)分辨人機(jī)的,這個(gè)方法適用。:param source: :param targetOffsetX: :return: None"""action_chains = webdriver.ActionChains(self.driver)# 點(diǎn)擊,準(zhǔn)備拖拽action_chains.click_and_hold(source)action_chains.pause(0.2)action_chains.move_by_offset(targetOffsetX, 0)action_chains.pause(0.6)action_chains.release()action_chains.perform()def checkVeriImage(driver):WebDriverWait(driver, 5).until(lambda driver: driver.find_element_by_css_selector('.geetest_canvas_bg.geetest_absolute'))time.sleep(1)im_info = driver.execute_script('return document.getElementsByClassName("geetest_canvas_bg geetest_absolute")[0].toDataURL("image/png");')# 拿到base64編碼的圖片信息im_base64 = im_info.split(',')[1]# 轉(zhuǎn)為bytes類型im_bytes = base64.b64decode(im_base64)with open('./temp_bg.png', 'wb') as f:# 保存圖片到本地f.write(im_bytes)image_data = BytesIO(im_bytes)bgImage = Image.open(image_data)# 滑塊距離左邊有 5 像素左右誤差offsetX = VeriImageUtil().getVerticalLineOffsetX(bgImage)print("offsetX: {}".format(offsetX))if not type(offsetX) == int:# 計(jì)算不出,重新加載driver.find_element_by_css_selector(".geetest_refresh_1").click()checkVeriImage(driver)returnelif offsetX == 0:# 計(jì)算不出,重新加載driver.find_element_by_css_selector(".geetest_refresh_1").click()checkVeriImage(driver)returnelse:dragVeriImage(driver, offsetX)def dragVeriImage(driver, offsetX):# 可能產(chǎn)生檢測(cè)到右邊緣的情況# 拖拽eleDrag = driver.find_element_by_css_selector(".geetest_slider_button")dragUtil = DragUtil(driver)dragUtil.simulateDragX(eleDrag, offsetX - 10)time.sleep(2.5)if isNeedCheckVeriImage(driver):checkVeriImage(driver)returndragUtil.simulateDragX(eleDrag, offsetX - 6)time.sleep(2.5)if isNeedCheckVeriImage(driver):checkVeriImage(driver)return# 滑塊寬度40左右dragUtil.simulateDragX(eleDrag, offsetX - 56)time.sleep(2.5)if isNeedCheckVeriImage(driver):checkVeriImage(driver)returndragUtil.simulateDragX(eleDrag, offsetX - 52)if isNeedCheckVeriImage(driver):checkVeriImage(driver)returndef isNeedCheckVeriImage(driver):if driver.find_element_by_css_selector(".geetest_panel_error").is_displayed():driver.find_element_by_css_selector(".geetest_panel_error_content").click();return Truereturn Falsedef task():# 此步驟很重要,設(shè)置chrome為開(kāi)發(fā)者模式,防止被各大網(wǎng)站識(shí)別出來(lái)使用了Selenium# options = webdriver.ChromeOptions()# options.add_experimental_option('excludeSwitches', ['enable-automation'])options = webdriver.FirefoxOptions()# driver = webdriver.Firefox(executable_path=r"../../../res/webdriver/geckodriver_x64_0.26.0.exe",options=options)driver = webdriver.Firefox(executable_path=r"../../../res/webdriver/geckodriver_x64_0.26.0.exe",options=options)driver.get('https://passport.bilibili.com/login')time.sleep(3)driver.find_element_by_css_selector("#login-username").send_keys("1234567")driver.find_element_by_css_selector("#login-passwd").send_keys("abcdefg")driver.find_element_by_css_selector(".btn.btn-login").click()time.sleep(2)checkVeriImage(driver)pass# 該方法用來(lái)確認(rèn)元素是否存在,如果存在返回flag=true,否則返回false def isElementExist(driver, css):try:driver.find_element_by_css_selector(css)return Trueexcept:return Falseif __name__ == '__main__':task()總結(jié)
以上是生活随笔為你收集整理的Python破解滑块验证码算法,完美避开人机识别的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 决定c++语言中函数的返回值类型的是,全
- 下一篇: Python 代码优化常见技巧