谷歌驗證碼 ReCAPTCHA 的模擬點選破解方案來了!

語言: CN / TW / HK

這是「進擊的Coder」的第  631  篇技術分享

作者:崔慶才

大家好,我是崔慶才。

之前的時候我分享過 ReCAPTCHA 的破解方案,那種方案是獲取到 ReCAPTCHA 其中的一個 SiteKey,然後將 SiteKey 直接提交給 ReCAPTCHA 相關的破解服務來實現破解,文章見: 我又找到了一個破解谷歌驗證碼的新方案!

這次,我們再來介紹一種更靈活更強大的全模擬點選破解方案,整體思路就是將全部的驗證碼圖片進行識別,並根據識別結果對 ReCAPTCHA 驗證碼進行模擬點選,從而最終通過驗證碼。

ReCAPTCHA 介紹

在開始之前,我這裡先簡單提下什麼是 ReCAPTCHA,可能大家見的不多,因為這個驗證碼在國內並沒有那麼普及。

驗證碼是類似這樣子的:

我們這時候需要點選驗證碼上的小框來觸發驗證,通常情況下,驗證碼會呈現如下的點選圖:

比如上面這張圖,驗證碼頁面會出現九張圖片,同時最上方出現文字「樹木」,我們需要點選下方九張圖中出現「樹木」的圖片,點選完成之後,可能還會出現幾張新的圖片,我們需要再次完成點選,最後點選「驗證」按鈕即可完成驗證。

ReCAPTCHA 也有體驗地址,大家可以開啟 https://www.google.com/recaptcha/api2/demo 檢視,開啟之後,我們可以發現有如上圖所示的內容,然後點選圖片進行識別即可。

整體識別思路

其實我們看,這種驗證碼其實主要就是一些格子的點選,我們只要把一些相應的位置點選對了,最後就能驗證通過了。

經過觀察我們發現,其實主要是 3x3 和 4x4  方格的驗證碼,比如 3x3 的就是這樣的:

4x4 的就是這樣的:

然後驗證碼上面還有一行加粗的文字,這就是我們要點選的目標。

所以,關鍵點就來了:

  • 第一就是把上面的文字內容找出來,以便於我們知道要點選的內容是什麼。

  • 第二就是我們要知道哪些目標圖片和上面的文字是匹配的,找到了依次模擬點選就好了。

聽起來似乎很簡單的對吧,但第二點是一個難點,我們咋知道哪些圖片和文字匹配的呢?這就難搞了。

其實,這個靠深度學習是能做到的,但要搞出這麼一個模型是很不容易的,我們需要大量的資料來訓練,需要收集很多驗證碼圖片和標註結果,這總的工作量是非常大的。

那怎麼辦呢?這裡給大家介紹一個服務網站 YesCaptcha,這個服務網站已經給我們做好了識別服務,我們只需要把驗證碼的大圖提交上去,然後同時告訴服務需要識別的內容是什麼,這個服務就可以返回對應識別結果了。

下面我們來藉助 YesCaptcha 來試試識別過程。

YesCaptcha

在使用之前我們需要先註冊下這個網站,網站地址是 https://yescaptcha.com/i/CnZPBu ,註冊個賬號之後大家可以在後臺獲取一個賬戶金鑰,也就是 ClientKey,儲存備用。

OK,然後我們可以檢視下這裡的官方文件: https://yescaptcha.atlassian.net/wiki/spaces/YESCAPTCHA/pages/18055169/ReCaptchaV2Classification+reCaptcha+V2 ,這裡介紹介紹了一個 API,大致內容是這樣的。

首先有一個建立任務的 API,API 地址為 https://api.yescaptcha.com/createTask ,然後看下請求引數:

這裡我們需要傳入這麼幾個引數:

  • type:內容就是 RecaptchaV2Classification

  • image:是驗證碼對應的 Base64 編碼

  • question:對應的問題 ID,也就是識別目標的代號。

比如這裡我們可以 POST 這樣的一個內容給伺服器,結構如下:

{
"clientKey": "cc9c18d3e263515c2c072b36a7125eecc078618f",
"task": {
"type": "ReCaptchaV2Classification",
"image": "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDc....",
"question": "/m/0k4j"
}
}

其中這裡 image 就可以是一個 3x3 或者 4x4 的驗證碼截圖對應的 Base64 編碼的字串。

然後伺服器就會返回類似這樣的響應:

{
"errorId": 0,
"errorCode": "",
"errorDescription": "null",
"status": "ready",
"taskId": "3a9e8cb8-3871-11ec-9794-94e6f7355a0b",
"solution": {
"objects": [1,5,8], // 影象需要點選的位置
"type": "multi"
}
}

OK,我們可以看到,返回結果的 solution 欄位中的 objects 欄位就包含了一些代號,比如這裡是 1, 5, 8 ,什麼意思呢?這個就是對應的目標點選代號。

對於 3x3 的圖片來說,對應的代號就是這樣的:

對於 4x4 的圖片來說,對應的代號就是這樣的:

OK,知道了代號之後,模擬點選就好辦多了吧,我們用一些模擬點選操作就可以完成了。

程式碼基礎實現

行,那有了基本思路之後,那我們就開始用 Python 實現下整個流程吧,這裡我們就拿 https://www.google.com/recaptcha/api2/demo 這個網站作為樣例來講解下整個識別和模擬點選過程。

識別封裝

首先我們對上面的任務 API 實現一下封裝,來先寫一個類:

from loguru import logger
from app.settings import CAPTCHA_RESOLVER_API_KEY, CAPTCHA_RESOLVER_API_URL
import requests

class CaptchaResolver(object):

def __init__(self, api_url=CAPTCHA_RESOLVER_API_URL, api_key=CAPTCHA_RESOLVER_API_KEY):
self.api_url = api_url
self.api_key = api_key

def create_task(self, image_base64_string, question_id):
logger.debug(f'start to recognize image for question {question_id}')
data = {
"clientKey": self.api_key,
"task": {
"type": "ReCaptchaV2Classification",
"image": image_base64_string,
"question": question_id
}
}
try:
response = requests.post(self.api_url, json=data)
result = response.json()
logger.debug(f'captcha recogize result {result}')
return result
except requests.RequestException:
logger.exception(
'error occurred while recognizing captcha', exc_info=True)

OK,這裡我們就先定義了一個類 CaptchaResolver,然後主要接收兩個引數,一個就是 api_url ,這個對應的就是 https://api.yescaptcha.com/createTask 這個 API 地址,然後還有一個引數是 api_key ,這個就是前文介紹的那個 ClientKey。

接著我們定義了一個 create_task 方法,接收兩個引數,第一個引數 image_base64_string 就是驗證碼圖片對應的 Base64 編碼,第二個引數 question_id 就是要識別的目標是什麼,這裡就是將整個請求用 requests 模擬實現了,最後返回對應的 JSON 內容的響應結果就好了。

基礎框架

OK,那麼接下來我們來用 Selenium 來模擬開啟這個例項網站,然後模擬點選來觸發驗證碼,接著識別驗證碼就好了。

首先寫一個大致框架:

import time
from selenium import webdriver
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.common.action_chains import ActionChains
from app.captcha_resolver import CaptchaResolver


class Solution(object):
def __init__(self, url):
self.browser = webdriver.Chrome()
self.browser.get(url)
self.wait = WebDriverWait(self.browser, 10)
self.captcha_resolver = CaptchaResolver()

def __del__(self):
time.sleep(10)
self.browser.close()

這裡我們先在構造方法裡面初始化了一個 Chrome 瀏覽器操作物件,然後呼叫對應的 get 方法開啟例項網站,接著聲明瞭一個 WebDriverWait 物件和 CaptchaResolver 物件,以分別應對節點查詢和驗證碼識別操作,留作備用。

iframe 切換支援

接著,下一步我們就該來模擬點選驗證碼的入口,來觸發驗證碼了對吧。

通過觀察我們發現這個驗證碼入口其實是在 iframe 裡面載入的,對應的 iframe 是這樣的:

另外彈出的驗證碼圖片又在另外一個 iframe 裡面,如圖所示:

Selenium 查詢節點是需要切換到對應的 iframe 裡面才行的,不然是沒法查到對應的節點,也就沒法模擬點選什麼的了。

所以這裡我們定義幾個工具方法,分別能夠支援切換到入口對應的 iframe 和驗證碼本身對應的 iframe,程式碼如下:

    def get_captcha_entry_iframe(self) -> WebElement:
self.browser.switch_to.default_content()
captcha_entry_iframe = self.browser.find_element_by_css_selector(
'iframe[title="reCAPTCHA"]')
return captcha_entry_iframe

def switch_to_captcha_entry_iframe(self) -> None:
captcha_entry_iframe: WebElement = self.get_captcha_entry_iframe()
self.browser.switch_to.frame(captcha_entry_iframe)

def get_captcha_content_iframe(self) -> WebElement:
self.browser.switch_to.default_content()
captcha_content_iframe = self.browser.find_element_by_xpath(
'//iframe[contains(@title, "recaptcha challenge")]')
return captcha_content_iframe

def switch_to_captcha_content_iframe(self) -> None:
captcha_content_iframe: WebElement = self.get_captcha_content_iframe()
self.browser.switch_to.frame(captcha_content_iframe)

這樣的話,我們只需要呼叫 switch_to_captcha_content_iframe 就能查詢驗證碼圖片裡面的內容,呼叫 switch_to_captcha_entry_iframe 就能查詢驗證碼入口裡面的內容。

觸發驗證碼

OK,那麼接下來的一步就是來模擬點選驗證碼的入口,然後把驗證碼觸發出來了對吧,就是模擬點選這裡:

實現很簡單,程式碼如下:

    def trigger_captcha(self) -> None:
self.switch_to_captcha_entry_iframe()
captcha_entry = self.wait.until(EC.presence_of_element_located(
(By.ID, 'recaptcha-anchor')))
captcha_entry.click()
time.sleep(2)
self.switch_to_captcha_content_iframe()
entire_captcha_element: WebElement = self.get_entire_captcha_element()
if entire_captcha_element.is_displayed:
logger.debug('trigged captcha successfully')

這裡首先我們首先呼叫 switch_to_captcha_entry_iframe 進行了 iframe 的切換,然後找到那個入口框對應的節點,然後點選一下。

點選完了之後我們再呼叫 switch_to_captcha_content_iframe 切換到驗證碼本身對應的 iframe 裡面,查詢驗證碼本身對應的節點是否加載出來了,如果加載出來了,那麼就證明觸發成功了。

找出識別目標

OK,那麼現在驗證碼可能就長這樣子了:

那接下來我們要做的就是兩件事了,一件事就是把匹配目標找出來,就是上圖中的加粗字型,第二件事就是把驗證碼的圖片儲存下來,然後轉成 Base64 編碼,提交給 CaptchaResolver 來識別。

好,那麼怎麼查詢匹配目標呢?也就是上圖中的 traffice lights,用 Selenium 常規的節點搜尋就好了:

    def get_captcha_target_name(self) -> WebElement:
captcha_target_name_element: WebElement = self.wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, '.rc-imageselect-desc-wrapper strong')))
return captcha_target_name_element.text

通過呼叫這個方法,我們就能得到上圖中類似 traffic lights 的內容了。

驗證碼識別

接著,我們對驗證碼圖片進行下載,然後轉 Base64 進行識別吧,整體程式碼如下:

def verify_entire_captcha(self):
self.entire_captcha_natural_width = self.get_entire_captcha_natural_width()
logger.debug(
f'entire_captcha_natural_width {self.entire_captcha_natural_width}'
)
self.captcha_target_name = self.get_captcha_target_name()
logger.debug(
f'captcha_target_name {self.captcha_target_name}'
)
entire_captcha_element: WebElement = self.get_entire_captcha_element()
entire_captcha_url = entire_captcha_element.find_element_by_css_selector(
'td img').get_attribute('src')
logger.debug(f'entire_captcha_url {entire_captcha_url}')
with open(CAPTCHA_ENTIRE_IMAGE_FILE_PATH, 'wb') as f:
f.write(requests.get(entire_captcha_url).content)
logger.debug(
f'saved entire captcha to {CAPTCHA_ENTIRE_IMAGE_FILE_PATH}')
resized_entire_captcha_base64_string = resize_base64_image(
CAPTCHA_ENTIRE_IMAGE_FILE_PATH, (self.entire_captcha_natural_width,
self.entire_captcha_natural_width))
logger.debug(
f'resized_entire_captcha_base64_string, {resized_entire_captcha_base64_string[0:100]}...')
entire_captcha_recognize_result = self.captcha_resolver.create_task(
resized_entire_captcha_base64_string,
get_question_id_by_target_name(self.captcha_target_name)
)

這裡我們首先獲取了一些驗證碼的基本資訊:

  • entire_captcha_natural_width:驗證碼圖片對應的圖片真實大小,這裡如果是 3x3 的驗證碼圖片,那麼圖片的真實大小就是 300,如果是 4x4 的驗證碼圖片,那麼圖片的真實大小是 450

  • captcha_target_name:識別目標名稱,就是剛才獲取到的內容

  • entire_captcha_element:驗證碼圖片對應的節點物件。

這裡我們先把 entire_captcha_element 裡面的 img 節點拿到,然後將 img 的 src 內容獲取下來,賦值為 entire_captcha_url,這樣其實就得到了一張完整的驗證碼大圖,然後我們將其寫入到檔案中。

結果就類似這樣的:

接著我們把這個圖片發給 YesCaptcha 進行識別就好了。

Base64 編碼

接著,我們把這張圖片轉下 Base64 編碼,定義這樣一個方法:

def resize_base64_image(filename, size):
width, height = size
img = Image.open(filename)
new_img = img.resize((width, height))
new_img.save(CAPTCHA_RESIZED_IMAGE_FILE_PATH)
with open(CAPTCHA_RESIZED_IMAGE_FILE_PATH, "rb") as f:
data = f.read()
encoded_string = base64.b64encode(data)
return encoded_string.decode('utf-8')

這裡值得注意的是,由於 API 對圖片大小有限制,如果是 3x3 的圖片,那麼我們需要將圖片調整成 300x300 才可以,如果是 4x4 的圖片,那麼我們需要將圖片調整成 450x450,所以這裡我們先呼叫了 Image 的 resize 方法調整了大小,接著再轉成了 Base64 編碼。

問題 ID 處理

那問題 ID 怎麼處理呢?通過 API 文件 https://yescaptcha.atlassian.net/wiki/spaces/YESCAPTCHA/pages/18055169 我們可以看到如下對映表:

所以,比如假如驗證碼裡面我們得到的是 traffic lights,那麼問題 ID 就是 /m/015qff ,行,那我們反向查詢就好了,定義這麼個方法:

CAPTCHA_TARGET_NAME_QUESTION_ID_MAPPING = {
"taxis": "/m/0pg52",
"bus": "/m/01bjv",
"school bus": "/m/02yvhj",
"motorcycles": "/m/04_sv",
"tractors": "/m/013xlm",
"chimneys": "/m/01jk_4",
"crosswalks": "/m/014xcs",
"traffic lights": "/m/015qff",
"bicycles": "/m/0199g",
"parking meters": "/m/015qbp",
"cars": "/m/0k4j",
"vehicles": "/m/0k4j",
"bridges": "/m/015kr",
"boats": "/m/019jd",
"palm trees": "/m/0cdl1",
"mountains or hills": "/m/09d_r",
"fire hydrant": "/m/01pns0",
"fire hydrants": "/m/01pns0",
"a fire hydrant": "/m/01pns0",
"stairs": "/m/01lynh",
}


def get_question_id_by_target_name(target_name):
logger.debug(f'try to get question id by {target_name}')
question_id = CAPTCHA_TARGET_NAME_QUESTION_ID_MAPPING.get(target_name)
logger.debug(f'question_id {question_id}')
return question_id

這樣傳入名稱,我們就可以得到問題 ID 了。

最後將上面的引數直接呼叫 CaptchaResovler 物件的 create_task 方法就能得到識別結果了。

模擬點選

得到結果之後,我們知道返回結果的 objects 就是需要點選的驗證碼格子的列表,下面進行模擬點選即可:

  single_captcha_elements = self.wait.until(EC.visibility_of_all_elements_located(
(By.CSS_SELECTOR, '#rc-imageselect-target table td')))
for recognized_index in recognized_indices:
single_captcha_element: WebElement = single_captcha_elements[recognized_index]
single_captcha_element.click()
# check if need verify single captcha
self.verify_single_captcha(recognized_index)

這裡我們首先得到了 recognized_indices 就是識別結果對應的標號,然後逐個遍歷進行模擬點選。

對於每次點選,我們可以直接獲取所有的驗證碼格子對應的節點,然後呼叫其 click 方法就可以完成點選了,其中格子的標號和返回結果的對應關係如圖:

當然我們也可以通過執行 JavaScript 來對每個節點進行模擬點選,效果是類似的。

這樣我們就可以實現驗證碼小圖的逐個識別了。

小圖識別

等等,在識別過程中還發現了一個坑,那就是有時候我們點選完一個小格子之後,這個小格子就消失了!然後在原來的小格子的位置出現了一個新的小圖,我們需要對新出現的圖片進行二次識別才可以。

這個怎麼處理呢?

我們其實可以在每點選完一個格子之後就來校驗下當前小格子有沒有圖片重新整理,如果有圖片重新整理,那麼對應的 HTML 的 class 就會變化,如果不變,class 就會包含 selected 字樣。根據這個資訊,如果圖片重新整理了,然後我們再繼續對小格子對應的圖進行二次識別就好了。

這裡我們再定義一個方法:

def verify_single_captcha(self, index):
time.sleep(3)
elements = self.wait.until(EC.visibility_of_all_elements_located(
(By.CSS_SELECTOR, '#rc-imageselect-target table td')))
single_captcha_element: WebElement = elements[index]
class_name = single_captcha_element.get_attribute('class')
logger.debug(f'verifiying single captcha {index}, class {class_name}')
if 'selected' in class_name:
logger.debug(f'no new single captcha displayed')
return
logger.debug('new single captcha displayed')
single_captcha_url = single_captcha_element.find_element_by_css_selector(
'img').get_attribute('src')
logger.debug(f'single_captcha_url {single_captcha_url}')
with open(CAPTCHA_SINGLE_IMAGE_FILE_PATH, 'wb') as f:
f.write(requests.get(single_captcha_url).content)
resized_single_captcha_base64_string = resize_base64_image(
CAPTCHA_SINGLE_IMAGE_FILE_PATH, (100, 100))
single_captcha_recognize_result = self.captcha_resolver.create_task(
resized_single_captcha_base64_string, get_question_id_by_target_name(self.captcha_target_name))
if not single_captcha_recognize_result:
logger.error('count not get single captcha recognize result')
return
has_object = single_captcha_recognize_result.get(
'solution', {}).get('hasObject')
if has_object is None:
logger.error('count not get captcha recognized indices')
return
if has_object is False:
logger.debug('no more object in this single captcha')
return
if has_object:
single_captcha_element.click()
# check for new single captcha
self.verify_single_captcha(index)

OK,這裡我們定義了一個 verify_single_captcha 方法,然後傳入了格子對應的序號。接著我們首先嚐試查詢格子對應的節點,然後找出對應的 HTML 的 class 屬性。如果沒有出現新的小圖,那就是這樣的選中狀態,對應的 class 就包含了 selected 字樣,如圖所示:

對於這樣的圖片,我們就不需要進行二次驗證,否則就需要對這個格子進行截圖和二次識別。

二次識別的步驟也是一樣的,我們需要將小格子對應的圖片單獨獲取其 url,然後下載下來,接著調整大小並轉化成 Base64 編碼,然後發給 API,API 會通過一個 hasObject 欄位告訴我們這個小圖裡面是否包含我們想要識別的目標內容,如果是,那就接著點選,然後遞迴進行下一次檢查,如果不是,那就跳過。

點選驗證

好,那麼有了上面的邏輯,我們就能完成整個 ReCAPTCHA 的識別和點選了。

最後,我們模擬點選驗證按鈕就好了:

def get_verify_button(self) -> WebElement:
verify_button = self.wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, '#recaptcha-verify-button')))
return verify_button

# after all captcha clicked
verify_button: WebElement = self.get_verify_button()
if verify_button.is_displayed:
verify_button.click()
time.sleep(3)

校驗結果

點選完了之後,我們可以嘗試檢查網頁變化,看看有沒有驗證成功。

比如驗證成功的標誌就是出現一個綠色小對勾:

檢查方法如下:

def get_is_successful(self):
self.switch_to_captcha_entry_iframe()
anchor: WebElement = self.wait.until(EC.visibility_of_element_located((
By.ID, 'recaptcha-anchor'
)))
checked = anchor.get_attribute('aria-checked')
logger.debug(f'checked {checked}')
return str(checked) == 'true'

這裡我們先切換了 iframe,然後檢查了對應的 class 是否是符合期望的。

最後如果 get_is_successful 返回結果是 True,那就代表識別成功了,那就整個完成了。

如果返回結果是 False,我們可以進一步遞迴呼叫上述邏輯進行二次識別,直到識別成功即可。

執行結果

最後看看執行效果吧:

看起來整個執行過程還是比較穩定的。

程式碼

以上程式碼可能比較複雜,這裡我將程式碼進行了規整,然後放到 GitHub 上了,大家如有需要可以自取: https://github.com/Python3WebSpider/RecaptchaResolver

註冊地址

最後需要說明一點,上面的驗證碼服務是收費的,每驗證一次可能花一定的點數,比如識別一次 3x3 的圖要花 10 點數,而充值一塊錢就能獲得 1000 點數,所以識別一次就一分錢,還是比較便宜的。

我這裡充值了好幾萬點數,然後我就變成了 VIP5級的賬號。我研究了下發現大家如果用我的邀請連結 https://yescaptcha.com/i/CnZPBu 註冊大家可以直接變成 VIP4,然後 VIP4 可以獲取首充贈送 10% 的優惠,還不錯哈~

希望本文對大家有幫助。

End

崔慶才的新書 《Python3網路爬蟲開發實戰(第二版)》 已經正式上市了!書中詳細介紹了零基礎用 Python 開發爬蟲的各方面知識,同時相比第一版新增了 JavaScript 逆向、Android 逆向、非同步爬蟲、深度學習、Kubernetes 相關內容,‍同時本書已經獲得 Python 之父 Guido 的推薦,目前本書正在七折促銷中!

內容介紹: 《Python3網路爬蟲開發實戰(第二版)》內容介紹

掃碼購買

好文和朋友一起看~