Короткий ответ: CaptchaAI подключается к Playwright тремя вызовами HTTP-клиента — отправить задачу на in.php, опросить res.php, подставить токен в форму. Playwright изначально построен на async/await, запускает Chromium, Firefox и WebKit из одной кодовой базы и требует меньше ручной донастройки под автоматизацию, чем более старый Selenium. Для команд, которые уже мигрировали свои QA-скрипты на Playwright, интеграция с CaptchaAI сводится к нескольким переиспользуемым функциям, а не к отдельному сервису.
В этом руководстве вы получите:
- рабочий асинхронный клиент CaptchaAI на aiohttp, который переиспользуется во всех сценариях ниже;
- готовые функции для reCAPTCHA v2, Cloudflare Turnstile и графической CAPTCHA в Playwright;
- класс
PlaywrightCaptchaSolver, который сам определяет тип CAPTCHA, решает её и отправляет форму; - таблицу типичных ошибок интеграции и разбор частых вопросов по потокам и таймаутам.
Что понадобится перед стартом
Установите Playwright и асинхронный HTTP-клиент, затем скачайте бинарник Chromium:
pip install playwright aiohttp
playwright install chromium
Асинхронный клиент CaptchaAI на aiohttp
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")
Функция solve_captcha — переиспользуемый фрагмент: принимает method (userrecaptcha, turnstile, base64 и т.д.), отправляет задачу на in.php и опрашивает res.php каждые 5 секунд до 30 попыток. Её вызывают все сценарии ниже.
Настройка браузера Playwright для стабильной автоматизации
Явные настройки контекста и user-agent делают поведение скрипта предсказуемым между прогонами:
from playwright.async_api import async_playwright
async def create_browser():
"""Launch Playwright browser with стандартная конфигурация браузера-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
--disable-blink-features=AutomationControlled убирает частый сигнал автоматизации, а add_init_script переопределяет navigator.webdriver до выполнения кода страницы.
Решаем reCAPTCHA v2 в Playwright
Сценарий находит sitekey в DOM, получает токен от CaptchaAI и вызывает JS-callback вручную, если он есть на странице:
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
Решаем Cloudflare Turnstile в Playwright
Turnstile прячет sitekey то в data-sitekey, то в инлайн-скрипте — код проверяет оба варианта:
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
Решаем графическую CAPTCHA в Playwright
Если CAPTCHA — картинка с искажённым текстом, проще всего сделать скриншот и отправить в CaptchaAI как 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
Перехват сетевых запросов для отладки CAPTCHA
Полезно, когда sitekey не лежит в HTML открыто, а приходит внутренним вызовом страницы — проще поймать его в трафике, чем парсить DOM:
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
Класс PlaywrightCaptchaSolver: собираем всё в один пайплайн
В реальном проекте эти три функции удобнее собрать в один класс, который сам определяет тип CAPTCHA, решает её и отправляет форму:
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": "[email protected]", "password": "pass123"},
)
print(f"Result: {result}")
finally:
await solver.stop()
asyncio.run(main())
Playwright или Selenium: что выбрать для решения CAPTCHA
| Критерий | Playwright | Selenium |
|---|---|---|
| Нативная асинхронность | Да | Нет (нужны потоки) |
| Готовность к автоматизации из коробки | Меньше донастройки | Требует больше ручной конфигурации |
| Скорость | Быстрее | Медленнее грузит страницы |
| Перехват запросов | Встроенный | Нужен прокси или расширение |
| Поддержка браузеров | Chromium, Firefox, WebKit | Chrome, Firefox, Edge, Safari |
| Стиль API | На промисах, современный | Императивный, классический |
Для CaptchaAI разницы нет: вызов in.php/res.php одинаков в обоих случаях.
Типичные проблемы и их решения
page.evaluateне срабатывает. Контент страницы ещё не загружен — дождитесьwait_until="networkidle"перед вызовом.- Токен не подставляется в форму. Обычно неверный селектор элемента — проверьте реальную разметку через
page.content(), а не через инструменты разработчика в обычном браузере. - Сайт распознаёт автоматизированный запуск. Не подключён
add_init_script— добавьте переопределениеnavigator.webdriverпри создании контекста, как в разделе выше. - Таймаут на
wait_until="networkidle". Страница держит открытые соединения из-за фоновых polling-скриптов — используйтеwait_until="domcontentloaded"вместоnetworkidle. - Скриншот CAPTCHA получается пустым. Элемент находится вне видимой области — прокрутите к нему:
await element.scroll_into_view_if_needed().
Часто задаваемые вопросы
Playwright или Selenium — что выбрать для решения CAPTCHA?
Для новых проектов — Playwright: выше производительность, нативный async/await и меньше донастройки браузера под автоматизацию. Selenium имеет смысл, если у вас уже есть рабочая кодовая база на нём и переписывать её нецелесообразно.
Сколько потоков CaptchaAI нужно для стабильного парсинга через Playwright?
Зависит от параллелизма ваших браузерных контекстов: один поток CaptchaAI обслуживает одну CAPTCHA одновременно, а после решения сразу освобождается под следующую. Для нескольких параллельных сессий Playwright (частый сценарий для QA-команд из СНГ, тестирующих staging в европейских регионах) обычно достаточно тарифа ADVANCE ($90/мес, 50 потоков); для крупного парсинга — CORPORATE ($240/мес, 150 потоков) с неограниченным числом решений на поток.
Playwright не находит sitekey Turnstile или reCAPTCHA — что делать?
Проверьте, не грузится ли виджет лениво после networkidle — тогда page.content() ещё не содержит data-sitekey. Добавьте page.wait_for_selector на контейнер виджета либо перехватите запрос к google.com/recaptcha, как показано выше.
Влияет ли headless-режим Playwright на решение CAPTCHA через CaptchaAI?
Нет: CaptchaAI решает CAPTCHA на собственной инфраструктуре независимо от того, в каком режиме запущен ваш браузер. headless=True ускоряет прогон в CI, но некоторые сайты по-разному ведут себя в headless и headed режимах — стоит протестировать оба варианта, если форма после решения CAPTCHA всё равно не отправляется.
Можно ли использовать прокси вместе с Playwright при парсинге через CaptchaAI?
Да, прокси задаётся при создании контекста браузера (browser.new_context(proxy=...)) и не влияет на работу CaptchaAI — сама CAPTCHA решается на стороне API независимо от того, откуда пришёл запрос браузера. Для распределённых QA-стендов из разных регионов это обычная практика: прокси нужен для стабильности сетевого соединения, а не для решения CAPTCHA.
Итоги
Python Playwright + CaptchaAI — рабочий стек для асинхронной автоматизации CAPTCHA: один клиент на aiohttp закрывает reCAPTCHA v2, Cloudflare Turnstile и графические CAPTCHA, а класс PlaywrightCaptchaSolver объединяет определение типа, решение и отправку формы в один вызов. Перехват сетевых запросов и явная настройка контекста браузера снимают большинство проблем с поиском sitekey и ложными срабатываниями антибот-проверок.