谷歌驗證碼ReCAPTCHA 的模擬點擊破解方案來了!
我們再來介紹一種更靈活更強大的全模擬點擊破解方案,整體思路就是將全部的驗證碼圖片進行識別,并根據(jù)識別結(jié)果對 ReCAPTCHA 驗證碼進行模擬點擊,從而最終通過驗證碼。
ReCAPTCHA 介紹
在開始之前,我這里先簡單提下什么是 ReCAPTCHA,可能大家見的不多,因為這個驗證碼在國內(nèi)并沒有那么普及。
驗證碼是類似這樣子的:
我們這時候需要點擊驗證碼上的小框來觸發(fā)驗證,通常情況下,驗證碼會呈現(xiàn)如下的點選圖:
比如上面這張圖,驗證碼頁面會出現(xiàn)九張圖片,同時最上方出現(xiàn)文字「樹木」,我們需要點選下方九張圖中出現(xiàn)「樹木」的圖片,點選完成之后,可能還會出現(xiàn)幾張新的圖片,我們需要再次完成點選,最后點擊「驗證」按鈕即可完成驗證。
ReCAPTCHA 也有體驗地址,大家可以打開 https://www.google.com/recaptcha/api2/demo 查看,打開之后,我們可以發(fā)現(xiàn)有如上圖所示的內(nèi)容,然后點選圖片進行識別即可。
整體識別思路
其實我們看,這種驗證碼其實主要就是一些格子的點選,我們只要把一些相應的位置點擊對了,最后就能驗證通過了。
經(jīng)過觀察我們發(fā)現(xiàn),其實主要是 3x3 和 4x4 方格的驗證碼,比如 3x3 的就是這樣的:
4x4 的就是這樣的:
然后驗證碼上面還有一行加粗的文字,這就是我們要點選的目標。
所以,關鍵點就來了:
- 第一就是把上面的文字內(nèi)容找出來,以便于我們知道要點擊的內(nèi)容是什么。
- 第二就是我們要知道哪些目標圖片和上面的文字是匹配的,找到了依次模擬點擊就好了。
聽起來似乎很簡單的對吧,但第二點是一個難點,我們咋知道哪些圖片和文字匹配的呢?這就難搞了。
其實,這個靠深度學習是能做到的,但要搞出這么一個模型是很不容易的,我們需要大量的數(shù)據(jù)來訓練,需要收集很多驗證碼圖片和標注結(jié)果,這總的工作量是非常大的。
那怎么辦呢?這里給大家介紹一個服務網(wǎng)站 YesCaptcha,這個服務網(wǎng)站已經(jīng)給我們做好了識別服務,我們只需要把驗證碼的大圖提交上去,然后同時告訴服務需要識別的內(nèi)容是什么,這個服務就可以返回對應識別結(jié)果了。
下面我們來借助 YesCaptcha 來試試識別過程。
YesCaptcha
在使用之前我們需要先注冊下這個網(wǎng)站,網(wǎng)站地址是 https://yescaptcha.com/i/woNWBQ
,注冊個賬號之后大家可以在后臺獲取一個賬戶密鑰,也就是 ClientKey,保存?zhèn)溆谩?/p>
OK,然后我們可以查看下這里的官方文檔:https://yescaptcha.atlassian.net/wiki/spaces/YESCAPTCHA/pages/18055169/ReCaptchaV2Classification+reCaptcha+V2,這里介紹介紹了一個 API,大致內(nèi)容是這樣的。
首先有一個創(chuàng)建任務的 API,API 地址為 https://api.yescaptcha.com/createTask,然后看下請求參數(shù):
這里我們需要傳入這么幾個參數(shù):
- type:內(nèi)容就是 ReCaptchaV2Classification
- image:是驗證碼對應的 Base64 編碼
- question:對應的問題 ID,也就是識別目標的代號。
比如這里我們可以 POST 這樣的一個內(nèi)容給服務器,結(jié)構(gòu)如下:
{
"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,我們可以看到,返回結(jié)果的 solution 字段中的 objects 字段就包含了一些代號,比如這里是 1, 5, 8
,什么意思呢?這個就是對應的目標點擊代號。
對于 3x3 的圖片來說,對應的代號就是這樣的:
對于 4x4 的圖片來說,對應的代號就是這樣的:
OK,知道了代號之后,模擬點擊就好辦多了吧,我們用一些模擬點擊操作就可以完成了。
代碼基礎實現(xiàn)
行,那有了基本思路之后,那我們就開始用 Python 實現(xiàn)下整個流程吧,這里我們就拿 https://www.google.com/recaptcha/api2/demo 這個網(wǎng)站作為樣例來講解下整個識別和模擬點擊過程。
識別封裝
首先我們對上面的任務 API 實現(xiàn)一下封裝,來先寫一個類:
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,然后主要接收兩個參數(shù),一個就是 api_url
,這個對應的就是 https://api.yescaptcha.com/createTask 這個 API 地址,然后還有一個參數(shù)是 api_key
,這個就是前文介紹的那個 ClientKey。
接著我們定義了一個 create_task 方法,接收兩個參數(shù),第一個參數(shù) image_base64_string
就是驗證碼圖片對應的 Base64 編碼,第二個參數(shù) question_id
就是要識別的目標是什么,這里就是將整個請求用 requests 模擬實現(xiàn)了,最后返回對應的 JSON 內(nèi)容的響應結(jié)果就好了。
基礎框架
OK,那么接下來我們來用 Selenium 來模擬打開這個實例網(wǎng)站,然后模擬點選來觸發(fā)驗證碼,接著識別驗證碼就好了。
首先寫一個大致框架:
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()
這里我們先在構(gòu)造方法里面初始化了一個 Chrome 瀏覽器操作對象,然后調(diào)用對應的 get 方法打開實例網(wǎng)站,接著聲明了一個 WebDriverWait 對象和 CaptchaResolver 對象,以分別應對節(jié)點查找和驗證碼識別操作,留作備用。
iframe 切換支持
接著,下一步我們就該來模擬點擊驗證碼的入口,來觸發(fā)驗證碼了對吧。
通過觀察我們發(fā)現(xiàn)這個驗證碼入口其實是在 iframe 里面加載的,對應的 iframe 是這樣的:
另外彈出的驗證碼圖片又在另外一個 iframe 里面,如圖所示:
Selenium 查找節(jié)點是需要切換到對應的 iframe 里面才行的,不然是沒法查到對應的節(jié)點,也就沒法模擬點擊什么的了。
所以這里我們定義幾個工具方法,分別能夠支持切換到入口對應的 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)
這樣的話,我們只需要調(diào)用 switch_to_captcha_content_iframe 就能查找驗證碼圖片里面的內(nèi)容,調(diào)用 switch_to_captcha_entry_iframe 就能查找驗證碼入口里面的內(nèi)容。
觸發(fā)驗證碼
OK,那么接下來的一步就是來模擬點擊驗證碼的入口,然后把驗證碼觸發(fā)出來了對吧,就是模擬點擊這里:
實現(xiàn)很簡單,代碼如下:
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')
這里首先我們首先調(diào)用 switch_to_captcha_entry_iframe 進行了 iframe 的切換,然后找到那個入口框?qū)墓?jié)點,然后點擊一下。
點擊完了之后我們再調(diào)用 switch_to_captcha_content_iframe 切換到驗證碼本身對應的 iframe 里面,查找驗證碼本身對應的節(jié)點是否加載出來了,如果加載出來了,那么就證明觸發(fā)成功了。
找出識別目標
OK,那么現(xiàn)在驗證碼可能就長這樣子了:
那接下來我們要做的就是兩件事了,一件事就是把匹配目標找出來,就是上圖中的加粗字體,第二件事就是把驗證碼進行保存,然后轉(zhuǎn)成 Base64 編碼,提交給 CaptchaResolver 來識別。
好,那么怎么查找匹配目標呢?也就是上圖中的 traffice lights,用 Selenium 常規(guī)的節(jié)點搜索就好了:
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
通過調(diào)用這個方法,我們就能得到上圖中類似 traffic lights 的內(nèi)容了。
驗證碼識別
接著,我們對驗證碼圖片進行下載,然后轉(zhuǎn) 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:識別目標名稱,就是剛才獲取到的內(nèi)容
- entire_captcha_element:驗證碼圖片對應的節(jié)點對象。
這里我們先把 entire_captcha_element 里面的 img 節(jié)點拿到,然后將 img 的 src 內(nèi)容獲取下來,賦值為 entire_captcha_url,這樣其實就得到了一張完整的驗證碼大圖,然后我們將其寫入到文件中。
結(jié)果就類似這樣的:
接著我們把這個圖片發(fā)給 YesCaptcha 進行識別就好了。
Base64 編碼
接著,我們把這張圖片轉(zhuǎn)下 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 的圖片,那么我們需要將圖片調(diào)整成 300x300 才可以,如果是 4x4 的圖片,那么我們需要將圖片調(diào)整成 450x450,所以這里我們先調(diào)用了 Image 的 resize 方法調(diào)整了大小,接著再轉(zhuǎn)成了 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 了。
最后將上面的參數(shù)直接調(diào)用 CaptchaResovler 對象的 create_task 方法就能得到識別結(jié)果了。
模擬點擊
得到結(jié)果之后,我們知道返回結(jié)果的 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 就是識別結(jié)果對應的標號,然后逐個遍歷進行模擬點擊。
對于每次點擊,我們可以直接獲取所有的驗證碼格子對應的節(jié)點,然后調(diào)用其 click 方法就可以完成點擊了,其中格子的標號和返回結(jié)果的對應關系如圖:
當然我們也可以通過執(zhí)行 JavaScript 來對每個節(jié)點進行模擬點擊,效果是類似的。
這樣我們就可以實現(xiàn)驗證碼小圖的逐個識別了。
小圖識別
等等,在識別過程中還發(fā)現(xiàn)了一個坑,那就是有時候我們點擊完一個小格子之后,這個小格子就消失了!然后在原來的小格子的位置出現(xiàn)了一個新的小圖,我們需要對新出現(xiàn)的圖片進行二次識別才可以。
這個怎么處理呢?
我們其實可以在每點擊完一個格子之后就來校驗下當前小格子有沒有圖片刷新,如果有圖片刷新,那么對應的 HTML 的 class 就會變化,否則就會包含 selected 字樣,然后我們再繼續(xù)對小格子對應的圖進行二次識別就好了。
這里我們再定義一個方法:
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 方法,然后傳入了格子對應的序號。接著我們首先嘗試查找格子對應的節(jié)點,然后找出對應的 HTML 的 class 屬性。如果沒有出現(xiàn)新的小圖,那就是這樣的選中狀態(tài),對應的 class 就包含了 selected 字樣,如圖所示:
對于這樣的圖片,我們就不需要進行二次驗證,否則就需要對這個格子進行截圖和二次識別。
二次識別的步驟也是一樣的,我們需要將小格子對應的圖片單獨獲取其 url,然后下載下來,接著調(diào)整大小并轉(zhuǎn)化成 Base64 編碼,然后發(fā)給 API,API 會通過一個 hasObject 字段告訴我們這個小圖里面是否包含我們想要識別的目標內(nèi)容,如果是,那就接著點擊,然后遞歸進行下一次檢查,如果不是,那就跳過。
點擊驗證
好,那么有了上面的邏輯,我們就能完成整個 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)
校驗結(jié)果
點擊完了之后,我們可以嘗試檢查網(wǎng)頁變化,看看有沒有驗證成功。
比如驗證成功的標志就是出現(xiàn)一個綠色小對勾:
檢查方法如下:
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 返回結(jié)果是 True,那就代表識別成功了,那就整個完成了。
如果返回結(jié)果是 False,我們可以進一步遞歸調(diào)用上述邏輯進行二次識別,直到識別成功即可。