HCaptcha 的模擬點擊破解教程

前面的文章“谷歌驗證碼ReCAPTCHA 的模擬點擊破解方案來了!”我們介紹過 ReCaptcha 的模擬點擊破解教程,但除了 ReCaptcha,還有另外和 ReCapacha 驗證流程很相似的驗證碼,叫做 HCaptcha。

ReCaptcha 是谷歌家的,因爲某些原因,咱們國內是無法使用 ReCaptcha 的,所以有時候 HCaptcha 也成了一些國際性網站的比較好的選擇。

那今天我們就來了解下 HCaptcha 和它的模擬點擊破解流程。

HCaptcha

我們首先看看 HCaptcha 的驗證交互流程,其 Demo 網站爲 ,打開之後,我們可以看到如下的驗證碼入口頁面:

 

 

看起來入口和 ReCaptcha 很相似的對吧,其實驗證流程也是很類似的。

當我們點擊複選框時,驗證碼會先通過其風險分析引擎判斷當前用戶的風險,如果是低風險用戶,便可以直接通過,反之,驗證碼會彈出對話框,讓我們回答對話框中的問題,類似如下:

 

 

 

這時候我們看到 HCaptcha驗證碼會給我們一個問題,比如上圖的問題是「請點擊每張包含飛機的圖片」,我們需要從下面的九張圖中選擇出含有飛機的圖片,如果九張圖片中,沒有飛機,則點擊「跳過 / Skip」按鈕,如果有,則將所有帶有飛機的圖片都選擇上,跳過按鈕會變成「檢查 / Verify」按鈕,驗證通過之後我們就可以看到如下的驗證成功的效果了:

 

 

 

是不是整體流程和 ReCaptcha 還是還是非常相近的?

但其實這個比 ReCaptcha 簡單一些,它的驗證碼圖片每次一定是 3x3 的,沒有 4x4 的,而且點擊一個圖之後不會再出現一個新的小圖讓我們二次選擇,所以其破解思路也相對簡單一些。

如何破解

整個流程其實我們稍微梳理下,就知道整體的的破解思路了,有這麼兩個關鍵點:

  • 第一就是把上面的文字內容找出來,以便於我們知道要點擊的內容是什麼。
  • 第二就是我們要知道哪些目標圖片和上面的文字是匹配的,找到了依次模擬點擊就好了。

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

前面 ReCaptcha 的破解過程我們瞭解過了使用 YesCaptcha 來進行圖片的識別,除了 ReCaptcha,YesCaptcha 其實也支持 HCaptcha 的驗證碼識別,利用 YesCaptcha 我們也能輕鬆知道哪些圖片和輸入內容是匹配的。

下面讓們來試試看。

YesCaptcha

在使用之前我們需要先註冊下這個網站,網站地址是 ,註冊個賬號之後大家可以在後臺獲取一個賬戶密鑰,也就是 ClientKey,保存備用。

 

 

OK,然後我們可以查看下這裏的官方文檔:,這裏介紹介紹了一個 API,大致內容是這樣的。

首先有一個創建任務的 API,API 地址爲 ,然後看下請求參數:

 

 

這裏我們需要傳入這麼幾個參數:

  • 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 實現下整個流程吧,這裏我們就拿 這個網站作爲樣例來講解下整個識別和模擬點擊過程。

識別封裝

首先我們對上面的任務 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,這個對應的就是 這個 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 上了,大家如有需要可以自取:

註冊地址

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

轉:https://zhuanlan.zhihu.com/p/522241889?utm_id=0

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章