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 | 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.