Precisa resolver reCAPTCHA, Cloudflare Turnstile ou CAPTCHA de imagem dentro de um script Playwright sem travar o event loop? A resposta curta: uma função assíncrona baseada em aiohttp conversa com a API da CaptchaAI enquanto o Playwright continua livre para navegar, e o token volta direto para o campo certo do formulário antes do envio. É esse padrão que este guia constrói do zero, com código completo para cada tipo de desafio. Diferente do Selenium, o Playwright já nasce assíncrono — a chamada ao solucionador não bloqueia nenhuma thread enquanto espera a resposta da CaptchaAI, que pode levar de poucos segundos a mais de um minuto dependendo do tipo de CAPTCHA.
Pré-requisitos: Playwright e aiohttp em Python
Instale as duas dependências antes de tudo — o Playwright cuida do navegador, o aiohttp fala com a API da CaptchaAI:
pip install playwright aiohttp
playwright install chromium
Crie a função assíncrona que fala com a CaptchaAI
Essa função é a base de tudo o que vem a seguir: ela envia a tarefa para in.php, faz o polling em res.php até o token ficar pronto e é reaproveitada por cada uma das funções de resolução deste guia.
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")
Configure o navegador Playwright para automação estável
Abra o Chromium com os argumentos que removem os sinais mais óbvios de automação e ajuste o sinal de navegador que identifica a sessão como controlada por script. Isso evita bloqueios genéricos de bot em sites comuns — mas lembre-se: a CaptchaAI resolve o desafio na própria infraestrutura, então essa configuração do navegador não muda a taxa de resolução do CAPTCHA em si, só o acesso à página até chegar nele.
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=[
"--disable-blink-features=AutomationControlled",
],
)
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
Resolva reCAPTCHA v2 no Playwright
Extraia a sitekey do HTML, envie para a CaptchaAI e injete o token de volta no campo oculto do reCAPTCHA — disparando também o callback do widget quando ele existir:
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
Resolva Cloudflare Turnstile no Playwright
O fluxo é o mesmo do reCAPTCHA — só muda o campo de destino do token, cf-turnstile-response em vez de g-recaptcha-response:
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
Resolva CAPTCHA de imagem no Playwright
Para CAPTCHA de imagem, o Playwright tira um screenshot do elemento, converte para base64 e envia direto para a CaptchaAI decodificar:
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
Intercepte requisições de rede para capturar parâmetros do CAPTCHA
O Playwright tem suporte nativo à interceptação de requisições, o que costuma ser mais confiável do que aplicar regex sobre o HTML para achar sitekey ou action: os parâmetros chegam prontos, extraídos direto da chamada de API que o próprio site já faz. Se você registrar esses parâmetros em log, trate a pageurl e qualquer token capturado como dado potencialmente sensível sob a LGPD — evite persisti-los em texto puro sem necessidade real.
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
Classe completa: detecte, resolva e envie automaticamente
Juntando as funções anteriores em uma única classe, PlaywrightCaptchaSolver identifica sozinha qual tipo de CAPTCHA está na página e aplica a resolução certa antes de enviar o formulário:
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=["--disable-blink-features=AutomationControlled"],
)
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 ou Selenium: qual usar para resolver CAPTCHA?
Os dois frameworks funcionam bem com a CaptchaAI — a diferença está na experiência de automação, não na taxa de resolução do CAPTCHA:
| Recurso | Playwright | Selenium |
|---|---|---|
| Nativo assíncrono | Sim | Não (exige threads separadas) |
| Configuração para automação | Padrões prontos | Exige mais configuração manual |
| Velocidade | Menor latência, carregamento mais rápido | Carregamento de página mais lento |
| Interceptação de requisições | Nativa | Requer proxy/extensão |
| Múltiplos navegadores | Chromium, Firefox, WebKit | Chrome, Firefox, Edge, Safari |
| Estilo de API | Baseado em promises, moderno | Imperativo, tradicional |
Solução de problemas comuns no Playwright com CaptchaAI
Os sintomas abaixo aparecem com frequência em quem está adaptando esse fluxo a um site novo:
| Sintoma | Causa | Correção |
|---|---|---|
page.evaluate falha |
Conteúdo não carregado | Use wait_until="networkidle" |
| O token não é aplicado no campo do formulário | Seletor de elemento errado | Inspecione com page.content() para achar o elemento real |
| A página identifica o navegador como automatizado | Script de inicialização ausente | Adicione a substituição da propriedade webdriver do navegador em add_init_script |
Timeout em networkidle |
A página roda scripts com polling infinito | Use wait_until="domcontentloaded" em vez disso |
| A captura de tela da imagem sai em branco | Elemento oculto | Role até o elemento: await element.scroll_into_view_if_needed() |
Perguntas frequentes
Playwright ou Selenium: qual compensa mais para resolver CAPTCHA com a CaptchaAI?
Para projetos novos, o Playwright compensa mais: suporte assíncrono nativo, interceptação de requisições embutida e menos configuração para reduzir sinais óbvios de automação. Vale ficar no Selenium só se você já tem uma base de código grande construída em cima dele — a taxa de resolução em si não muda, porque a CaptchaAI processa o CAPTCHA na própria infraestrutura, fora do navegador.
Quantas páginas do Playwright posso resolver em paralelo?
O limite não é do Playwright, é do seu plano CaptchaAI. O BASIC (US$ 15/mês, 5 threads) já permite 5 resoluções simultâneas; para escalar além disso, o ADVANCE (US$ 90/mês, 50 threads) ou um plano superior libera mais threads em paralelo, com solves ilimitados por thread durante o mês.
Por que o Playwright não encontra a sitekey do reCAPTCHA na página?
Geralmente porque o widget carrega depois do networkidle disparar, ou porque o data-sitekey está em um atributo diferente do que a regex espera. Troque wait_until="networkidle" por page.wait_for_selector(".g-recaptcha, [data-sitekey]") antes de ler o HTML, e confira page.content() para ver o atributo real que o site usa.
hCaptcha funciona com a CaptchaAI no Playwright?
Não. O hCaptcha não é suportado atualmente pela CaptchaAI, independentemente do framework de automação usado — o Playwright só cuida do navegador, quem resolve o desafio é a API. Este guia cobre reCAPTCHA v2, Cloudflare Turnstile e CAPTCHA de imagem, os tipos com disponibilidade geral mais usados em automação com Playwright.
Comece a resolver CAPTCHA com Playwright e a CaptchaAI
Python Playwright + CaptchaAI entrega uma pilha de automação assíncrona completa: PlaywrightCaptchaSolver detecta o tipo de CAPTCHA, resolve e envia o formulário em um único fluxo, com interceptação de requisições e uma configuração de navegador pronta para automação estável.
Pegue sua chave de API em captchaai.com e adapte a classe acima ao primeiro site que você precisa automatizar.