Hướng Dẫn Thực Hành

Tải CAPTCHA động: Phát hiện CAPTCHA được tải chậm

page.goto() xong, lấy HTML ra mà không thấy .g-recaptcha hay .cf-turnstile đâu cả? Không có nghĩa là trang không có CAPTCHA — nó chỉ chưa được chèn vào DOM. Nhiều site tải CAPTCHA "lười" (lazy-loaded): chỉ render sau khi bấm nút, focus ô nhập, cuộn tới form, hoặc sau một request AJAX cụ thể. Script lấy sitekey ngay sau khi trang load xong sẽ luôn nhận null. Bài này chỉ ra 3 cách bắt sự kiện CAPTCHA xuất hiện bằng Puppeteer và Selenium, cùng cách nối thẳng vào luồng giải token của CaptchaAI.

Tình huống quen thuộc với team QA outsourcing ở TP.HCM/Hà Nội test flow đăng nhập nội bộ, hoặc theo dõi giá Shopee/Lazada — nơi CAPTCHA chỉ xuất hiện sau khi cuộn tới đúng vị trí.

Ba cách bắt sự kiện đó, theo thứ tự nên thử:

  1. MutationObserver — theo dõi mọi thay đổi DOM, không cần biết trước trigger.
  2. Chờ script CAPTCHA nạp xong — kiểm tra biến global khi đã biết CAPTCHA nào sẽ xuất hiện.
  3. Chặn lệnh render — hook thư viện CAPTCHA trước cả khi nó chạy, bắt được sitekey sớm nhất.

Vì sao CAPTCHA "biến mất" trong page source

Đây là những kiểu trigger phổ biến nhất khiến CAPTCHA không có sẵn khi script vừa lấy HTML:

  • Bấm nút — nút "Gửi" mới chèn reCAPTCHA vào form; kích hoạt bằng cách click vào nút trước.
  • Focus vào ô nhập — CAPTCHA xuất hiện khi focus vào input; kích hoạt bằng cách focus trường email/password.
  • Vị trí cuộn — CAPTCHA chỉ tải khi section đó lọt vào viewport; kích hoạt bằng cách cuộn tới form.
  • Hẹn giờ (timer) — CAPTCHA tải sau vài giây; kích hoạt bằng cách chờ đúng khoảng delay.
  • Điều kiện JavaScript — CAPTCHA tải sau khi có phản hồi AJAX; kích hoạt bằng cách gọi request tiên quyết trước.

Cách 1: bắt CAPTCHA bằng MutationObserver

Theo dõi DOM để biết chính xác thời điểm phần tử CAPTCHA được thêm vào, bất kể trang kích hoạt bằng cách nào:

Code Puppeteer

const puppeteer = require('puppeteer');

const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://staging.example.com/qa-login');

// Set up MutationObserver before triggering the CAPTCHA
const captchaInfo = await page.evaluate(() => {
  return new Promise((resolve) => {
    // Check if already present
    const existing = document.querySelector('.g-recaptcha, .cf-turnstile, .h-captcha');
    if (existing) {
      resolve({
        type: existing.className,
        sitekey: existing.getAttribute('data-sitekey'),
      });
      return;
    }

    // Watch for new elements
    const observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        for (const node of mutation.addedNodes) {
          if (node.nodeType !== 1) continue;

          const captcha = node.matches?.('.g-recaptcha, .cf-turnstile, .h-captcha')
            ? node
            : node.querySelector?.('.g-recaptcha, .cf-turnstile, .h-captcha');

          if (captcha) {
            observer.disconnect();
            resolve({
              type: captcha.className,
              sitekey: captcha.getAttribute('data-sitekey'),
            });
            return;
          }
        }
      }
    });

    observer.observe(document.body, { childList: true, subtree: true });

    // Timeout after 30 seconds
    setTimeout(() => {
      observer.disconnect();
      resolve(null);
    }, 30000);
  });
});

console.log('Detected CAPTCHA:', captchaInfo);

Cách trigger CAPTCHA xuất hiện

Gọi hành động kích hoạt (click, focus, cuộn) trước, rồi mới đợi observer báo kết quả:

// Click the submit button to trigger CAPTCHA
await page.click('#submit-btn');

// Or focus the input
await page.focus('#email');

// Or scroll to the form
await page.evaluate(() => {
  document.querySelector('#signup-form').scrollIntoView();
});

Cách 2: chờ script CAPTCHA được chèn vào trang

Mỗi loại CAPTCHA cần thư viện JavaScript riêng nạp xong mới render. Kiểm tra biến global thay vì đoán thời gian chờ:

// Wait for reCAPTCHA script to load
await page.waitForFunction(() => {
  return typeof window.grecaptcha !== 'undefined' 
    && typeof window.grecaptcha.render === 'function';
}, { timeout: 30000 });

// Now extract parameters
const sitekey = await page.evaluate(() => {
  const el = document.querySelector('.g-recaptcha');
  return el?.getAttribute('data-sitekey');
});

Với Cloudflare Turnstile

Logic tương tự, chỉ đổi biến global cần kiểm tra:

await page.waitForFunction(() => {
  return typeof window.turnstile !== 'undefined';
}, { timeout: 30000 });

const sitekey = await page.evaluate(() => {
  const el = document.querySelector('.cf-turnstile');
  return el?.getAttribute('data-sitekey');
});

Cách 3: chặn lệnh gọi render trước khi CAPTCHA kịp hiển thị

Hook vào thư viện CAPTCHA ngay khi nó gán vào window, trước cả khi trang chạy script — bắt được sitekey và callback mà không cần đợi phần tử xuất hiện trong DOM:

// Inject before page scripts run
await page.evaluateOnNewDocument(() => {
  window.__captchaDetected = null;

  // Hook grecaptcha.render
  let _grecaptcha;
  Object.defineProperty(window, 'grecaptcha', {
    set(val) {
      _grecaptcha = val;
      const origRender = val.render;
      val.render = function(container, params) {
        window.__captchaDetected = {
          type: 'recaptcha',
          sitekey: params.sitekey,
          callback: params.callback?.name,
          container: typeof container === 'string' ? container : container.id,
        };
        return origRender.apply(this, arguments);
      };
    },
    get() { return _grecaptcha; },
  });
});

await page.goto('https://example.com/signup');

// Trigger the CAPTCHA (click, scroll, etc.)
await page.click('#show-form');

// Wait for detection
await page.waitForFunction(() => window.__captchaDetected !== null, {
  timeout: 30000,
});

const detected = await page.evaluate(() => window.__captchaDetected);
console.log('Detected:', detected);
// { type: 'recaptcha', sitekey: '6Le-wvkS...', callback: 'onCaptcha', container: 'recaptcha-box' }
  • Cách này chỉ bắt được reCAPTCHA (hook grecaptcha.render); với Turnstile/hCaptcha cần hook biến global tương ứng theo cùng pattern.

Selenium (Python): chờ CAPTCHA tải chậm

Cùng logic Puppeteer, dùng WebDriverWait thay cho waitForFunction:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

driver = webdriver.Chrome()
driver.get("https://staging.example.com/qa-login")

# Trigger the CAPTCHA loading
submit = driver.find_element(By.ID, "submit-btn")
submit.click()

# Wait for CAPTCHA to appear
try:
    captcha_el = WebDriverWait(driver, 30).until(
        EC.presence_of_element_located((
            By.CSS_SELECTOR,
            ".g-recaptcha, .cf-turnstile, .h-captcha"
        ))
    )
    sitekey = captcha_el.get_attribute("data-sitekey")
    captcha_class = captcha_el.get_attribute("class")

    if "g-recaptcha" in captcha_class:
        captcha_type = "recaptcha"
    elif "cf-turnstile" in captcha_class:
        captcha_type = "turnstile"
    else:
        captcha_type = "hcaptcha"

    print(f"Type: {captcha_type}, Sitekey: {sitekey}")
except Exception:
    print("No CAPTCHA appeared within 30 seconds")

Chờ iframe reCAPTCHA xuất hiện

Div .g-recaptcha có thể đã có trong DOM dù script chưa chạy xong — đợi thẳng iframe mới đáng tin:

# reCAPTCHA loads an iframe even when the div exists but the script is still loading
WebDriverWait(driver, 30).until(
    EC.presence_of_element_located((
        By.CSS_SELECTOR,
        "iframe[src*='recaptcha'], iframe[src*='challenges.cloudflare.com']"
    ))
)
print("CAPTCHA iframe loaded")

Luồng đầy đủ: phát hiện, giải và điền token

Gộp lại: kích hoạt, chờ CAPTCHA xuất hiện, gửi sitekey tới in.php, polling res.php tới khi có token, rồi điền token vào form:

import requests
import time

def detect_and_solve(driver, api_key, trigger_action=None):
    """Detect a lazy-loaded CAPTCHA, solve it, and inject the token."""

    # 1. Trigger the CAPTCHA
    if trigger_action:
        trigger_action(driver)

    # 2. Wait for it to appear
    captcha_el = WebDriverWait(driver, 30).until(
        EC.presence_of_element_located((
            By.CSS_SELECTOR,
            ".g-recaptcha, .cf-turnstile, .h-captcha"
        ))
    )

    sitekey = captcha_el.get_attribute("data-sitekey")
    page_url = driver.current_url
    captcha_class = captcha_el.get_attribute("class")

    # 3. Determine type and method
    if "g-recaptcha" in captcha_class:
        method, key_param, token_field = "userrecaptcha", "googlekey", "g-recaptcha-response"
    elif "cf-turnstile" in captcha_class:
        method, key_param, token_field = "turnstile", "sitekey", "cf-turnstile-response"
    else:
        method, key_param, token_field = "hcaptcha", "sitekey", "h-captcha-response"

    # 4. Solve with CaptchaAI
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": api_key, "method": method,
        key_param: sitekey, "pageurl": page_url, "json": "1",
    }).json()

    task_id = resp["request"]
    for _ in range(24):
        time.sleep(5)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": api_key, "action": "get", "id": task_id, "json": "1"
        }).json()
        if result["status"] == 1:
            token = result["request"]
            break

    # 5. Inject
    driver.execute_script(f"""
        const el = document.querySelector('textarea[name="{token_field}"], input[name="{token_field}"]');
        if (el) el.value = arguments[0];
    """, token)

    return token

Các lỗi thường gặp khi CAPTCHA tải động

  • CAPTCHA không bao giờ xuất hiện — trigger sai hoặc chưa đủ; thử từng thao tác (click, focus, cuộn) để tìm đúng yếu tố kích hoạt.
  • Sitekey trả về null — phần tử đã có trong DOM nhưng script chưa chạy xong; đợi iframe CAPTCHA thay vì chỉ đợi div.
  • MutationObserver không bắt được — CAPTCHA đã render trước khi gắn observer; kiểm tra phần tử có sẵn trước khi gắn observer.
  • Luôn timeout — trang chỉ tải CAPTCHA cho phiên giống người dùng thật; dùng trình duyệt có giao diện đầy đủ, viewport và user-agent hợp lệ.

Câu hỏi thường gặp

Làm sao biết một CAPTCHA là tải ngay hay tải chậm (lazy-loaded)?

Xem page source (Ctrl+U) ngay khi trang vừa load. Nếu div/script CAPTCHA không có ở đó nhưng xuất hiện sau khi click, focus hoặc cuộn, đó là CAPTCHA tải chậm — script phải đợi đúng sự kiện đó.

Timeout bao lâu là hợp lý khi chờ CAPTCHA xuất hiện?

30 giây là mốc an toàn — đủ cho AJAX chậm hoặc timer dài. Trang nặng hơn thì tăng timeout theo thực tế đo được, luôn xử lý except/timeout thay vì để script treo.

Nên dùng MutationObserver hay waitForFunction?

Tùy vào việc bạn có biết trước trigger hay không:

  • MutationObserver — chưa biết hành động nào kích hoạt CAPTCHA; bắt mọi thay đổi DOM.
  • waitForFunction — đã biết CAPTCHA gắn vào biến global nào (window.grecaptcha, window.turnstile); code ngắn, dễ đọc hơn.

Chạy trên trình duyệt headless có phát hiện được CAPTCHA tải chậm không?

Được, nhưng một số trang chỉ render CAPTCHA cho phiên giống người dùng thật. Dùng headless: 'new' trong Puppeteer với viewport, user-agent hợp lệ.

CaptchaAI có tính phí riêng cho CAPTCHA tải chậm không?

Không. CaptchaAI tính theo thread, không theo loại CAPTCHA hay thời điểm nó xuất hiện trên trang.

  • Gói BASIC ($15/tháng, 5 thread) đã đủ chạy mọi ví dụ trong bài này.

Giải bất kỳ CAPTCHA nào bằng CaptchaAI

Sau khi bắt được sitekey, phần còn lại chỉ là gọi API.

Nhận khóa API của bạn tại captchaai.com và cắm thẳng vào luồng detect_and_solve ở trên.


Đọc thêm

Ba bài liên quan nếu bạn đang xử lý CAPTCHA phức tạp hơn một trang đơn lẻ:

Os comentários estão desativados para este artigo.