Python破解BiliBili滑塊驗證碼,完美避開人機識別
| 完美是不可能的,加個震驚!Python破解BiliBili滑塊驗證碼,完美避開人機識別,可以有
準備工作
- B站登入頁 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站的滑塊驗證碼如上。
這類驗證碼可以使用 selenium 操作瀏覽器拖拽滑塊來進行破解,難點兩個,一個如何確定拖拽到的位置,另一個是避開人機識別(反爬蟲)。
確定滑塊驗證碼需要拖拽的位移距離
有三種方式
- 人工智慧機器學習,確定滑塊位置
- 通過完整圖片與缺失滑塊的圖片進行畫素對比,確定滑塊位置
- 邊緣檢測演算法,確定位置
各有優缺點。人工智慧機器學習,確定滑塊位置,需要進行訓練,比較麻煩,也可以看是否存在線上api可以呼叫。以下介紹其他兩種方式。
對比完整圖片與缺失滑塊的圖片
| 僅介紹,本文不進行實現。對於B站來說,是準確率最高的方式(100%),但不能保證未來B站的滑塊驗證升級,導致不可用。
B站的滑塊驗證模組,一共有三張圖片:完整圖、缺失滑塊圖、滑塊圖,都是由畫布繪製出的。類似於:
完整圖:
缺失滑塊圖:
滑塊圖:
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>
只需要通過selenium獲取畫布元素,執行js拿到畫布畫素,遍歷完整圖和缺失滑塊圖的畫素,一旦獲取到差異(需要允許少許畫素誤差),畫素矩陣x軸方向即是滑塊位置。
另外由於滑塊圖距離畫布座標原點有距離,還需要減去這部分距離。
最後使用 selenium 拖拽即可。
邊緣檢測演算法,確定位置
| 滑塊基本上是個方形,通過演算法確定方形起始位置即可。
介紹兩種方式
- 滑塊是方形的,存在垂直與水平的邊,該邊在缺失滑塊圖中基本都是灰黑的。遍歷畫素找到基本都是灰黑的邊即可。
- 缺失滑塊圖中滑塊位置是灰黑封閉的。通過演算法可以找到封閉區域,大小與滑塊相近,即是滑塊需要拖拽到的位置。
第二種實現起來有些複雜,不進行實現了。
下面是第一種實現方式(只實現了垂直邊的檢測,水平邊檢測原理一致),會存在檢測不出或錯誤的情況,使用時需要換一張驗證碼。也可能存在檢測出的邊是另一條(因為B站的滑塊不是長方形,存在弧形邊),那麼需要減去滑塊寬度
class 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): # 計算偏移平均值最大的數 av = sum(args) / len(args) maxOffset = 0 for a in args: offset = abs(av - a) if offset > maxOffset: maxOffset = offset return maxOffset def isGrayPx(self, r, g, b): # 是否是灰度畫素點,允許波動offset return self.getMaxOffset(r, g, b) < self.config["grayOffset"] def isDarkStyle(self, r, g, b): # 灰暗風格 return r < 128 and g < 128 and b < 128 def 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 = 0 while x < bgImage.size[0]: y = 0 # 點》》線,灰度線條數量 verticalLineCount = 0 while 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 += 1 else: verticalLineCount = 0 y += 1 continue if verticalLineCount >= self.config["minVerticalLineCount"]: # 連續多個畫素都是灰度畫素,直線 # print(x, y) return x y += 1 x += 1 pass if __name__ == '__main__': bgImage = Image.open("./image/bg.png") veriImageUtil = VeriImageUtil() # veriImageUtil.updateConfig({ # "grayOffset": 20, # "opaque": 0.6, # "minVerticalLineCount": 10 # }) bgOffsetX = veriImageUtil.getVerticalLineOffsetX(bgImage) print("bgOffsetX:{} ".format(bgOffsetX))
使用selenium進行滑動驗證(會失敗)
首先,我們需要從html中獲取滑塊驗證的圖片,通過執行js,將畫布畫素轉為base64,然後python即可獲取,進行拖拽處理:
from selenium import webdriver import time import base64 from PIL import Image from io import BytesIO from selenium.webdriver.support.ui import WebDriverWait 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] # 轉為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~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()
貌似可以了,但實際上,驗證時會遇到“拼圖被怪物吃掉了,請重試”,導致失敗。這是因為被檢測到機器人(爬蟲)操作了。
避開人機識別
| B站滑塊驗證碼的人機識別,其實不咋滴,主要靠是否存在停留間隔來判斷。一開始被網上文章誤導,弄了什麼距離=初速度乘以時間t + 1/2加速度乘以(時間平方)模擬拖拽,實際上是完全不對路的。
webdriver.ActionChains(driver).drag_and_drop_by_offset(eleDrag,offsetX-10,0).perform()拖動滑塊會導致驗證失敗。在B站中,這是因為這個動作太快了的緣故。
有的同學就打算直接加time.sleep(1)了,這麼做是不會成功的,會提示拼圖被怪物吃掉了,請重試
實際上人做滑塊驗證的過程可以歸為:手指快速拖拽驗證碼到指定位置,修正誤差,停留一會兒,釋放滑塊。
簡單實現
程式碼可以簡單實現,都不需要模擬人修正拖拽誤差的過程,普通網站不會去統計這個,至少B站不會。
def simpleSimulateDragX(self, source, targetOffsetX): """ 簡單拖拽模仿人的拖拽:快速沿著X軸拖動,直接一步到達正確位置,再暫停一會兒,然後釋放拖拽動作 B站是依據是否有暫停時間來分辨人機的,這個方法適用。 :param source: :param targetOffsetX: :return: None """ #參考`drag_and_drop_by_offset(eleDrag,offsetX-10,0)`的實現,使用move方法 action_chains = webdriver.ActionChains(self.driver) # 點選,準備拖拽 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()
新增修正過程的實現
其實也就最後一段多出了fix的過程,action_chains.move_by_offset(10,0)
def fixedSimulateDragX(self, source, targetOffsetX): #參考`drag_and_drop_by_offset(eleDrag,offsetX-10,0)`的實現,使用move方法 action_chains = webdriver.ActionChains(self.driver) # 點選,準備拖拽 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()
終極版實現
| 為了更像人類操作,可以進行拖拽間隔時間和拖拽次數、距離的隨機化。雖然這對B站沒什麼用,還可能會導致驗證時間變久一些。
拖拽多次,可以使用迴圈遍歷,不過程式碼可能不好理解,直接判斷就行,最多也就兩到3次就完成修正誤差的過程。
def __getRadomPauseScondes(self): """ :return:隨機的拖動暫停時間 """ return random.uniform(0.6, 0.9) def simulateDragX(self, source, targetOffsetX): """ 模仿人的拖拽動作:快速沿著X軸拖動(存在誤差),再暫停,然後修正誤差 防止被檢測為機器人,出現“圖片被怪物吃掉了”等驗證失敗的情況 :param source:要拖拽的html元素 :param targetOffsetX: 拖拽目標x軸距離 :return: None """ action_chains = webdriver.ActionChains(self.driver) # 點選,準備拖拽 action_chains.click_and_hold(source) # 拖動次數,二到三次 dragCount = random.randint(2, 3) if dragCount == 2: # 總誤差值 sumOffsetx = random.randint(-15, 15) action_chains.move_by_offset(targetOffsetX + sumOffsetx, 0) # 暫停一會 action_chains.pause(self.__getRadomPauseScondes()) # 修正誤差,防止被檢測為機器人,出現圖片被怪物吃掉了等驗證失敗的情況 action_chains.move_by_offset(-sumOffsetx, 0) elif dragCount == 3: # 總誤差值 sumOffsetx = random.randint(-15, 15) action_chains.move_by_offset(targetOffsetX + sumOffsetx, 0) # 暫停一會 action_chains.pause(self.__getRadomPauseScondes()) # 已修正誤差的和 fixedOffsetX = 0 # 第一次修正誤差 if sumOffsetx < 0: offsetx = random.randint(sumOffsetx, 0) else: offsetx = random.randint(0, sumOffsetx) fixedOffsetX = fixedOffsetX + offsetx action_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("莫不是系統出現了問題?!") # 參考action_chains.drag_and_drop_by_offset() action_chains.release() action_chains.perform()
終章(完整程式碼)
| 示例程式碼和效果圖。完整示例程式碼本身只是示例,方便測試用的,不進行成功驗證等處理,驗證成功後python會直接異常退出。
本文完整示例程式碼如下
# -*- coding: utf-8 -*- # @Date:2020/2/15 2:09 # @Author: Lu # @Description bilibili滑塊驗證碼識別。B站有反爬限制,過快地拖拽會提示“怪物吃了拼圖,請重試”。 # 目前B站有三張圖片,只要對比完整圖和缺失滑塊背景圖的畫素,就可以得到偏移圖片y軸距離,減去滑塊空白距離=需要滑動的畫素距離 # 這裡採用邊緣檢測,檢測缺失滑塊的底圖是否存在一條灰色豎線,即認為是滑塊目標位置,存在失敗的概率,適用範圍應該更大些。 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 copy class 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): # 計算偏移平均值最大的數 av = sum(args) / len(args) maxOffset = 0 for a in args: offset = abs(av - a) if offset > maxOffset: maxOffset = offset return maxOffset def isGrayPx(self, r, g, b): # 是否是灰度畫素點,允許波動offset return self.getMaxOffset(r, g, b) < self.config["grayOffset"] def isDarkStyle(self, r, g, b): # 灰暗風格 return r < 128 and g < 128 and b < 128 def 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 = 0 while x < bgImage.size[0]: y = 0 # 點》》線,灰度線條數量 verticalLineCount = 0 while 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 += 1 else: verticalLineCount = 0 y += 1 continue if verticalLineCount >= self.config["minVerticalLineCount"]: # 連續多個畫素都是灰度畫素,直線,認為需要滑動這麼多 # print(x, y) return x y += 1 x += 1 pass class DragUtil(): def __init__(self, driver): self.driver = driver def __getRadomPauseScondes(self): """ :return:隨機的拖動暫停時間 """ return random.uniform(0.6, 0.9) def simulateDragX(self, source, targetOffsetX): """ 模仿人的拖拽動作:快速沿著X軸拖動(存在誤差),再暫停,然後修正誤差 防止被檢測為機器人,出現“圖片被怪物吃掉了”等驗證失敗的情況 :param source:要拖拽的html元素 :param targetOffsetX: 拖拽目標x軸距離 :return: None """ action_chains = webdriver.ActionChains(self.driver) # 點選,準備拖拽 action_chains.click_and_hold(source) # 拖動次數,二到三次 dragCount = random.randint(2, 3) if dragCount == 2: # 總誤差值 sumOffsetx = random.randint(-15, 15) action_chains.move_by_offset(targetOffsetX + sumOffsetx, 0) # 暫停一會 action_chains.pause(self.__getRadomPauseScondes()) # 修正誤差,防止被檢測為機器人,出現圖片被怪物吃掉了等驗證失敗的情況 action_chains.move_by_offset(-sumOffsetx, 0) elif dragCount == 3: # 總誤差值 sumOffsetx = random.randint(-15, 15) action_chains.move_by_offset(targetOffsetX + sumOffsetx, 0) # 暫停一會 action_chains.pause(self.__getRadomPauseScondes()) # 已修正誤差的和 fixedOffsetX = 0 # 第一次修正誤差 if sumOffsetx < 0: offsetx = random.randint(sumOffsetx, 0) else: offsetx = random.randint(0, sumOffsetx) fixedOffsetX = fixedOffsetX + offsetx action_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("莫不是系統出現了問題?!") # 參考action_chains.drag_and_drop_by_offset() action_chains.release() action_chains.perform() def simpleSimulateDragX(self, source, targetOffsetX): """ 簡單拖拽模仿人的拖拽:快速沿著X軸拖動,直接一步到達正確位置,再暫停一會兒,然後釋放拖拽動作 B站是依據是否有暫停時間來分辨人機的,這個方法適用。 :param source: :param targetOffsetX: :return: None """ action_chains = webdriver.ActionChains(self.driver) # 點選,準備拖拽 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] # 轉為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: # 計算不出,重新載入 driver.find_element_by_css_selector(".geetest_refresh_1").click() checkVeriImage(driver) return elif offsetX == 0: # 計算不出,重新載入 driver.find_element_by_css_selector(".geetest_refresh_1").click() checkVeriImage(driver) return else: dragVeriImage(driver, offsetX) def dragVeriImage(driver, offsetX): # 可能產生檢測到右邊緣的情況 # 拖拽 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) return dragUtil.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) return dragUtil.simulateDragX(eleDrag, offsetX - 52) if isNeedCheckVeriImage(driver): checkVeriImage(driver) return def 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 True return False def task(): # 此步驟很重要,設定chrome為開發者模式,防止被各大網站識別出來使用了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 # 該方法用來確認元素是否存在,如果存在返回flag=true,否則返回false def isElementExist(driver, css): try: driver.find_element_by_css_selector(css) return True except: return False if __name__ == '__main__': task()
學會了嗎,如需獲取原始碼的話加群哦:1136192749