لحلّ أي اختبار CAPTCHA داخل سكربت Playwright تحتاج إلى أربع خطوات واضحة: التقط مفتاح الموقع من الصفحة، أرسل المهمة إلى CaptchaAI، استطلع النتيجة حتى تجهز، ثم احقن الرمز الناتج في الصفحة وأرسل النموذج. الميزة الأكبر هنا أن Playwright غير متزامن (async) في جوهره، فتشغّل عملية الحل جنباً إلى جنب مع بقية خطوات الأتمتة دون تعطيل الخيط الرئيسي. يشرح هذا الدليل كل خطوة بأمثلة Python قابلة للنسخ المباشر، ويغطي reCAPTCHA v2 وCloudflare Turnstile وصور CAPTCHA، إضافة إلى فئة جاهزة تكتشف نوع التحقق وتحلّه وترسله تلقائياً.
المتطلبات الأساسية
قبل أي شيء، ثبّت مكتبة Playwright ومكتبة aiohttp التي سنستخدمها لإرسال الطلبات غير المتزامنة إلى CaptchaAI. الأمر الأول يجهّز الحزم، والثاني ينزّل متصفح Chromium الذي سيقود الأتمتة:
pip install playwright aiohttp
playwright install chromium
بناء دالة الحل غير المتزامنة
تتبع دالة الحل نمطاً بسيطاً: الإرسال ثم الاستطلاع الدوري. نرسل المهمة إلى نقطة النهاية in.php مع مفتاح الـ API واسم الطريقة وأي معلمات خاصة بنوع التحقق، فتعيد لنا معرّف المهمة. بعد ذلك نستفسر عن النتيجة عبر res.php كل خمس ثوانٍ حتى تجهز. الحد الأقصى ثلاثون محاولة، أي مهلة تقارب 150 ثانية وهي كافية لأبطأ الأنواع. لاحظ التعامل الصريح مع رمز الخطأ ERROR_CAPTCHA_UNSOLVABLE حتى لا يعلّق السكربت على تحدٍّ تعذّر حله:
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")
لأن الدالة غير متزامنة بالكامل، يمكن للسكربت أن ينتظر النتيجة دون أن يجمّد بقية صفحات Playwright التي قد تعمل بالتوازي في المشروع نفسه.
تهيئة متصفح Playwright لأتمتة موثوقة
تضبط هذه الدالة المتصفح بإعدادات أقرب إلى متصفح مستخدم حقيقي: وكيل مستخدم واضح، أبعاد نافذة كاملة، ولغة محددة. كما تزيل خاصية navigator.webdriver عبر سكربت تهيئة مبكر، وهي الخاصية التي تكشف أدوات الأتمتة وتتسبب أحياناً في حالات حظر كاذبة أثناء الاختبار. الهدف هنا استقرار الأتمتة لا أكثر:
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
حلّ reCAPTCHA v2 داخل Playwright
هذا هو التدفق الكامل لأكثر الأنواع شيوعاً. نستخرج مفتاح الموقع من محتوى الصفحة عبر تعبير نمطي، ثم نرسله إلى CaptchaAI بالطريقة userrecaptcha مع عنوان الصفحة. عند وصول الرمز نحقنه في الحقل g-recaptcha-response، ونشغّل دالة رد النداء (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 قد يظهر في السمة data-sitekey أو داخل كائن إعدادات JavaScript، لذا نجرّب نمطين للاستخراج. بعد الحصول على الرمز نحقنه في كل الحقول المخفية التي تحمل الاسم cf-turnstile-response، لأن بعض الصفحات تكرّر الحقل أكثر من مرة. Turnstile من الأنواع سريعة الحل عادةً، ما يجعله مناسباً لتدفقات تسجيل الدخول والنماذج:
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
مثال تطبيقي: اختبار تسجيل الدخول في متجر إقليمي
تخيّل فريق ضمان الجودة في متجر إلكتروني في الخليج أو منصّة حجوزات في مصر يريد تشغيل اختبار تلقائي ليلي على صفحة تسجيل الدخول المحمية بـ Cloudflare Turnstile. عند فتح عشرات الصفحات بالتوازي في Playwright، يصبح عدد عمليات الحل المتزامنة هو العامل الحاسم في التكلفة. لأن CaptchaAI يفوتر حسب عدد الـ Threads المتزامنة مع عمليات حل غير محدودة لكل Thread، تصبح الكلفة الشهرية ثابتة ومتوقعة بدل الدفع لكل عملية. فريق يشغّل انحداراً ليلياً متوسط الحجم قد تكفيه خطة ADVANCE ($90 شهرياً، 50 Thread)، بينما تناسب الأحمال الأكبر خطة PREMIUM ($170 شهرياً، 100 Thread). كل الاستخدام هنا في إطار اختبار جودة معتمد على بيئة يملكها الفريق.
حلّ صور CAPTCHA (OCR) في Playwright
بالنسبة إلى صور CAPTCHA الظاهرة على الصفحة، نلتقط لقطة شاشة للعنصر ونحوّلها إلى 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
اعتراض طلبات الشبكة لاستخراج المعلمات
يتفوّق Playwright في اعتراض الطلبات، وهو أسلوب مفيد حين لا يكون مفتاح الموقع ظاهراً في HTML مباشرة بل يمرّ عبر استدعاء API. نراقب كل الطلبات ونلتقط معلمات reCAPTCHA أو Turnstile منها:
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
فئة أتمتة كاملة: اكتشف، حُلّ، أرسل
تجمع الفئة التالية كل ما سبق في مكوّن واحد قابل لإعادة الاستخدام. الدالة detect_captcha تحدد نوع التحقق الموجود في الصفحة، وsolve_and_submit تنفّذ التدفق الكامل من التنقل إلى الإرسال، بينما تتولى _api_solve التخاطب مع CaptchaAI. هكذا يكفيك سطر واحد لتشغيل دورة الكشف والحل والإرسال على أي صفحة:
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://example.com/login",
form_data={"email": "user@example.com", "password": "pass123"},
)
print(f"Result: {result}")
finally:
await solver.stop()
asyncio.run(main())
Playwright مقابل Selenium في حل CAPTCHA
كلاهما يقود المتصفح، لكن Playwright يقدّم دعماً غير متزامن أصلياً وإعدادات افتراضية أنسب للأتمتة الحديثة. الجدول التالي يوضح الفروق العملية عند دمج حل الكابتشا:
| الميزة | Playwright | Selenium |
|---|---|---|
| دعم async أصلي | نعم | لا (يتطلب خيوطاً) |
| ضبط تقليل إشارات الأتمتة | افتراضيات أفضل | يتطلب تكويناً إضافياً |
| السرعة | أسرع | تحميل أبطأ للصفحة |
| اعتراض الطلبات | مدمج | يتطلب وسيطاً أو إضافة |
| تعدد المتصفحات | Chromium وFirefox وWebKit | Chrome وFirefox وEdge وSafari |
| نمط الواجهة | حديث قائم على الوعود | تقليدي حتمي |
معالجة الأخطاء الشائعة
معظم مشكلات التكامل ترجع إلى توقيت تحميل الصفحة أو محددات عناصر خاطئة. الجدول يلخّص الأعراض المتكررة وحلولها:
| العَرَض | السبب | الإصلاح |
|---|---|---|
فشل page.evaluate |
لم يكتمل تحميل المحتوى | استخدم wait_until="networkidle" |
| حقن الرمز لا يعمل | محدد عنصر خاطئ | افحص الصفحة عبر page.content() لإيجاد العنصر الفعلي |
| كشف أدوات الأتمتة | سكربت التهيئة مفقود | أضف تجاوز webdriver في add_init_script |
مهلة على networkidle |
سكربتات استطلاع دوري لا تتوقف | استخدم wait_until="domcontentloaded" بدلاً منها |
| لقطة الصورة فارغة | العنصر مخفي | مرّره إلى العرض عبر await element.scroll_into_view_if_needed() |
الأسئلة الشائعة
كيف أتعامل مع reCAPTCHA الموجود داخل iframe؟
لا تحتاج غالباً إلى الدخول إلى الـ iframe يدوياً. مفتاح الموقع يظهر عادةً في HTML الصفحة الرئيسية، فتستخرجه بتعبير نمطي وترسله إلى CaptchaAI، ثم تحقن الرمز في الحقل g-recaptcha-response على الصفحة الأم. إن لم يظهر المفتاح، استخدم اعتراض الطلبات لالتقاطه من استدعاءات الشبكة.
ما تكلفة تشغيل الحل على نطاق واسع مع Playwright؟
يفوتر CaptchaAI حسب عدد الـ Threads المتزامنة، مع عمليات حل غير محدودة لكل Thread خلال الشهر. لذا يتحدد اختيار الخطة بعدد الصفحات التي تعالجها بالتوازي لا بإجمالي عدد الحلول. تبدأ الخطط من BASIC ($15 شهرياً، 5 Threads) وتصعد حتى VIP-3 ($7,500 شهرياً، 5,000 Thread)، ما يجعل الكلفة الشهرية متوقعة بدل الدفع لكل عملية.
لماذا يفشل حقن التوكن أحياناً رغم نجاح الحل؟
السبب الأكثر شيوعاً أن الموقع لا يكتفي بقيمة الحقل بل ينتظر استدعاء دالة رد النداء (callback) لتفعيل الإرسال. تأكد من تشغيل الـ callback بعد الحقن كما في مثال reCAPTCHA v2، وتحقق من أن اسم الحقل مطابق تماماً للصفحة المستهدفة.
هل يدعم CaptchaAI كل أنواع CAPTCHA التي قد أواجهها؟
يحلّ CaptchaAI reCAPTCHA v2/v3 وCloudflare Turnstile وTurnstile Challenge وGeeTest v3 وصور OCR والشبكات. أما hCaptcha وFunCaptcha فغير مدعومة حالياً، وGeeTest v4 قيد الإضافة قريباً وليست متاحة بعد، لذا خطّط لهذه الأنواع بحل بديل.
كيف أضيف إعادة المحاولة والتعامل مع المهلات؟
غلّف استدعاء solve_captcha بحلقة إعادة محاولة مع تراجع أسي بين المحاولات، والتقط استثناءات المهلة وERROR_CAPTCHA_UNSOLVABLE بشكل منفصل. هكذا يتعافى السكربت من الأخطاء العابرة دون أن يتوقف تدفق الأتمتة بالكامل.
الخلاصة
يمنحك Playwright بايثون مع CaptchaAI مكدّس أتمتة غير متزامن حديث لحل الكابتشا. استخدم الفئة PlaywrightCaptchaSolver لتدفق الكشف والحل والإرسال الكامل، مع الدعم غير المتزامن الأصلي واعتراض الطلبات وإعدادات متصفح مستقرة للاختبار المعتمد.
مقالات ذات صلة
- مقارنة GeeTest وCloudflare Turnstile
- تكامل CaptchaAI مع Google Cloud Functions
- دمج CaptchaAI مع Crawlee لاستخراج بيانات حديث