前面的文章“谷歌驗證碼ReCAPTCHA 的模擬點擊破解方案來了!”我們介紹過 ReCaptcha 的模擬點擊破解教程,但除了 ReCaptcha,還有另外和 ReCapacha 驗證流程很相似的驗證碼,叫做 HCaptcha。
ReCaptcha 是谷歌家的,因爲某些原因,咱們國內是無法使用 ReCaptcha 的,所以有時候 HCaptcha 也成了一些國際性網站的比較好的選擇。
那今天我們就來了解下 HCaptcha 和它的模擬點擊破解流程。
HCaptcha
我們首先看看 HCaptcha 的驗證交互流程,其 Demo 網站爲 https://democaptcha.com/demo-form-eng/hcaptcha.html,打開之後,我們可以看到如下的驗證碼入口頁面:
看起來入口和 ReCaptcha 很相似的對吧,其實驗證流程也是很類似的。
當我們點擊複選框時,驗證碼會先通過其風險分析引擎判斷當前用戶的風險,如果是低風險用戶,便可以直接通過,反之,驗證碼會彈出對話框,讓我們回答對話框中的問題,類似如下:
這時候我們看到 HCaptcha驗證碼會給我們一個問題,比如上圖的問題是「請點擊每張包含飛機的圖片」,我們需要從下面的九張圖中選擇出含有飛機的圖片,如果九張圖片中,沒有飛機,則點擊「跳過 / Skip」按鈕,如果有,則將所有帶有飛機的圖片都選擇上,跳過按鈕會變成「檢查 / Verify」按鈕,驗證通過之後我們就可以看到如下的驗證成功的效果了:
是不是整體流程和 ReCaptcha 還是還是非常相近的?
但其實這個比 ReCaptcha 簡單一些,它的驗證碼圖片每次一定是 3x3 的,沒有 4x4 的,而且點擊一個圖之後不會再出現一個新的小圖讓我們二次選擇,所以其破解思路也相對簡單一些。
如何破解
整個流程其實我們稍微梳理下,就知道整體的的破解思路了,有這麼兩個關鍵點:
- 第一就是把上面的文字內容找出來,以便於我們知道要點擊的內容是什麼。
- 第二就是我們要知道哪些目標圖片和上面的文字是匹配的,找到了依次模擬點擊就好了。
聽起來似乎很簡單的對吧,但第二點是一個難點,我們咋知道哪些圖片和文字匹配的呢?這就是一個難題。
前面 ReCaptcha 的破解過程我們瞭解過了使用 YesCaptcha 來進行圖片的識別,除了 ReCaptcha,YesCaptcha 其實也支持 HCaptcha 的驗證碼識別,利用 YesCaptcha 我們也能輕鬆知道哪些圖片和輸入內容是匹配的。
下面讓們來試試看。
YesCaptcha
在使用之前我們需要先註冊下這個網站,網站地址是 https://yescaptcha.com/i/CnZPBu ,註冊個賬號之後大家可以在後臺獲取一個賬戶密鑰,也就是 ClientKey,保存備用。
OK,然後我們可以查看下這裏的官方文檔:https://yescaptcha.atlassian.net/wiki/spaces/YESCAPTCHA/pages/24543233/HCaptchaClassification+Hcaptcha,這裏介紹介紹了一個 API,大致內容是這樣的。
首先有一個創建任務的 API,API 地址爲 https://api.yescaptcha.com/createTask,然後看下請求參數:
這裏我們需要傳入這麼幾個參數:
- type:內容就是
- queries:是驗證碼對應的 Base64 編碼,這裏直接轉成一個列表就可以
- question:對應的問題 ID,也就是識別目標的代號,這裏其實就是問題整句的內容
- corrdinate:一個返回結果的控制開關,默認會返回每張圖片識別的 true / false 結果,也就是第 x 張圖片是否和圖片匹配,如果加上該參數,那麼 API 就會返回對應匹配圖片的索引。
比如這裏我們可以 POST 這樣的一個內容給服務器,結構如下:
{
"clientKey": "cc9c18d3e263515c2c072b36a7125eecc078618f",
"task": {
"type": "HCaptchaClassification",
"queries": [
"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8Uw...",
"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8Uw...",
...
"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8Uw...",
],
"question": "請單擊每個包含卡車的圖像。" // 直接上傳問題整句
}
}
然後服務器就會返回類似這樣的響應:
{
"errorId": 0,
"errorCode": "",
"status": "ready",
"solution": {
"objects": [true, false, false, true, true, false, true, true] // 返回圖片是否爲目標,
"labels": ["truck", "boat", "boat", "truck", "truck", "airplane-right", "truck", "truck"] // 返回圖片對應的標籤
},
"taskId": "5aa8be0c-94a5-11ec-80d7-00163f00a53c""
}
OK,我們可以看到,返回結果的 solution 字段中的 objects 字段就包含了一串 true 和 false 的列表,這就代表了每張圖片是否和目標匹配。
知道了這個結果之後,我們只需要將返回結果爲 true 的圖片進行模擬點擊就好了。
代碼基礎實現
行,那有了基本思路之後,那我們就開始用 Python 實現下整個流程吧,這裏我們就拿 https://democaptcha.com/demo-form-eng/hcaptcha.html 這個網站作爲樣例來講解下整個識別和模擬點擊過程。
識別封裝
首先我們對上面的任務 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, queries, question):
logger.debug(f'start to recognize image for question {question}')
data = {
"clientKey": self.api_key,
"task": {
"type": "HCaptchaClassification",
"queries": queries,
"question": question
}
}
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 方法,接收兩個參數,第一個參數 queries 就是每張驗證碼圖片對應的 Base64 編碼,第二個參數 question
就是要識別的問題整句,這裏就是將整個請求用 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 切換支持
接着,下一步我們就該來模擬點擊驗證碼的入口,來觸發驗證碼了對吧。
通過觀察我們發現這個驗證碼和 ReCaptcha 非常類似,其入口其實是在 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(
'.h-captcha > iframe')
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, "Main content")]')
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.CSS_SELECTOR, '#anchor #checkbox')))
captcha_entry.click()
time.sleep(2)
self.switch_to_captcha_content_iframe()
captcha_element: WebElement = self.get_captcha_element()
if captcha_element.is_displayed:
logger.debug('trigged captcha successfully')
這裏首先我們首先調用 switch_to_captcha_entry_iframe 進行了 iframe 的切換,然後找到那個入口框對應的節點,然後點擊一下。
點擊完了之後我們再調用 switch_to_captcha_content_iframe 切換到驗證碼本身對應的 iframe 裏面,查找驗證碼本身對應的節點是否加載出來了,如果加載出來了,那麼就證明觸發成功了。
找出識別目標
OK,那麼現在驗證碼可能就長這樣子了:
那接下來我們要做的就是兩件事了,一件事就是把匹配目標,也就是問題本身找出來,第二件事就是把每張驗證碼保存下來,然後轉成 Base64 編碼。
好,那麼怎麼查找問題呢呢?用 Selenium 常規的節點搜索就好了:
def get_captcha_target_text(self) -> WebElement:
captcha_target_name_element: WebElement = self.wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, '.prompt-text')))
return captcha_target_name_element.text
通過調用這個方法,我們就能得到上圖中完整的問題文本了。
驗證碼識別
接下來,我們就需要把每張圖片進行下載並轉成 Base64 編碼了,我們觀察下它的 HTML 結構:
我們可以看到,每個驗證碼其實都對應了一個 .task-image
的節點,然後裏面有個 .image-wrapper
的節點,在裏面有一個 .image
的節點,那圖片怎麼呈現的呢?這裏它是設置了一個 style CSS 樣式,通過 CSS 的 backgroud
來設置了驗證碼圖片的地址。
所以,我們要想提取驗證碼圖片也比較容易了,我們只需要找出 .image
節點的 style 屬性的內容,然後提取其中的 url 就好了。
得到 URL 之後,轉下 Base64 編碼,利用 captcha_resolver 就可以對內容進行識別了。
所以代碼可以寫爲如下內容:
def verify_captcha(self):
# get target text
self.captcha_target_text = self.get_captcha_target_text()
logger.debug(
f'captcha_target_text {self.captcha_target_text}'
)
# extract all images
single_captcha_elements = self.wait.until(EC.visibility_of_all_elements_located(
(By.CSS_SELECTOR, '.task-image .image-wrapper .image')))
resized_single_captcha_base64_strings = []
for i, single_captcha_element in enumerate(single_captcha_elements):
single_captcha_element_style = single_captcha_element.get_attribute(
'style')
pattern = re.compile('url\("(https.*?)"\)')
match_result = re.search(pattern, single_captcha_element_style)
single_captcha_element_url = match_result.group(
1) if match_result else None
logger.debug(
f'single_captcha_element_url {single_captcha_element_url}')
with open(CAPTCHA_SINGLE_IMAGE_FILE_PATH % (i,), 'wb') as f:
f.write(requests.get(single_captcha_element_url).content)
resized_single_captcha_base64_string = resize_base64_image(
CAPTCHA_SINGLE_IMAGE_FILE_PATH % (i,), (100, 100))
resized_single_captcha_base64_strings.append(
resized_single_captcha_base64_string)
logger.debug(
f'length of single_captcha_element_urls {len(resized_single_captcha_base64_strings)}')
這裏我們提取出來了每張驗證碼圖片的 url,這裏是用正則表達式進行批評的,提取出 url 之後,我們然後將其存入了 resized_single_captcha_base64_strings 列表裏面。
其中這裏的 Base64 編碼我們單獨定義了一個方法,傳入了圖片路徑和調整大小,然後可以返回編碼後的結果,定義如下:
from PIL import Image
import base64
from app.settings import CAPTCHA_RESIZED_IMAGE_FILE_PATH
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')
圖片識別
好,那麼現在我們已經可以得到問題內容了,也能得到每張圖片對應的 Base64 編碼了,我們直接利用 YesCaptcha 進行圖像識別就好了,代碼調用如下:
# try to verify using API
captcha_recognize_result = self.captcha_resolver.create_task(
resized_single_captcha_base64_strings,
self.captcha_target_text
)
if not captcha_recognize_result:
logger.error('count not get captcha recognize result')
return
recognized_results = captcha_recognize_result.get(
'solution', {}).get('objects')
if not recognized_results:
logger.error('count not get captcha recognized indices')
return
如果運行正常的話,我們可能得到如下的返回結果:
{
"errorId":0,
"errorCode":"",
"status":"ready",
"solution":{
"objects":[
true,
false,
false,
false,
true,
false,
true,
true,
false
],
"labels":[
"boat",
"seaplane",
"bicycle",
"train",
"boat",
"train",
"boat",
"boat",
"bus"
]
},
"taskId":"25fee484-df63-11ec-b02e-c2654b11608a"
}
現在我們可以看到 sulution 裏面的 objects 字段就包含了 true false 的列表,比如第一個 true 就代表了第一個驗證碼是和問題匹配的,第二個 false 就代表了第二個驗證碼圖片和問題是不匹配的。那序號和圖片又是怎麼對應的呢?見下圖:
從左到右一行行地數,序號依次遞增,比如第一行第一個序號就是 0,那麼其結果就是 objects 結果裏面的第一個結果,true。
模擬點擊
現在我們已經得到 true false 列表了,我們只需要將結果是 true 的序號提取出來,然後對這些驗證碼小圖點擊就好了,代碼如下:
# click captchas
recognized_indices = [i for i, x in enumerate(recognized_results) if x]
logger.debug(f'recognized_indices {recognized_indices}')
click_targets = self.wait.until(EC.visibility_of_all_elements_located(
(By.CSS_SELECTOR, '.task-image')))
for recognized_index in recognized_indices:
click_target: WebElement = click_targets[recognized_index]
click_target.click()
time.sleep(random())
當然我們也可以通過執行 JavaScript 來對每個節點進行模擬點擊,效果是類似的。
這裏我們用 for 循環將 true false 列表轉成了一個列表,列表的每個元素代表 true 在列表中的位置,其實就是我們的點擊目標了。
然後接着我們獲取了所有的驗證碼小圖對應的節點,然後依次調用 click 方法進行點擊即可。
這樣我們就可以實現驗證碼小圖的逐個識別了。
點擊驗證
好,那麼有了上面的邏輯,我們就能完成整個 HCaptcha 的識別和點選了。
最後,我們模擬點擊驗證按鈕就好了:
# after all captcha clicked
verify_button: WebElement = self.get_verify_button()
if verify_button.is_displayed:
verify_button.click()
time.sleep(3)
而 verfiy_button 的提取也是用 Selenium 即可:
def get_verify_button(self) -> WebElement:
verify_button = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.button-submit')))
return verify_button
校驗結果
點擊完了之後,我們可以嘗試檢查網頁變化,看看有沒有驗證成功。
比如驗證成功的標誌就是出現一個綠色小對勾:
檢查方法如下:
def get_is_successful(self):
self.switch_to_captcha_entry_iframe()
anchor: WebElement = self.wait.until(EC.visibility_of_element_located((
By.CSS_SELECTOR, '#anchor #checkbox'
)))
checked = anchor.get_attribute('aria-checked')
logger.debug(f'checked {checked}')
return str(checked) == 'true'
這裏我們先切換了 iframe,然後檢查了對應的 class 是否是符合期望的。
最後如果 get_is_successful 返回結果是 True,那就代表識別成功了,那就整個完成了。
如果返回結果是 False,我們可以進一步遞歸調用上述邏輯進行二次識別,直到識別成功即可。
# check if succeed
is_succeed = self.get_is_successful()
if is_succeed:
logger.debug('verifed successfully')
else:
self.verify_captcha()
運行視頻
代碼
以上代碼可能比較複雜,這裏我將代碼進行了規整,然後放到 GitHub 上了,大家如有需要可以自取:https://github.com/Python3WebSpider/HCaptchaResolver
註冊地址
最後需要說明一點,上面的驗證碼服務是收費的,每驗證一次可能花一定的點數,比如識別一次 3x3 的圖要花 10 點數,而充值一塊錢就能獲得 1000 點數,所以識別一次就一分錢,還是比較便宜的。
轉:https://zhuanlan.zhihu.com/p/522241889?utm_id=0