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

Giải CAPTCHA bằng Playwright Python và CaptchaAI

Tích hợp CaptchaAI vào một script Playwright bất đồng bộ chỉ cần bốn bước: lấy sitekey, gửi task tới in.php, polling res.php tới khi có token, rồi tiêm token vào form trước khi submit. Playwright chạy nhanh hơn Selenium và hỗ trợ async gốc — nhiều đội automation/QA tại các công ty outsourcing sản phẩm ở TP.HCM và Hà Nội đang chuyển stack testing sang Playwright vì lý do này. Bài này đi thẳng vào code: một class PlaywrightCaptchaSolver hoàn chỉnh để giải reCAPTCHA v2, Cloudflare Turnstile và CAPTCHA hình ảnh.

Toàn bộ ví dụ trong bài chạy trên hệ thống staging bạn có quyền truy cập. Nếu pipeline có lưu dữ liệu thu thập được, nhớ áp dụng nguyên tắc tối thiểu hóa dữ liệu và ghi log truy cập theo Nghị định 13/2023/NĐ-CP về bảo vệ dữ liệu cá nhân.


So sánh Playwright và Selenium khi giải CAPTCHA

Trước khi vào code, đây là lý do nhiều đội QA/automation VN đổi stack — khác biệt chính nằm ở tốc độ, hỗ trợ async và cách chặn request; cả hai đều tích hợp CaptchaAI qua cùng endpoint in.php/res.php:

Tiêu chí Playwright Selenium
Async gốc Không (cần threading)
Cấu hình QA chuẩn Mặc định tốt hơn Cần cấu hình thêm
Tốc độ Nhanh hơn Tải trang chậm hơn
Chặn request Có sẵn Cần proxy/extension
Đa trình duyệt Chromium, Firefox, WebKit Chrome, Firefox, Edge, Safari
Kiểu API Promise/async, hiện đại Imperative, truyền thống

Nên chuyển sang Playwright khi:

  • Đội đang viết test mới từ đầu và chưa có codebase Selenium lớn cần giữ.
  • Pipeline QA cần chạy nhiều worker song song — async gốc của Playwright tránh được overhead threading của Selenium.
  • Trang đích thay đổi DOM thường xuyên và cần page.route để chặn request thay vì cấu hình proxy riêng.

Điều kiện tiên quyết

Cài Playwright cùng aiohttp (client HTTP bất đồng bộ gọi API CaptchaAI), rồi tải binary Chromium:

pip install playwright aiohttp
playwright install chromium

Viết bộ giải CaptchaAI bất đồng bộ

Hàm solve_captcha dưới đây gửi task tới in.php, polling res.php mỗi 5 giây và trả token khi giải xong. Mọi hàm giải CAPTCHA trong bài đều gọi lại hàm này:

import aiohttp
import asyncio

API_KEY = "YOUR_API_KEY"

async def solve_captcha(method, **params):
    """Async CaptchaAI solver for Playwright workflows."""
    async with aiohttp.ClientSession() as session:
        # Submit task
        submit_data = {
            "key": API_KEY,
            "method": method,
            "json": 1,
            **params,
        }
        async with session.post("https://ocr.captchaai.com/in.php", data=submit_data) as resp:
            data = await resp.json(content_type=None)
            if data.get("status") != 1:
                raise Exception(f"Submit error: {data.get('request')}")
            task_id = data["request"]

        # Poll for result
        for _ in range(30):
            await asyncio.sleep(5)
            async with session.get("https://ocr.captchaai.com/res.php", params={
                "key": API_KEY,
                "action": "get",
                "id": task_id,
                "json": 1,
            }) as resp:
                result = await resp.json(content_type=None)
                if result.get("status") == 1:
                    return result["request"]
                if result.get("request") == "ERROR_CAPTCHA_UNSOLVABLE":
                    raise Exception("CAPTCHA unsolvable")

        raise TimeoutError("Solve timed out")

Vòng lặp dừng sau 30 lần polling (~150 giây); tăng số này nếu CAPTCHA cần thời gian xử lý lâu hơn.


Khởi tạo trình duyệt Playwright với cấu hình QA chuẩn

Trước khi vào từng loại CAPTCHA cụ thể, khởi tạo một trình duyệt Playwright dùng chung cho cả script:

from playwright.async_api import async_playwright

async def create_browser():
    """Launch Playwright browser with stealth-configuredion settings."""
    pw = await async_playwright().start()
    browser = await pw.chromium.launch(
        headless=False,
        args=[
            "",
        ],
    )
    context = await browser.new_context(
        user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                   "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
        viewport={"width": 1920, "height": 1080},
        locale="en-US",
    )

    # Remove Playwright detection signals
    await context.add_init_script("""
        Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
        delete navigator.__proto__.webdriver;
    """)

    page = await context.new_page()
    return pw, browser, context, page

Context ở trên set locale en-US, viewport 1920x1080 và user agent Chrome thật để trang đích render đúng như trên trình duyệt thông thường.


Giải reCAPTCHA v2 với Playwright

Hàm dưới đây lấy sitekey bằng regex trên HTML, gửi cho CaptchaAI qua method userrecaptcha, rồi tiêm token vào ô ẩn g-recaptcha-response và gọi lại callback JS của Google nếu trang dùng callback thay vì submit form thường:

import re

async def solve_recaptcha_v2_playwright(page, url):
    """Complete reCAPTCHA v2 solve in Playwright."""
    await page.goto(url, wait_until="networkidle")

    # Extract sitekey from the page
    content = await page.content()
    match = re.search(r'data-sitekey=["\']([A-Za-z0-9_-]{40})["\']', content)
    if not match:
        raise ValueError("reCAPTCHA sitekey not found")

    sitekey = match.group(1)
    print(f"Sitekey: {sitekey}")

    # Solve via CaptchaAI
    token = await solve_captcha(
        "userrecaptcha",
        googlekey=sitekey,
        pageurl=url,
    )
    print(f"Token: {token[:50]}...")

    # Inject token
    await page.evaluate(f"""() => {{
        document.getElementById('g-recaptcha-response').value = '{token}';
        document.getElementById('g-recaptcha-response').style.display = 'block';
    }}""")

    # Trigger callback if available
    await page.evaluate(f"""() => {{
        if (typeof ___grecaptcha_cfg !== 'undefined') {{
            var clients = ___grecaptcha_cfg.clients;
            for (var key in clients) {{
                var client = clients[key];
                try {{
                    Object.keys(client).forEach(function(k) {{
                        if (client[k] && client[k].callback) {{
                            client[k].callback('{token}');
                        }}
                    }});
                }} catch(e) {{}}
            }}
        }}
    }}""")

    # Submit form
    await page.click("button[type='submit'], input[type='submit']")
    await page.wait_for_load_state("networkidle")

    return token

Nếu trang không expose callback qua ___grecaptcha_cfg, bỏ qua khối đó — điền token vào ô ẩn rồi submit form trực tiếp là đủ.


Giải Cloudflare Turnstile với Playwright

Turnstile dùng input ẩn tên cf-turnstile-response thay vì g-recaptcha-response, và cách lấy sitekey cũng khác vì Cloudflare không cố định độ dài chuỗi:

async def solve_turnstile_playwright(page, url):
    """Complete Turnstile solve in Playwright."""
    await page.goto(url, wait_until="networkidle")

    content = await page.content()

    # Extract sitekey
    match = re.search(r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', content)
    if not match:
        match = re.search(r"sitekey\s*:\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]", content)
    if not match:
        raise ValueError("Turnstile sitekey not found")

    sitekey = match.group(1)
    print(f"Turnstile sitekey: {sitekey}")

    # Solve via CaptchaAI
    token = await solve_captcha(
        "turnstile",
        sitekey=sitekey,
        pageurl=url,
    )

    # Inject token into hidden inputs
    await page.evaluate(f"""() => {{
        document.querySelectorAll('[name="cf-turnstile-response"]')
            .forEach(el => el.value = '{token}');
    }}""")

    # Submit
    await page.click("button[type='submit'], input[type='submit']")
    await page.wait_for_load_state("networkidle")

    return token

Giải CAPTCHA hình ảnh với Playwright

Với CAPTCHA hình ảnh thuần túy, chụp ảnh phần tử bằng Playwright, encode base64 rồi gửi cho CaptchaAI qua method base64:

async def solve_image_captcha_playwright(page, captcha_selector):
    """Solve image CAPTCHA visible on the page."""
    captcha_element = page.locator(captcha_selector)

    # Screenshot the CAPTCHA image
    img_bytes = await captcha_element.screenshot()
    import base64
    img_base64 = base64.b64encode(img_bytes).decode()

    # Solve via CaptchaAI
    answer = await solve_captcha("base64", body=img_base64)
    print(f"Answer: {answer}")

    # Type the answer
    captcha_input = page.locator("input[name='captcha'], input[name='code'], input.captcha-input")
    await captcha_input.fill(answer)

    return answer

Chặn request mạng để lấy tham số CAPTCHA

Nếu sitekey không nằm sẵn trong HTML, chặn request mạng bằng page.route để đọc tham số trực tiếp từ URL gọi API:

async def intercept_captcha_params(page, url):
    """Intercept network requests to find CAPTCHA parameters."""
    captcha_params = {}

    async def handle_request(route, request):
        if "recaptcha" in request.url or "turnstile" in request.url:
            from urllib.parse import urlparse, parse_qs
            parsed = urlparse(request.url)
            params = parse_qs(parsed.query)
            captcha_params.update(params)
            print(f"Intercepted: {request.url}")
        await route.continue_()

    await page.route("**/*", handle_request)
    await page.goto(url, wait_until="networkidle")
    await page.unroute("**/*")

    return captcha_params

Class PlaywrightCaptchaSolver: gộp toàn bộ workflow

Class dưới đây gộp các hàm ở trên thành một workflow duy nhất: mở trình duyệt, nhận diện loại CAPTCHA đang hiển thị, giải, điền form và submit:

import re
import asyncio
import aiohttp
import base64
from playwright.async_api import async_playwright

API_KEY = "YOUR_API_KEY"

class PlaywrightCaptchaSolver:
    """Complete Playwright + CaptchaAI automation class."""

    def __init__(self, api_key, headless=False):
        self.api_key = api_key
        self.headless = headless
        self.pw = None
        self.browser = None
        self.context = None
        self.page = None

    async def start(self):
        """Initialize the browser."""
        self.pw = await async_playwright().start()
        self.browser = await self.pw.chromium.launch(
            headless=self.headless,
            args=[""],
        )
        self.context = await self.browser.new_context(
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                       "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
            viewport={"width": 1920, "height": 1080},
        )
        await self.context.add_init_script(
            "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
        )
        self.page = await self.context.new_page()

    async def stop(self):
        """Close the browser."""
        if self.browser:
            await self.browser.close()
        if self.pw:
            await self.pw.stop()

    async def navigate(self, url):
        """Navigate and wait for page to load."""
        await self.page.goto(url, wait_until="networkidle")

    async def detect_captcha(self):
        """Detect which CAPTCHA type is present."""
        content = await self.page.content()

        if re.search(r'data-sitekey=["\'][A-Za-z0-9_-]{40}["\']', content):
            if "recaptcha" in content.lower():
                return "recaptcha_v2"

        if "cf-turnstile" in content or "challenges.cloudflare.com/turnstile" in content:
            return "turnstile"

        if re.search(r"render=[A-Za-z0-9_-]{40}", content):
            return "recaptcha_v3"

        img_count = await self.page.locator(
            "img.captcha, img[alt*='captcha'], img[src*='captcha']"
        ).count()
        if img_count > 0:
            return "image"

        return None

    async def solve_and_submit(self, url, form_data=None):
        """Full workflow: navigate, detect, solve, fill, submit."""
        await self.navigate(url)
        captcha_type = await self.detect_captcha()

        if captcha_type:
            print(f"Detected: {captcha_type}")
            await self._solve(captcha_type)

        if form_data:
            for name, value in form_data.items():
                try:
                    await self.page.fill(f"[name='{name}']", value)
                except Exception:
                    pass

        await self.page.click("button[type='submit'], input[type='submit']")
        await self.page.wait_for_load_state("networkidle")
        return self.page.url

    async def _solve(self, captcha_type):
        content = await self.page.content()
        url = self.page.url

        if captcha_type == "recaptcha_v2":
            match = re.search(r'data-sitekey=["\']([A-Za-z0-9_-]{40})["\']', content)
            token = await self._api_solve("userrecaptcha", googlekey=match.group(1), pageurl=url)
            await self.page.evaluate(f"""() => {{
                document.getElementById('g-recaptcha-response').value = '{token}';
            }}""")

        elif captcha_type == "turnstile":
            match = re.search(r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', content)
            token = await self._api_solve("turnstile", sitekey=match.group(1), pageurl=url)
            await self.page.evaluate(f"""() => {{
                document.querySelectorAll('[name="cf-turnstile-response"]')
                    .forEach(el => el.value = '{token}');
            }}""")

        elif captcha_type == "image":
            img = self.page.locator("img.captcha, img[alt*='captcha'], img[src*='captcha']").first
            img_bytes = await img.screenshot()
            answer = await self._api_solve("base64", body=base64.b64encode(img_bytes).decode())
            await self.page.fill("input[name='captcha'], input[name='code']", answer)

    async def _api_solve(self, method, **params):
        async with aiohttp.ClientSession() as session:
            async with session.post("https://ocr.captchaai.com/in.php", data={
                "key": self.api_key, "method": method, "json": 1, **params,
            }) as resp:
                data = await resp.json(content_type=None)
                if data.get("status") != 1:
                    raise Exception(f"Submit error: {data.get('request')}")
                task_id = data["request"]

            for _ in range(30):
                await asyncio.sleep(5)
                async with session.get("https://ocr.captchaai.com/res.php", params={
                    "key": self.api_key, "action": "get", "id": task_id, "json": 1,
                }) as resp:
                    result = await resp.json(content_type=None)
                    if result.get("status") == 1:
                        return result["request"]
            raise TimeoutError("Solve timed out")

# Usage
async def main():
    solver = PlaywrightCaptchaSolver(API_KEY)
    await solver.start()
    try:
        result = await solver.solve_and_submit(
            "https://staging.example.com/qa-login",
            form_data={"email": "[email protected]", "password": "pass123"},
        )
        print(f"Result: {result}")
    finally:
        await solver.stop()

asyncio.run(main())

Luôn kiểm thử trên hệ thống bạn có quyền truy cập, như staging.example.com ở ví dụ trên, trước khi đưa vào production.


Khắc phục sự cố thường gặp

Triệu chứng Nguyên nhân Cách xử lý
page.evaluate lỗi Nội dung trang chưa load xong Dùng wait_until="networkidle"
Tiêm token không có tác dụng Sai selector phần tử Kiểm tra bằng page.content() để tìm đúng phần tử
Trang phát hiện Playwright Thiếu init script Thêm ghi đè navigator.webdriver trong add_init_script
Timeout ở networkidle Trang có script polling vô hạn Đổi sang wait_until="domcontentloaded"
Screenshot CAPTCHA trống Phần tử đang ẩn Cuộn vào khung nhìn: await element.scroll_into_view_if_needed()

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

CaptchaAI hỗ trợ những loại CAPTCHA nào khi dùng với Playwright?

reCAPTCHA v2 (kể cả Enterprise), reCAPTCHA v3, Cloudflare Turnstile, Cloudflare Challenge, GeeTest v3, CAPTCHA hình ảnh/OCR và grid image — dùng chung hàm solve_captcha như trong bài, chỉ đổi method. CaptchaAI không hỗ trợ hCaptcha và FunCaptcha (Arkose Labs); GeeTest v4 đang trong giai đoạn sắp ra mắt.

Chạy Playwright ở chế độ headless có ảnh hưởng tới tỷ lệ giải CAPTCHA không?

Không. CaptchaAI giải trên hạ tầng riêng, tách biệt với trình duyệt Playwright của bạn, nên headless=True hay headless=False không đổi kết quả giải. Một số trang đích tự kiểm tra dấu hiệu headless ở tầng của họ — nếu form không submit được, thử headless=False trước khi báo lỗi cho CaptchaAI.

Nên dùng polling hay callback trong workflow Playwright?

Polling (như hàm solve_captcha ở trên) là lựa chọn mặc định cho script chạy độc lập, đơn giản và không cần expose endpoint public. Dùng callback/webhook khi bạn chạy nhiều worker Playwright song song và muốn CaptchaAI tự gửi token về một service trung tâm.

Giải CAPTCHA cho pipeline QA bằng Playwright tốn bao nhiêu mỗi tháng?

Tùy khối lượng và số luồng chạy song song. CaptchaAI tính phí theo thread chứ không theo lượt giải: BASIC ($15/tháng, 5 thread) đủ cho pipeline QA nhỏ chạy tuần tự; chạy nhiều worker song song thì cân nhắc STANDARD ($30/tháng, 15 thread) hoặc ADVANCE ($90/tháng, 50 thread).

Có cần đổi code khi chuyển từ script Selenium cũ sang class PlaywrightCaptchaSolver?

Chỉ phần điều khiển trình duyệt (goto, locator, evaluate) đổi cú pháp; phần gọi CaptchaAI qua in.php/res.php giữ nguyên logic, vì hàm solve_captcha không phụ thuộc Playwright hay Selenium.


Tóm tắt

Playwright kết hợp CaptchaAI cho ra một stack CAPTCHA bất đồng bộ, dễ bảo trì hơn cách làm dựa trên Selenium truyền thống. Dùng class PlaywrightCaptchaSolver ở trên làm khung cho quy trình nhận diện → giải → điền form → submit, rồi mở rộng thêm loại CAPTCHA khác khi cần.

Bài viết liên quan

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