القاعدة العملية هنا واحدة: لا تكتب نوع الكابتشا داخل السكربت، بل اقرأه من HTML الصفحة في كل طلب. إن عثرت على العنصر cf-turnstile فأرسل المهمة بالطريقة turnstile، وإن عثرت على g-recaptcha فأرسلها بالطريقة userrecaptcha، ثم ضع الرمز العائد في الحقل الذي يخصّ ذلك الموفر تحديداً.
ثلاث نقاط فقط تفصل بين المسارين — العلامة داخل الصفحة، ومعامل مفتاح الموقع في الطلب، واسم حقل الاستجابة — ومن يثبّت أحدها في الكود يجد سير العمل متوقفاً عند أول صفحة غيّرت موفّرها دون إشعار.
ما الذي يتغيّر فعلياً بين reCAPTCHA v2 وTurnstile
الفروق محدودة وقابلة للحصر، وهذا بالضبط ما يجعل الكشف التلقائي رخيصاً في التنفيذ:
- العلامة في HTML:
class="g-recaptcha"مقابلclass="cf-turnstile"، وكلاهما يحملdata-sitekeyفي السمة نفسها. - معامل مفتاح الموقع: reCAPTCHA v2 يُرسل مفتاحه في
googlekey، بينما Turnstile يُرسله فيsitekey. - حقل الاستجابة في النموذج:
g-recaptcha-responseمقابلcf-turnstile-response. الرمز الصحيح في الحقل الخطأ يعني رفضاً صامتاً من الخادم. - زمن الحل المتوقع: Turnstile ينتهي عادةً في أقل من 10 ثوانٍ، بينما reCAPTCHA v2 قد يصل إلى أقل من 60 ثانية، فلا تضع مهلة انتهاء واحدة للنوعين.
نقطة مهمة للتشغيل: نقطتا النهاية in.php وres.php هما نفسهما للنوعين، ومفتاح الـ API واحد. ما يتغيّر هو قيمة method والمعاملات المصاحبة لها فقط، وهو ما يجعل دالة كشف واحدة كافية لتغطية المسارين.
مرجع سريع لعلامات الكشف
| الموفر | علامة HTML | عنوان سكربت التحميل | حقل الاستجابة |
|---|---|---|---|
| reCAPTCHA v2 | class="g-recaptcha" |
google.com/recaptcha/api.js |
g-recaptcha-response |
| Cloudflare Turnstile | class="cf-turnstile" |
challenges.cloudflare.com/turnstile |
cf-turnstile-response |
| hCaptcha | class="h-captcha" |
js.hcaptcha.com/1/api.js |
h-captcha-response |
الصف الأخير مذكور للتعريف فقط: hCaptcha ليس ضمن الأنواع المدعومة في CaptchaAI، فإذا ظهرت علامته على الصفحة فالمسار الصحيح هو تسجيل الحالة وإيقاف المحاولة بدل إرسال طلب لن يُنفَّذ.
لماذا يجتمع reCAPTCHA v2 وTurnstile على الموقع نفسه
| السيناريو | كيف يظهر عملياً |
|---|---|
| صفحات مختلفة، فرق مختلفة | تسجيل الدخول reCAPTCHA، وإتمام الشراء Turnstile |
| اختبار A/B بين الموفرين | الرابط نفسه يعرض النوعين بالتناوب بين زيارة وأخرى |
| هجرة غير مكتملة | الصفحات القديمة على reCAPTCHA والجديدة على Turnstile |
| تبديل عند الفشل | تعذّر تحميل الموفر الأساسي فيحل محله الثانوي |
| اختلاف إقليمي | نوع لزوار منطقة ونوع آخر لزوار منطقة تخضع لقواعد خصوصية أشد |
مثال مألوف في السوق العربي: متجر إلكتروني في الرياض يعتمد reCAPTCHA v2 على صفحة تسجيل الدخول منذ سنوات، ثم ينقل فريق البنية التحتية الواجهة الجديدة لصفحة الدفع خلف Cloudflare فتظهر Turnstile هناك وحدها. سكربت اختبار الجودة الذي يفتح المسارين في الجلسة نفسها يمرّ على النوعين خلال أقل من دقيقة، ومن يشغّله بطريقة ثابتة واحدة سيرى نصف الاختبارات الليلية تفشل من دون سبب واضح في السجلات.
Python: كشف الموفر وحلّه في مسار واحد
الترتيب في الكود التالي مقصود: يُفحص Turnstile أولاً لأن علامته أكثر تحديداً، ثم يُفحص reCAPTCHA بثلاثة أنماط — العلامة المباشرة، والترتيب المعكوس للسمات، وأخيراً الاستدعاء grecaptcha.render() للصفحات التي تبني الودجة عبر JavaScript. دالة solve_captcha تستقبل النتيجة وتبني الطلب بالمعاملات المناسبة لكل نوع، ثم تستطلع النتيجة دورياً حتى وصولها.
import requests
import time
import re
from dataclasses import dataclass
API_KEY = "YOUR_API_KEY"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
@dataclass
class CaptchaInfo:
provider: str # "recaptcha" or "turnstile"
method: str # API method name
sitekey: str
pageurl: str
response_field: str # Form field name for the token
def detect_captcha_type(html, pageurl):
"""
Detect which CAPTCHA provider is on the page.
Returns CaptchaInfo or None.
"""
# Check for Turnstile
turnstile_match = re.search(
r'class=["\'][^"\']*cf-turnstile[^"\']*["\'][^>]*data-sitekey=["\']([^"\']+)["\']',
html,
)
if not turnstile_match:
turnstile_match = re.search(
r'data-sitekey=["\']([^"\']+)["\'][^>]*class=["\'][^"\']*cf-turnstile',
html,
)
if turnstile_match:
return CaptchaInfo(
provider="turnstile",
method="turnstile",
sitekey=turnstile_match.group(1),
pageurl=pageurl,
response_field="cf-turnstile-response",
)
# Check for reCAPTCHA
recaptcha_match = re.search(
r'class=["\'][^"\']*g-recaptcha[^"\']*["\'][^>]*data-sitekey=["\']([^"\']+)["\']',
html,
)
if not recaptcha_match:
recaptcha_match = re.search(
r'data-sitekey=["\']([^"\']+)["\'][^>]*class=["\'][^"\']*g-recaptcha',
html,
)
# Also check for script-rendered reCAPTCHA
if not recaptcha_match:
recaptcha_match = re.search(
r'grecaptcha\.render\([^,]+,\s*\{[^}]*["\']sitekey["\']\s*:\s*["\']([^"\']+)["\']',
html,
)
if recaptcha_match:
return CaptchaInfo(
provider="recaptcha",
method="userrecaptcha",
sitekey=recaptcha_match.group(1),
pageurl=pageurl,
response_field="g-recaptcha-response",
)
return None
def solve_captcha(info):
"""Solve any detected CAPTCHA type via CaptchaAI."""
params = {
"key": API_KEY,
"method": info.method,
"json": 1,
}
if info.method == "userrecaptcha":
params["googlekey"] = info.sitekey
params["pageurl"] = info.pageurl
elif info.method == "turnstile":
params["sitekey"] = info.sitekey
params["pageurl"] = info.pageurl
resp = requests.post(SUBMIT_URL, data=params, timeout=30).json()
if resp.get("status") != 1:
raise RuntimeError(f"Submit failed: {resp.get('request')}")
task_id = resp["request"]
for _ in range(60):
time.sleep(5)
poll = requests.get(RESULT_URL, params={
"key": API_KEY, "action": "get",
"id": task_id, "json": 1,
}, timeout=15).json()
if poll.get("request") == "CAPCHA_NOT_READY":
continue
if poll.get("status") == 1:
return poll["request"]
raise RuntimeError(f"Solve failed: {poll.get('request')}")
raise RuntimeError("Timeout")
def process_page(session, url):
"""Fetch page, detect CAPTCHA type, solve, and return form-ready data."""
response = session.get(url)
captcha_info = detect_captcha_type(response.text, url)
if not captcha_info:
print(f"No CAPTCHA detected on {url}")
return None
print(f"Detected {captcha_info.provider} on {url}")
print(f" Sitekey: {captcha_info.sitekey[:30]}...")
token = solve_captcha(captcha_info)
print(f" Solved: {token[:30]}...")
return {
"provider": captcha_info.provider,
"response_field": captcha_info.response_field,
"token": token,
}
# Usage: Handle multiple pages with different providers
session = requests.Session()
pages = [
"https://example.com/login", # Might have reCAPTCHA
"https://example.com/checkout", # Might have Turnstile
]
for url in pages:
result = process_page(session, url)
if result:
form_data = {result["response_field"]: result["token"]}
# Add other form fields...
# session.post(url, data=form_data)
ثلاث تفاصيل تستحق الانتباه قبل نقل هذا الكود إلى بيئة الإنتاج: الحلقة تنتظر 5 ثوانٍ بين كل استفسار ونظيره وتتوقف بعد 60 محاولة، أي مهلة قصوى تقارب خمس دقائق؛ والاستجابة CAPCHA_NOT_READY حالة طبيعية تعني "لم ينتهِ بعد" ولا تستدعي إعادة الإرسال؛ وقيمة response_field تنتقل مع الرمز حتى لحظة بناء بيانات النموذج، فلا يحتاج بقية السكربت إلى معرفة أي نوع كان على الصفحة أصلاً.
JavaScript: الكشف الديناميكي داخل Node.js
المنطق نفسه بأدوات Node.js: تعبيرات نمطية للكشف، ثم URLSearchParams لبناء حمولة الطلب، ثم انتظار غير متزامن للنتيجة. النسخة هنا أقصر لأنها تعتمد على fetch المدمج، وهي مناسبة للتشغيل داخل سكربت أتمتة قائم على Playwright أو Puppeteer حيث تكون HTML الصفحة متاحة أصلاً في الذاكرة.
const API_KEY = "YOUR_API_KEY";
const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";
function detectCaptchaType(html, pageurl) {
// Turnstile
const turnstileMatch = html.match(/cf-turnstile[^>]*data-sitekey=["']([^"']+)["']/);
if (turnstileMatch) {
return { provider: "turnstile", method: "turnstile", sitekey: turnstileMatch[1], pageurl, field: "cf-turnstile-response" };
}
// reCAPTCHA
const recaptchaMatch = html.match(/g-recaptcha[^>]*data-sitekey=["']([^"']+)["']/);
if (recaptchaMatch) {
return { provider: "recaptcha", method: "userrecaptcha", sitekey: recaptchaMatch[1], pageurl, field: "g-recaptcha-response" };
}
// Script-rendered reCAPTCHA
const scriptMatch = html.match(/sitekey["']\s*:\s*["']([^"']+)["']/);
if (scriptMatch) {
return { provider: "recaptcha", method: "userrecaptcha", sitekey: scriptMatch[1], pageurl, field: "g-recaptcha-response" };
}
return null;
}
async function solveCaptcha(info) {
const body = new URLSearchParams({ key: API_KEY, method: info.method, json: "1" });
if (info.method === "userrecaptcha") { body.set("googlekey", info.sitekey); body.set("pageurl", info.pageurl); }
else if (info.method === "turnstile") { body.set("sitekey", info.sitekey); body.set("pageurl", info.pageurl); }
const resp = await (await fetch(SUBMIT_URL, { method: "POST", body })).json();
if (resp.status !== 1) throw new Error(`Submit: ${resp.request}`);
const taskId = resp.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const url = `${RESULT_URL}?key=${API_KEY}&action=get&id=${taskId}&json=1`;
const poll = await (await fetch(url)).json();
if (poll.request === "CAPCHA_NOT_READY") continue;
if (poll.status === 1) return poll.request;
throw new Error(`Solve: ${poll.request}`);
}
throw new Error("Timeout");
}
async function processPage(url) {
const response = await fetch(url);
const html = await response.text();
const info = detectCaptchaType(html, url);
if (!info) { console.log(`No CAPTCHA on ${url}`); return null; }
console.log(`${info.provider} detected on ${url}`);
const token = await solveCaptcha(info);
return { provider: info.provider, field: info.field, token };
}
// Usage
const pages = ["https://example.com/login", "https://example.com/checkout"];
for (const url of pages) {
const result = await processPage(url);
if (result) {
console.log(`Solved ${result.provider}: ${result.token.substring(0, 30)}...`);
}
}
أعطال متكررة وسببها الحقيقي
| العطل | السبب الأرجح | الإجراء |
|---|---|---|
| كُشف النوع الخطأ | التعبير النمطي التقط data-sitekey وحده دون علامة الموفر |
ابحث عن اسم الفئة أولاً — cf-turnstile أو g-recaptcha — ثم استخرج المفتاح |
| الرمز صحيح لكن النموذج يرفضه | الرمز وُضع في حقل الموفر الآخر | طابق اسم الحقل مع النوع المكتشف: g-recaptcha-response أو cf-turnstile-response |
| النوع يتبدّل بين زيارة وأخرى | اختبار A/B أو اختيار قائم على الموقع الجغرافي | أعد الكشف مع كل طلب صفحة، ولا تخزّن الموفر من الزيارة الأولى |
| العلامتان موجودتان في الصفحة نفسها | إحدى الودجتين مخفية أو معطّلة | تحقق من ظهور العنصر فعلياً وحلّ الودجة المرئية فقط |
| الكشف يفشل رغم وجود كابتشا | الودجة تُبنى عبر JavaScript ولا أثر لها في HTML الأولي | ابحث عن استدعاءات grecaptcha.render() أو turnstile.render() داخل السكربتات |
| الاختبار ينجح والإنتاج يفشل | الجلسة أو الرؤوس أو الخادم الوسيط مختلف بين البيئتين | سجّل نوع الموفر ووقت الحل ورمز الخطأ في السطر نفسه لتتبّع الفرق |
الرصيد وعدد الـ threads عند التعامل مع موفرَين
الفاتورة في CaptchaAI مرتبطة بعدد الـ threads المتزامنة لا بعدد عمليات الحل، ولا فرق في السعر بين حل reCAPTCHA v2 وحل Turnstile. الـ thread وحدة تزامن: مهمة واحدة قيد التنفيذ، وحين تنتهي يلتقط الـ thread المهمة التالية مباشرة، وعدد عمليات الحل داخل الاشتراك الشهري غير محدود.
عملياً هذا يعني أن سكربت اختبار يفتح صفحتين بموفرين مختلفين بالتتابع لا يحتاج إلا thread واحداً. خطة BASIC بسعر $15 شهرياً تمنح 5 threads وتكفي لدورة اختبار قبول ليلية لفريق صغير، وخطة STANDARD بسعر $30 شهرياً ترفع الرقم إلى 15 thread عند تشغيل عدة مسارات بالتوازي، بينما تناسب خطة ADVANCE بسعر $90 شهرياً بـ 50 thread الفرق التي تراقب عشرات المسارات على مدار اليوم. القياس الصحيح هنا هو أعلى عدد طلبات متزامنة في ذروة التشغيل، لا مجموع الطلبات اليومي.
الأسئلة الشائعة
كم عدد الـ threads الذي أحتاجه إذا كان الموقع يعرض نوعين؟
العدد يتحدد بالتزامن لا بعدد الأنواع. مسار يفتح تسجيل الدخول ثم الدفع بالتتابع يستهلك thread واحداً في كل لحظة، بينما خمسة عمّال متوازيين يحتاجون خمسة threads بغض النظر عن كون الكابتشا reCAPTCHA v2 أو Turnstile.
لماذا يُرفض الإرسال رغم أن الحل نجح؟
في أغلب الحالات يكون الرمز قد وُضع في حقل الموفر الآخر. تحقق من أن response_field المرافق للنتيجة هو نفسه المستخدم في بيانات النموذج، ثم تأكد من أن الإرسال يتم عبر الجلسة نفسها التي جُلبت منها الصفحة وقبل انتهاء صلاحية الرمز.
هل يختلف زمن الحل بين النوعين؟
نعم، وبفارق ملموس: Turnstile ينتهي عادةً في أقل من 10 ثوانٍ، بينما reCAPTCHA v2 قد يمتد إلى أقل من 60 ثانية. اجعل مهلة الانتظار مرتبطة بالنوع المكتشف، وإلا ستقطع مهلة قصيرة موحّدة مهام reCAPTCHA سليمة قبل اكتمالها.
ماذا أفعل إذا ظهر hCaptcha بدل أحد النوعين؟
CaptchaAI لا يدعم hCaptcha ولا FunCaptcha، أما GeeTest v4 فمدرج بوصفه قادماً قريباً وليس متاحاً بعد. المعالجة الصحيحة أن يسجّل السكربت الحالة وينهي المسار برسالة واضحة بدل الدخول في حلقة إعادة محاولة لا تنتهي. أما GeeTest v3 وCloudflare Challenge فمدعومان ويمكن إضافتهما إلى دالة الكشف بالمنطق نفسه.