Migrasi dari Selenium ke Playwright biasanya memunculkan satu pertanyaan lebih dulu daripada yang lain: bagaimana cara solve CAPTCHA di alur async tanpa memblokir event loop? Jawabannya sederhana — panggil CaptchaAI dari coroutine terpisah, submit task ke in.php, lalu polling res.php sampai token siap. Ini persis pola yang dipakai automation engineer dan tim scraping freelance di Indonesia saat menjalankan puluhan job pemantauan harga marketplace secara paralel tanpa saling menunggu. Panduan ini membangun integrasi itu langkah demi langkah: solver async, browser Playwright, solve reCAPTCHA v2, Cloudflare Turnstile, CAPTCHA gambar, sampai satu class PlaywrightCaptchaSolver yang siap dipakai di skrip produksi.
Prasyarat
Install dua dependency berikut sebelum mulai — aiohttp untuk komunikasi async ke CaptchaAI, playwright untuk kontrol browser:
pip install playwright aiohttp
playwright install chromium
Fungsi solver CAPTCHA async
Fungsi async ini jadi fondasi semua solve di panduan ini: submit task ke in.php, lalu polling res.php tiap 5 detik sampai token keluar atau lima menit habis. Karena aiohttp non-blocking, Anda bisa menjalankan beberapa solve paralel dari job Playwright berbeda tanpa saling menunggu.
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")
Menyiapkan browser Playwright
Fungsi ini membuka Chromium lewat Playwright dengan viewport desktop dan user agent Chrome standar, lalu menghapus properti sinyal browser otomatis yang bisa dibaca skrip deteksi bot di halaman target. Ini konfigurasi standar untuk automation Playwright — bukan bagian dari solve CAPTCHA itu sendiri, karena CaptchaAI menyelesaikan CAPTCHA di infrastrukturnya sendiri, terlepas dari cara browser Anda dikonfigurasi.
from playwright.async_api import async_playwright
async def create_browser():
"""Launch Playwright browser with standar 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
Solve reCAPTCHA v2 lewat Playwright
Alurnya empat langkah: ambil sitekey dari HTML halaman, kirim ke CaptchaAI, suntik token ke elemen g-recaptcha-response, lalu trigger callback reCAPTCHA kalau situs target punya satu, sebelum submit form.
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
Solve Cloudflare Turnstile lewat Playwright
Polanya sama seperti reCAPTCHA v2, cuma beda method (turnstile) dan nama field token (cf-turnstile-response). CaptchaAI biasanya menyelesaikan Turnstile di bawah 10 detik dengan tingkat keberhasilan tinggi, jadi cocok dipasang di alur checkout atau login yang butuh respons cepat.
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
Solve CAPTCHA gambar lewat Playwright
Untuk CAPTCHA gambar klasik, screenshot elemennya langsung dari halaman, encode base64, kirim ke CaptchaAI dengan method base64, lalu isi hasilnya ke input jawaban. Tipe ini solve di bawah 0.5 detik dengan tingkat keberhasilan tinggi — cara paling ringkas kalau situs target tidak pakai reCAPTCHA atau Turnstile.
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
Intersepsi request untuk ambil parameter CAPTCHA
Playwright punya request interception bawaan lewat page.route, jadi Anda bisa menangkap parameter CAPTCHA (sitekey, action, dan lainnya) langsung dari panggilan API situs target tanpa parsing HTML manual — berguna kalau sitekey di-render lewat JavaScript, bukan atribut HTML statis:
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 otomatisasi end-to-end
PlaywrightCaptchaSolver menyatukan semua fungsi di atas jadi satu alur: buka browser, deteksi jenis CAPTCHA di halaman, solve lewat CaptchaAI, isi form, submit. Method detect_captcha mengecek pola data-sitekey, penanda cf-turnstile, dan elemen gambar CAPTCHA secara berurutan, jadi Anda tidak perlu tahu jenis CAPTCHA-nya lebih dulu sebelum menjalankan skrip.
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": "user@example.com", "password": "pass123"},
)
print(f"Result: {result}")
finally:
await solver.stop()
asyncio.run(main())
Playwright atau Selenium untuk solve CAPTCHA?
Untuk proyek baru, Playwright unggul di hampir semua sisi — async asli, default browser yang lebih stabil, dan request interception bawaan tanpa proxy tambahan. Selenium masih masuk akal kalau tim Anda sudah punya codebase Selenium besar dan biaya migrasi belum sepadan.
| Fitur | Playwright | Selenium |
|---|---|---|
| Async native | Ya, bawaan | Tidak, perlu threading manual |
| Konfigurasi browser | Default lebih matang | Butuh setup tambahan |
| Kecepatan | Lebih cepat | Load halaman lebih lambat |
| Intersepsi request | Bawaan (page.route) |
Perlu proxy/extension tambahan |
| Multi-browser | Chromium, Firefox, WebKit | Chrome, Firefox, Edge, Safari |
| Gaya API | Promise-based, modern | Imperatif, klasik |
Mengatasi error umum
Error paling sering muncul saat elemen belum termuat atau selector token salah. Tabel ini jadi referensi cepat sebelum Anda mulai debug manual:
| Gejala | Penyebab | Solusi |
|---|---|---|
page.evaluate gagal |
Konten belum termuat | Pakai wait_until="networkidle" |
| Token tidak ke-set | Selector elemen salah | Cek page.content() untuk cari elemen sebenarnya |
| Situs mendeteksi Playwright | Init script belum jalan | Tambahkan override properti driver browser di add_init_script |
networkidle timeout |
Ada polling tanpa henti di halaman | Ganti ke wait_until="domcontentloaded" |
| Screenshot CAPTCHA gambar kosong | Elemen tersembunyi/di luar viewport | Panggil await element.scroll_into_view_if_needed() dulu |
Pertanyaan yang sering diajukan
Playwright atau Selenium, mana yang lebih baik untuk solve CAPTCHA?
Playwright untuk proyek baru — async asli, default browser lebih stabil, dan setup CAPTCHA lebih ringkas. Pertimbangkan pindah dari Selenium hanya kalau ROI migrasi dari basis kode besar yang sudah ada sepadan dengan usahanya.
FunCaptcha di Playwright, didukung CaptchaAI atau belum?
Belum. FunCaptcha (Arkose Labs) tidak didukung CaptchaAI saat ini. Panduan ini fokus ke reCAPTCHA v2/v3, Cloudflare Turnstile, dan CAPTCHA gambar — tipe yang memang didukung penuh.
Berapa lama waktu solve reCAPTCHA v2 dan Turnstile lewat CaptchaAI di Playwright?
Cloudflare Turnstile biasanya selesai di bawah 10 detik, reCAPTCHA v2 di bawah 60 detik pada konkurensi tinggi — keduanya dengan tingkat keberhasilan tinggi pada infrastruktur CaptchaAI sendiri.
Apakah mode headless memengaruhi tingkat keberhasilan solve CaptchaAI?
Tidak. CaptchaAI solve CAPTCHA di infrastrukturnya sendiri, terpisah dari browser Playwright Anda — headless atau headed sama saja untuk hasil solve. Yang perlu diuji terpisah justru apakah situs target mendeteksi mode headless untuk alasan lain di luar CAPTCHA.
Bisakah saya menjalankan beberapa solve CAPTCHA sekaligus dari satu skrip Playwright?
Bisa. Karena solve_captcha async dan aiohttp non-blocking, Anda bisa memicu beberapa task Playwright sekaligus dengan asyncio.gather. Batasnya bukan di kode, tapi di jumlah thread pada paket CaptchaAI Anda — paket BASIC ($15/bulan, 5 thread), misalnya, mendukung 5 solve bersamaan sebelum task berikutnya harus antre.
Ringkasan
Kombinasi Playwright Python + CaptchaAI memberi Anda stack automation CAPTCHA async yang stabil untuk scraping, QA, dan pengujian form berulang. Pakai fungsi individual (solve_captcha, solve_recaptcha_v2_playwright, solve_turnstile_playwright) untuk kasus sederhana, atau langsung pakai class PlaywrightCaptchaSolver kalau Anda butuh alur deteksi-solve-submit lengkap dalam satu skrip produksi. Sebelum dijalankan ke situs produksi milik pihak lain, pastikan Anda memang berwenang mengakses data di target automation tersebut — sejalan dengan UU Pelindungan Data Pribadi (UU 27/2022).