本節我們将介紹新浪微網誌宮格驗證碼的識别。微網誌宮格驗證碼是一種新型互動式驗證碼,每個宮格之間會有一條訓示連線,訓示了應該的滑動軌迹。我們要按照滑動軌迹依次從起始宮格滑動到終止宮格,才可以完成驗證,如下圖所示。
滑鼠滑動後的軌迹會以黃色的連線來辨別,如下圖所示。通路新浪微網誌移動版登入頁面,就可以看到如上驗證碼,連結為https://passport.weibo.cn/signin/login。不是每次登入都會出現驗證碼,當頻繁登入或者賬号存在安全風險的時候,驗證碼才會出現。
一、本節目标
我們的目标是用程式來識别并通過微網誌宮格驗證碼的驗證。
二、準備工作
本次我們使用的Python庫是Selenium,使用的浏覽器為Chrome,請確定已經正确安裝好Selenium庫、Chrome浏覽器,并配置好ChromeDriver。
三、識别思路
識别從探尋規律入手。規律就是,此驗證碼的四個宮格一定是有連線經過的,每一條連線上都會相應的訓示箭頭,連線的形狀多樣,包括C型、Z型、X型等,如下圖所示。
我們發現,同一類型的連線軌迹是相同的,唯一不同的就是連線的方向,如下圖所示。
這兩種驗證碼的連線軌迹是相同的。但是由于連線上面的訓示箭頭不同,導緻滑動的宮格順序有所不同。
如果要完全識别滑動宮格順序,就需要具體識别出箭頭的朝向。而整個驗證碼箭頭朝向一共有8種,而且會出現在不同的位置。如果要寫一個箭頭方向識别算法,需要考慮不同箭頭所在的位置,找出各個位置箭頭的像素點坐标,計算像素點變化規律,這個工作量就會變得比較大。
這時我們可以考慮用模闆比對的方法,就是将一些識别目标提前儲存并做好标記,這稱作模闆。這裡将驗證碼圖檔做好拖動順序的标記當做模闆。對比要新識别的目标和每一個模闆,如果找到比對的模闆,則就成功識别出要新識别的目标。在圖像識别中,模闆比對也是常用的方法,實作簡單且易用性好。
我們必須要收集到足夠多的模闆,模闆比對方法的效果才會好。而對于微網誌宮格驗證碼來說,宮格隻有4個,驗證碼的樣式最多4×3×2×1=24種,則我們可以将所有模闆都收集下來。
接下來我們需要考慮的就是,用何種模闆來進行比對,隻比對箭頭還是比對整個驗證碼全圖呢?我們權衡一下這兩種方式的比對精度和工作量。
首先是精度問題。如果是比對箭頭,比對的目标隻有幾個像素點範圍的箭頭,我們需要精确知道各個箭頭所在的像素點,一旦像素點有偏差,那麼會直接錯位,導緻比對結果大打折扣。如果是比對全圖,我們無需關心箭頭所在位置,同時還有連線幫助輔助比對。顯然,全圖比對的精度更高。其次是工作量的問題。如果是比對箭頭,我們需要儲存所有不同朝向的箭頭模闆,而相同位置箭頭的朝向可能不一,相同朝向的箭頭位置可能不一,那麼我們需要算出每個箭頭的位置并将其逐個截出儲存成模闆,依次探尋驗證碼對應位置是否有比對模闆。如果是比對全圖,我們不需要關心每個箭頭的位置和朝向,隻需要将驗證碼全圖儲存下來即可,在比對的時候也不需要計算箭頭的位置。顯然,比對全圖的工作量更少。
綜上考慮,我們選用全圖比對的方式來進行識别。找到比對的模闆之後,我們就可以得到事先為模闆定義的拖動順序,然後模拟拖動即可。
四、擷取模闆
我們需要做一下準備工作。先儲存24張驗證碼全圖。因為驗證碼是随機的,一共有24種。我們可以寫一段程式來批量儲存驗證碼圖檔,然後從中篩選出需要的圖檔,代碼如下所示:
import time
from io import BytesIO
from PIL import Image
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
USERNAME = ''
PASSWORD = ''
class CrackWeiboSlide():
def __init__(self):
self.url = 'https://passport.weibo.cn/signin/login'
self.browser = webdriver.Chrome()
self.wait = WebDriverWait(self.browser, 20)
self.username = USERNAME
self.password = PASSWORD
def __del__(self):
self.browser.close()
def open(self):
"""
打開網頁輸入使用者名密碼并點選
:return: None
"""
self.browser.get(self.url)
username = self.wait.until(EC.presence_of_element_located((By.ID, 'loginName')))
password = self.wait.until(EC.presence_of_element_located((By.ID, 'loginPassword')))
submit = self.wait.until(EC.element_to_be_clickable((By.ID, 'loginAction')))
username.send_keys(self.username)
password.send_keys(self.password)
submit.click()
def get_position(self):
"""
擷取驗證碼位置
:return: 驗證碼位置元組
"""
try:
img = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'patt-shadow')))
except TimeoutException:
print('未出現驗證碼')
self.open()
time.sleep(2)
location = img.location
size = img.size
top, bottom, left, right = location['y'], location['y'] + size['height'], location['x'], location['x'] + size['width']
return (top, bottom, left, right)
def get_screenshot(self):
"""
擷取網頁截圖
:return: 截圖對象
"""
screenshot = self.browser.get_screenshot_as_png()
screenshot = Image.open(BytesIO(screenshot))
return screenshot
def get_image(self, name='captcha.png'):
"""
擷取驗證碼圖檔
:return: 圖檔對象
"""
top, bottom, left, right = self.get_position()
print('驗證碼位置', top, bottom, left, right)
screenshot = self.get_screenshot()
captcha = screenshot.crop((left, top, right, bottom))
captcha.save(name)
return captcha
def main(self):
"""
批量擷取驗證碼
:return: 圖檔對象
"""
count = 0
while True:
self.open()
self.get_image(str(count) + '.png')
count += 1
if __name__ == '__main__':
crack = CrackWeiboSlide()
crack.main()
這裡需要将
USERNAME
和
PASSWORD
修改為自己微網誌的使用者名和密碼。運作一段時間後,本地多了很多以數字命名的驗證碼,如下圖所示。
這裡我們隻需要挑選出不同的24張驗證碼圖檔并命名儲存。名稱可以直接取作宮格的滑動的順序,如下圖所示。
我們将圖檔命名為4132.png,代表滑動順序為4-1-3-2。按照這樣的規則,我們将驗證碼整理為如下24張圖,如下圖所示。
如上24張圖就是我們的模闆。接下來,識别過程隻需要周遊模闆進行比對即可。
五、模闆比對
調用
get_image()
方法,得到驗證碼圖檔對象。然後,對驗證碼圖檔對象進行模闆比對,定義如下所示的方法:
from os import listdir
def detect_image(self, image):
"""
比對圖檔
:param image: 圖檔
:return: 拖動順序
"""
for template_name in listdir(TEMPLATES_FOLDER):
print('正在比對', template_name)
template = Image.open(TEMPLATES_FOLDER + template_name)
if self.same_image(image, template):
# 傳回順序
numbers = [int(number) for number in list(template_name.split('.')[0])]
print('拖動順序', numbers)
return numbers
TEMPLATES_FOLDER
就是模闆所在的檔案夾。這裡通過
listdir()
方法擷取所有模闆的檔案名稱,然後對其進行周遊,通過
same_image()
方法對驗證碼和模闆進行比對。如果比對成功,那麼就将比對到的模闆檔案名轉換為清單。如模闆檔案3124.png比對到了,則傳回結果為[3, 1, 2, 4]。
比對的方法實作如下所示:
def is_pixel_equal(self, image1, image2, x, y):
"""
判斷兩個像素是否相同
:param image1: 圖檔1
:param image2: 圖檔2
:param x: 位置x
:param y: 位置y
:return: 像素是否相同
"""
# 取兩個圖檔的像素點
pixel1 = image1.load()[x, y]
pixel2 = image2.load()[x, y]
threshold = 20
if abs(pixel1[0] - pixel2[0]) < threshold and abs(pixel1[1] - pixel2[1]) < threshold and abs(
pixel1[2] - pixel2[2]) < threshold:
return True
else:
return False
def same_image(self, image, template):
"""
識别相似驗證碼
:param image: 待識别驗證碼
:param template: 模闆
:return:
"""
# 相似度門檻值
threshold = 0.99
count = 0
for x in range(image.width):
for y in range(image.height):
# 判斷像素是否相同
if self.is_pixel_equal(image, template, x, y):
count += 1
result = float(count) / (image.width * image.height)
if result > threshold:
print('成功比對')
return True
return False
在這裡比對圖檔也利用了周遊像素的方法。
same_image()
方法接收兩個參數,
image
為待檢測的驗證碼圖檔對象,
template
是模闆對象。由于二者大小是完全一緻的,是以在這裡我們周遊了圖檔的所有像素點。比對二者同一位置的像素點,如果像素點相同,計數就加1。最後計算相同的像素點占總像素的比例。如果該比例超過一定門檻值,那就判定圖檔完全相同,則比對成功。這裡門檻值設定為0.99,即如果二者有0.99以上的相似比,則代表比對成功。
通過上面的方法,依次比對24個模闆。如果驗證碼圖檔正常,我們總能找到一個比對的模闆,這樣就可以得到宮格的滑動順序了。
六、模拟拖動
接下來,根據滑動順序拖動滑鼠,連接配接各個宮格,方法實作如下所示:
def move(self, numbers):
"""
根據順序拖動
:param numbers:
:return:
"""
# 獲得四個按點
circles = self.browser.find_elements_by_css_selector('.patt-wrap .patt-circ')
dx = dy = 0
for index in range(4):
circle = circles[numbers[index] - 1]
# 如果是第一次循環
if index == 0:
# 點選第一個按點
ActionChains(self.browser) \
.move_to_element_with_offset(circle, circle.size['width'] / 2, circle.size['height'] / 2) \
.click_and_hold().perform()
else:
# 小幅移動次數
times = 30
# 拖動
for i in range(times):
ActionChains(self.browser).move_by_offset(dx / times, dy / times).perform()
time.sleep(1 / times)
# 如果是最後一次循環
if index == 3:
# 松開滑鼠
ActionChains(self.browser).release().perform()
else:
# 計算下一次偏移
dx = circles[numbers[index + 1] - 1].location['x'] - circle.location['x']
dy = circles[numbers[index + 1] - 1].location['y'] - circle.location['y']
這裡方法接收的參數就是宮格的點按順序,如[3,1,2,4]。首先我們利用f
ind_elements_by_css_selector()
方法擷取到4個宮格元素,它是一個清單形式,每個元素代表一個宮格。接下來周遊宮格的點按順序,做一系列對應操作。
其中如果目前周遊的是第一個宮格,那就直接滑鼠點選并保持動作,否則移動到下一個宮格。如果目前周遊的是最後一個宮格,那就松開滑鼠,如果不是最後一個宮格,則計算移動到下一個宮格的偏移量。
通過4次循環,我們便可以成功操作浏覽器完成宮格驗證碼的拖拽填充,松開滑鼠之後即可識别成功。運作效果如下圖所示。
滑鼠會慢慢從起始位置移動到終止位置。最後一個宮格松開之後,驗證碼的識别便完成了。
至此,微網誌宮格驗證碼的識别就全部完成。驗證碼視窗會自動關閉。直接點選登入按鈕即可登入微網誌。
七、本節代碼
本節代碼位址為:https://github.com/Python3WebSpider/CrackWeiboSlide。
八、結語
本節介紹了一種常用的模闆比對識别圖檔的方式,模拟了滑鼠拖拽動作來實作驗證碼的識别。如果遇到類似的驗證碼,我們可以采用同樣的思路進行識别。
原文釋出時間為:2018-06-25
本文作者:崔慶才
本文來自雲栖社群官方釘群“
Python技術進階”,了解相關資訊可以關注“
”。
Python技術進階交流群