這一篇寫完好久了,由於識別率一直很低,沒辦法拿出來見你們,因此一直隱藏着,今天終於能夠拿出來見見陽光了。html
哈嘍,你們好,我是星星在線,我又來了,今天給你們帶來的是極驗驗證碼的selenium破解之法,是否是有點小激動呢,小夥伴們等不了了,讓咱們趕忙直入主題吧。python
此次咱們是拿虎嗅開刀,註冊帳號的時候須要滑動圖片到缺口位置,這種驗證碼咱們如今也常常遇到,這個就不用詳細介紹了吧
web
針對這種驗證碼咱們首先肯定了使用selenium模擬滑動破解方式,selenium鼠標移動點擊拖動都比較簡單,那麼問題就在於拖動多少距離,眼睛看起來很直觀,可是程序怎麼獲取呢?利用圖像識別......,額,這個只能想一想了吧。不如看看網頁源碼或者請求信息,看看有沒有有效的信息。ajax
鼠標右鍵點擊到圖片上,查看元素
chrome
這一瞬間的圖片,還好我二十幾年的麒麟臂沒白練,咱們看看元素查看到的都是什麼東西
瀏覽器
這看起來有點奇怪哦,有個圖片連接,還有位置信息,並且還那麼多,先把圖片連接拷貝到瀏覽器裏訪問下看看
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,這是肯定的,咱們找找看app
是13,難道每一小塊前面都多餘了1個像素?按照這種的話也應該是12呀,按照這種方式咱們繼續找一找剩下的,經過分析咱們發現每一個小塊+12做爲下一個小塊的起點。這樣的話左右各去掉一個像素,寬度不就是10了嗎?並且每一個小塊是12,26個是312,跟咱們看到的拼圖大小差很少,說明咱們分析的是正確的。按照元素裏提供的座標,取寬度爲10的大小便可。接下來分析一下這些座標的意義。less
分析一下咱們圖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的順序同樣。到這裏就差很少了。dom
總結一下:最終的圖片就是把拼圖,即圖4,按照x=15七、y=5八、w=十、h=58截取出來,放在上半部分第一個位置,x=14五、y=5八、w=十、h=58截取出來放在上半部分第二個位置,緊挨着第一個,以此類推,拼成一張整圖。
這個就是我拼出來的,恩,很好,很不錯嘛小夥子。不過好像哪裏不對,缺口嘞。仔細看看網頁元素
原來一個是fullbg,一個是cutbg,這個名字就頗有寓意嘛,那就行了,再把cutbg拼一下看看
這回就對上了。如今的問題就變成怎麼計算缺口位置了
我以爲可能會有計算兩張圖片不一樣位置的方式吧,度娘來一發,而後獲取了python實戰===用python對比兩張圖片的不一樣,而後發現了ImageChops.difference這個接口,結果大家知道的,不許確,爲啥捏?仔細看拼好的兩張圖,除了缺口還有其餘地方不同呀。看到圖16缺口後面那個陰影沒,讓個人內心蒙上了一層陰影,再觀察其餘的圖片,基本都有相似的,這可怎麼辦?這在後面還好說,若是是在前面呢,那不就計算到陰影裏去了嘛。若是這個對比有一個容差就行了,我之前用按鍵精靈的時候好像就有這種嘛,這個好不智能呀。既然它是對比像素,我直接取像素對比一下不就得了,並且我還不給它用==,給它一個範圍,若是色差在這個範圍內就算同樣了,這樣不就有容差了嗎?這個缺口通常都很是明顯,而陰影跟背景又很模糊,應該是可行的。思路就是獲取圖片的寬高,而後一個像素一個像素的遍歷對比。
這個色差怎麼肯定?一種方式就是調試,這種是比較麻煩的,還有一種方式就是獲取多張圖片,全圖和缺陷圖,而後使用取色工具,取對應位置的顏色值,肯定一個大概範圍。距離肯定了,下面就是移動了
selenium的模擬操做網上介紹不少,這裏咱們只要確認須要哪些接口就好了。
ActionChains方法:
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爬蟲實戰之路
也能夠掃描下面二維碼,添加個人微信號