1. 程式人生 > >極驗驗證碼破解之selenium

極驗驗證碼破解之selenium

抖動 位數 print fresh 調試 start 這樣的 破解 分享

這一篇寫完很久了,因為識別率一直很低,沒辦法拿出來見大家,所以一直隱藏著,今天終於可以拿出來見見陽光了。

哈嘍,大家好,我是星星在線,我又來了,今天給大家帶來的是極驗驗證碼的selenium破解之法,是不是有點小激動呢,小夥伴們等不了了,讓我們趕緊直入主題吧。

虎嗅網註冊

這次我們是拿虎嗅開刀,註冊賬號的時候需要滑動圖片到缺口位置,這種驗證碼我們現在也經常遇到,這個就不用詳細介紹了吧
技術分享圖片

針對這種驗證碼我們首先確定了使用selenium模擬滑動破解方式,selenium鼠標移動點擊拖動都比較簡單,那麽問題就在於拖動多少距離,眼睛看起來很直觀,但是程序怎麽獲取呢?利用圖像識別......,額,這個只能想想了吧。不如看看網頁源碼或者請求信息,看看有沒有有效的信息。

查看網頁信息

鼠標右鍵點擊到圖片上,查看元素
技術分享圖片

這一瞬間的圖片,還好我二十幾年的麒麟臂沒白練,我們看看元素查看到的都是什麽東西
技術分享圖片

這看起來有點奇怪哦,有個圖片鏈接,還有位置信息,而且還那麽多,先把圖片鏈接拷貝到瀏覽器裏訪問下看看
技術分享圖片
WF,這是什麽鬼?註意到那個像豬尾巴一樣的6了嗎?還有那個小箭頭,跟上面完整圖片對比一下,發現把箭頭挪動到小6旁邊,豬尾巴就成功了。當然你仔細觀察的話,還有其他的比如文字也是類似。那麽我們可以確認這張圖片應該是被打亂的,如果我們可以把它拼起來,是不是就離計算缺口位置比較近了。現在我們應該要註意到元素查看裏後面的位置信息了,那麽多,看起來應該跟這個打亂順序有點關系吧。我們來確認一下。我的想法是這樣子的,既然這個位置和拼圖有關,而且再看我們上面麒麟臂截的圖,我再標記一下
技術分享圖片


我們點擊查看元素的時候,瀏覽器會幫我們突出顯示一下,本來我是在圖片上點擊查看的,按照我的想法,它不是應該整張圖片突出顯示一下嗎?看起來好像不是這麽回事,只有那麽一小部分,而且上面還有元素信息,寬高類名,再回去看看圖3,位置坐標裏,前面應該是x軸,後面是y軸,y軸只有58和0,再根據圖2一看,圖片分為上下兩部分,再數一下div的數量,26塊,每一塊寬10x高58。按照這個來算的話,那麽整個圖片的寬就是260,高116,用截圖工具去拉一下圖片的寬高,基本吻合
技術分享圖片
接下來就是確定怎麽拼了。這裏很抱歉的告訴大家,豬沒了,等我寫到這裏再去查看網頁的時候,圖片已經刷新了。所以接下來的截圖可能不一樣,在這裏提前跟大家說明一下
。反正就是找特征點嘛,每個圖片應該都有的。先隨便找一個特征點,查看元素,看它定位到那個div元素那裏,然後再看看後面的位置。基本就是這樣,所以我們找圖片既然和位置有關,那麽我們最好選一些位置明顯的地方,比如中間,或者兩邊。
技術分享圖片
這個差不多算中間位置了吧,查那麽一點點無所謂了
技術分享圖片
我去,這......跟我想的不太一樣呀,再找兩張看看,代表性及其強烈的
技術分享圖片
技術分享圖片
技術分享圖片
技術分享圖片
為了防止有人說我水字數,另外兩個角就不截圖了。到這一步可能有人納悶了,為啥?你剛才說圖片寬度260,為什麽坐標裏出現了289這樣的坐標,這不就是超標了嗎?一開始我也有這樣的疑惑,可能我們看到圖片比實際的小,也許人家在圖片外面還留了邊框呢,我一開始是這麽想的。但是這個坐標是前面url裏面的圖片坐標,然後我就去看了一下圖4
技術分享圖片
這個圖片盡然比較大,坐標問題有答案了,但是這個跟260有什麽關系呢?打亂的圖片比較大,拼好的小,那它是怎麽拼的呢?幸好我們看到了一個比較有用信息
技術分享圖片

技術分享圖片

看到這個-1px了嗎?它成功引起了我的註意,因為按照我的想法,如果是從拼圖裏拿出一部分拼成一個完成圖片的話,那麽最左邊拿出來的圖片,應該是從(0,0),(0,58),但是我們看到的是(1,0),(1,58),y值還是比較符合我們的預期的,第一部分從0開始,高58,第二部分從58開始。但是x值有點問題,按照1作為起點,那第二個應該是11,因為寬度是10,這是確定的,我們找找看

技術分享圖片

是13,難道每一小塊前面都多余了1個像素?按照這種的話也應該是12呀,按照這種方式我們繼續找一找剩下的,通過分析我們發現每個小塊+12作為下一個小塊的起點。這樣的話左右各去掉一個像素,寬度不就是10了嗎?而且每個小塊是12,26個是312,跟我們看到的拼圖大小差不多,說明我們分析的是正確的。按照元素裏提供的坐標,取寬度為10的大小即可。接下來分析一下這些坐標的意義。

坐標分析

分析一下我們圖9到圖12的截圖,首先說圖9,我本來覺得它x、y應該是0,就算不是0,也應該是各位數字吧,結果的y是58,這個算到下半截圖片區域了,x是157,跑中場去了。圖11呢,你的x應該在300左右,y應該100以上吧,結果y是0,到上半段,x是205,在中場偏後,離守門員還遠呢。這是怎麽肥事?不過我們發現了,圖9在元素裏是第一個,圖11在元素裏是最後一個,再結合坐標前面的y值全是58,後面的y值全是0,符合我們上半段下半段顛倒的想法了,然後你再分別查看圖9右邊/圖11左邊的元素就會發現,和元素裏面div的順序一樣。到這裏就差不多了。

總結一下:最終的圖片就是把拼圖,即圖4,按照x=157、y=58、w=10、h=58截取出來,放在上半部分第一個位置,x=145、y=58、w=10、h=58截取出來放在上半部分第二個位置,緊挨著第一個,以此類推,拼成一張整圖。
技術分享圖片
這個就是我拼出來的,恩,很好,很不錯嘛小夥子。不過好像哪裏不對,缺口嘞。仔細看看網頁元素
技術分享圖片
原來一個是fullbg,一個是cutbg,這個名字就很有寓意嘛,那就好了,再把cutbg拼一下看看
技術分享圖片
這回就對上了。現在的問題就變成怎麽計算缺口位置了

缺口位置

我覺得可能會有計算兩張圖片不同位置的方式吧,度娘來一發,然後獲取了python實戰===用python對比兩張圖片的不同,然後發現了ImageChops.difference這個接口,結果你們知道的,不準確,為啥捏?仔細看拼好的兩張圖,除了缺口還有其他地方不一樣呀。看到圖16缺口後面那個陰影沒,讓我的心裏蒙上了一層陰影,再觀察其他的圖片,基本都有類似的,這可怎麽辦?這在後面還好說,如果是在前面呢,那不就計算到陰影裏去了嘛。如果這個對比有一個容差就好了,我以前用按鍵精靈的時候好像就有這種嘛,這個好不智能呀。既然它是對比像素,我直接取像素對比一下不就得了,而且我還不給它用==,給它一個範圍,如果色差在這個範圍內就算一樣了,這樣不就有容差了嗎?這個缺口一般都非常明顯,而陰影跟背景又很模糊,應該是可行的。思路就是獲取圖片的寬高,然後一個像素一個像素的遍歷對比。

色差

這個色差怎麽確定?一種方式就是調試,這種是比較麻煩的,還有一種方式就是獲取多張圖片,全圖和缺陷圖,然後使用取色工具,取對應位置的顏色值,確定一個大概範圍。距離確定了,下面就是移動了

selenium模擬移動

selenium的模擬操作網上介紹很多,這裏我們只要確認需要哪些接口就行了。
ActionChains方法:

  • move_to_element(to_element) - 鼠標移動到某個元素
  • click_and_hold(on_element =None) - 點擊鼠標左鍵,不松開
  • move_by_offset(xoffset,yoffset) - 鼠標從當前位置移動到某個坐標
  • release(on_element = None) - 在某個元素位置松開鼠標左鍵
  • perform() - 執行操作,記住這個很重要,調用上面的方法後,一定要執行perform才能真正執行

selenium的操作我就不詳細描述了,這裏用到的都是比較簡單的用法。

原理分析就完了,這一次必須要貼代碼了,否則可能很多人完成不了,也有利於大家的理解。

# -*- coding: utf-8 -*-
import random
import time, re
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
from PIL import Image
import requests
from io import BytesIO

class HuXiu(object):
    def __init__(self):
        chrome_option = webdriver.ChromeOptions()
        # chrome_option.set_headless()

        self.driver = webdriver.Chrome(executable_path=r"/usr1/webdrivers/chromedriver", chrome_options=chrome_option)
        self.driver.set_window_size(1440, 900)

    def visit_index(self):
        self.driver.get("https://www.huxiu.com/")

        WebDriverWait(self.driver, 10, 0.5).until(EC.element_to_be_clickable((By.XPATH, ‘//*[@class="js-register"]‘)))
        reg_element = self.driver.find_element_by_xpath(‘//*[@class="js-register"]‘)
        reg_element.click()

        WebDriverWait(self.driver, 10, 0.5).until(EC.element_to_be_clickable((By.XPATH, ‘//div[@class="gt_slider_knob gt_show"]‘)))

        # 進入模擬拖動流程
        self.analog_drag()

    def analog_drag(self):
        #鼠標移動到拖動按鈕,顯示出拖動圖片
        element = self.driver.find_element_by_xpath(‘//div[@class="gt_slider_knob gt_show"]‘)
        ActionChains(self.driver).move_to_element(element).perform()
        time.sleep(3)

        # 刷新一下極驗圖片
        element = self.driver.find_element_by_xpath(‘//a[@class="gt_refresh_button"]‘)
        element.click()
        time.sleep(1)

        # 獲取圖片地址和位置坐標列表
        cut_image_url, cut_location = self.get_image_url(‘//div[@class="gt_cut_bg_slice"]‘)
        full_image_url, full_location = self.get_image_url(‘//div[@class="gt_cut_fullbg_slice"]‘)

        # 根據坐標拼接圖片
        cut_image = self.mosaic_image(cut_image_url, cut_location)
        full_image = self.mosaic_image(full_image_url, full_location)

        # 保存圖片方便查看
        cut_image.save("cut.jpg")
        full_image.save("full.jpg")

        # 根據兩個圖片計算距離
        distance = self.get_offset_distance(cut_image, full_image)

        # 開始移動
        self.start_move(distance)

        # 如果出現error
        try:
            WebDriverWait(self.driver, 5, 0.5).until(EC.element_to_be_clickable((By.XPATH, ‘//div[@class="gt_ajax_tip gt_error"]‘)))
            print("驗證失敗")
            return
        except TimeoutException as e:
            pass

        # 判斷是否驗證成功
        try:
            WebDriverWait(self.driver, 10, 0.5).until(EC.element_to_be_clickable((By.XPATH, ‘//div[@class="gt_ajax_tip gt_success"]‘)))
        except TimeoutException:
            print("again times")
            time.sleep(5)
            # 失敗後遞歸執行拖動
            self.analog_drag()
        else:
            # 成功後輸入手機號,發送驗證碼
            self.register()

    # 獲取圖片和位置列表
    def get_image_url(self, xpath):
        link = re.compile(‘background-image: url\("(.*?)"\); background-position: (.*?)px (.*?)px;‘)
        elements = self.driver.find_elements_by_xpath(xpath)
        image_url = None
        location = list()
        for element in elements:
            style = element.get_attribute("style")
            groups = link.search(style)
            url = groups[1]
            x_pos = groups[2]
            y_pos = groups[3]
            location.append((int(x_pos), int(y_pos)))
            image_url = url
        return image_url, location

    # 拼接圖片
    def mosaic_image(self, image_url, location):
        resq = requests.get(image_url)
        file = BytesIO(resq.content)
        img = Image.open(file)
        image_upper_lst = []
        image_down_lst = []
        for pos in location:
            if pos[1] == 0:
                # y值==0的圖片屬於上半部分,高度58
                image_upper_lst.append(img.crop((abs(pos[0]), 0, abs(pos[0]) + 10, 58)))
            else:
                # y值==58的圖片屬於下半部分
                image_down_lst.append(img.crop((abs(pos[0]), 58, abs(pos[0]) + 10, img.height)))

        x_offset = 0
        # 創建一張畫布,x_offset主要為新畫布使用
        new_img = Image.new("RGB", (260, img.height))
        for img in image_upper_lst:
            new_img.paste(img, (x_offset, 58))
            x_offset += img.width

        x_offset = 0
        for img in image_down_lst:
            new_img.paste(img, (x_offset, 0))
            x_offset += img.width

        return new_img

    # 判斷顏色是否相近
    def is_similar_color(self, x_pixel, y_pixel):
        for i, pixel in enumerate(x_pixel):
            if abs(y_pixel[i] - pixel) > 50:
                return False
        return True

    # 計算距離
    def get_offset_distance(self, cut_image, full_image):
        for x in range(cut_image.width):
            for y in range(cut_image.height):
                cpx = cut_image.getpixel((x, y))
                fpx = full_image.getpixel((x, y))
                if not self.is_similar_color(cpx, fpx):
                    img = cut_image.crop((x, y, x + 50, y + 40))
                    # 保存一下計算出來位置圖片,看看是不是缺口部分
                    img.save("1.jpg")
                    return x

    # 開始移動
    def start_move(self, distance):
        element = self.driver.find_element_by_xpath(‘//div[@class="gt_slider_knob gt_show"]‘)

        # 這裏就是根據移動進行調試,計算出來的位置不是百分百正確的,加上一點偏移
        distance -= element.size.get(‘width‘) / 2
        distance += 15

        # 按下鼠標左鍵
        ActionChains(self.driver).click_and_hold(element).perform()
        time.sleep(0.5)
        while distance > 0:
            if distance > 10:
                # 如果距離大於10,就讓他移動快一點
                span = random.randint(5, 8)
            else:
                # 快到缺口了,就移動慢一點
                span = random.randint(2, 3)
            ActionChains(self.driver).move_by_offset(span, 0).perform()
            distance -= span
            time.sleep(random.randint(10,50)/100)
        
        ActionChains(self.driver).move_by_offset(distance, 1).perform()
        ActionChains(self.driver).release(on_element=element).perform()

    def register(self):
        element = self.driver.find_element_by_xpath(‘//input[@id="sms_username"]‘)
        element.clear()
        element.send_keys("手機號")

        ele_captcha = self.driver.find_element_by_xpath(‘//span[@class="js-btn-captcha btn-captcha"]‘)
        ele_captcha.click()

if __name__ == "__main__":
    h = HuXiu()
    h.visit_index()

這個移動move_by_offset,我之前的y值也是隨機的[-5,5],我覺得這個模擬會更真實一點,總會上下抖動的嘛,結果就是因為這個考慮的太人性了,識別率非常低,改了好多範圍,更大的、更小的,結果最後不偏移,竟然識別率奇高。TMD考慮的太人性化了竟然識別不了,我也是醉了。最後再把執行效果發一下吧
技術分享圖片


如果你覺得我的文章還可以,可以關註我的微信公眾號:Python爬蟲實戰之路
也可以掃描下面二維碼,添加我的微信號
技術分享圖片

技術分享圖片

極驗驗證碼破解之selenium