الدروس التطبيقية

حل Cloudflare Turnstile في Python بمكتبة requests وCaptchaAI

ثلاثة طلبات HTTP تفصل سكربت Python عن رمز Turnstile صالح: اقرأ مفتاح الموقع من صفحة الهدف، أرسِل مهمة الحل إلى in.php، ثم استطلع res.php حتى تعود قيمة الرمز. بعدها تمرّر تلك القيمة في حقل cf-turnstile-response ضمن بيانات النموذج، وينتهي المسار.

الخبر الجيد أنك لا تحتاج متصفحاً كاملاً في أغلب الحالات. مكتبة requests وحدها تكفي ما دام وسم Turnstile موجوداً في HTML الأولي للصفحة، وهذا هو الوضع الشائع في نماذج التسجيل وتسجيل الدخول وإرسال الطلبات.

يغطي هذا الدليل المسار كاملاً: الاستخراج، الإرسال، الاستطلاع، ثم صنف جاهز للإنتاج وجدول أعطال وحساب سريع لعدد الـ threads.


ما الذي يختلف في Turnstile عن reCAPTCHA داخل الكود

قبل كتابة أي سطر، أربع نقاط تختصر الفروق العملية:

  • اسم حقل الاستجابة هو cf-turnstile-response، وليس أي اسم آخر إلا إذا أعاد الموقع تسميته صراحةً في HTML.
  • الأوضاع الثلاثة — managed وnon-interactive وinvisible — تُحلّ جميعها باستدعاء API واحد؛ لا تحتاج معلمة إضافية لتمييزها.
  • وقت حل Cloudflare Turnstile لدى CaptchaAI يقلّ عادةً عن 10 ثوانٍ، أي أسرع من reCAPTCHA v2 بفارق واضح.
  • الرمز قصير العمر ويُستهلك عند أول تحقق على الخادم، لذا عامله كقيمة تُستخدم فوراً ولا تُخزَّن.

ما تحتاجه قبل أول سطر

pip install requests

قائمة قصيرة قبل البدء:

  • مفتاح CaptchaAI API من captchaai.com — إن كانت هذه أول مرة، ابدأ من دليل البدء السريع.
  • عنوان URL للصفحة التي تحمل الودجت.
  • مفتاح الموقع الخاص بـ Turnstile، وهو ما تستخرجه في الخطوة التالية.

الخطوة 1: استخرج مفتاح الموقع من HTML

مفتاح الموقع يظهر عادةً في السمة data-sitekey داخل عنصر div يحمل الصنف cf-turnstile، وتبدأ قيمته بـ 0x. بعض المواقع تمرّره بدل ذلك كخيار داخل استدعاء JavaScript، لذا يجرّب الكود التالي ثلاثة أنماط بحث قبل أن يستسلم.

import re
import requests

def extract_turnstile_sitekey(url):
    """Extract Cloudflare Turnstile sitekey from page HTML."""
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                      "AppleWebKit/537.36 Chrome/120.0.0.0",
        "Accept": "text/html,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.9",
    }
    response = requests.get(url, headers=headers, timeout=15)

    patterns = [
        r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']',
        r"sitekey\s*:\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]",
        r"siteKey\s*[=:]\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]",
    ]

    for pattern in patterns:
        match = re.search(pattern, response.text)
        if match:
            return match.group(1)

    return None


sitekey = extract_turnstile_sitekey("https://example.com/signup")
print(f"Sitekey: {sitekey}")

إن عادت النتيجة None، فالودجت غالباً يُحقن بعد تحميل الصفحة، وهذه حالة تحتاج متصفحاً فعلياً لا مكتبة requests.


الخطوة 2: أرسِل المهمة إلى CaptchaAI

الإرسال طلب POST واحد إلى in.php يحمل مفتاح الـ API ونوع المهمة turnstile ومفتاح الموقع وعنوان الصفحة. مع json=1 تعود الاستجابة بصيغة JSON، وتكون status بقيمة 1 عند القبول، بينما يحمل الحقل request معرّف المهمة الذي تستطلع به لاحقاً.

import requests

API_KEY = "YOUR_API_KEY"

def submit_turnstile(sitekey, page_url):
    """Submit Turnstile solving task to CaptchaAI."""
    response = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "turnstile",
        "sitekey": sitekey,
        "pageurl": page_url,
        "json": 1,
    })

    data = response.json()

    if data.get("status") != 1:
        raise Exception(f"Submit failed: {data.get('request')}")

    return data["request"]


task_id = submit_turnstile("0x4AAAAAAAC3DHQhMMQ_Rxrg", "https://example.com/signup")
print(f"Task ID: {task_id}")

قيم الفشل واضحة الدلالة:

  • ERROR_WRONG_USER_KEY — مفتاح API خاطئ.
  • ERROR_ZERO_BALANCE — رصيد منتهٍ.

لا فائدة من إعادة المحاولة مع أيٍّ منهما.


الخطوة 3: استطلع النتيجة حتى يعود الرمز

دورة الاستطلاع الدوري تتلخّص في ثلاث خطوات:

  1. انتظر خمس ثوانٍ قبل أول سؤال.
  2. اسأل res.php عن الحالة وكرّر حتى تعود status بقيمة 1.
  3. أوقف الدورة عند مهلة عليا، وعامل ERROR_CAPTCHA_UNSOLVABLE كفشل نهائي لا كخطأ مؤقت.
import time

def poll_result(task_id, timeout=120):
    """Poll CaptchaAI for the solved Turnstile token."""
    start = time.time()

    while time.time() - start < timeout:
        time.sleep(5)

        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY,
            "action": "get",
            "id": task_id,
            "json": 1,
        }).json()

        if result.get("status") == 1:
            return result["request"]

        if result.get("request") == "ERROR_CAPTCHA_UNSOLVABLE":
            raise Exception("Turnstile could not be solved")

    raise TimeoutError("Solve timed out")


token = poll_result(task_id)
print(f"Token: {token[:50]}...")

السكربت الكامل: من فتح الصفحة إلى إرسال النموذج

هنا تجتمع الخطوات الثلاث في مسار واحد، وتحكمه قاعدتان:

  • استخدم Session واحدة: نفس الترويسات وملفات تعريف الارتباط في قراءة الصفحة وفي إرسال النموذج.
  • لا تبدّل بصمة الطلب بين الخطوتين؛ اختلافها سبب شائع لرفض النموذج رغم صحة الرمز.
import re
import time
import requests

API_KEY = "YOUR_API_KEY"
TARGET_URL = "https://example.com/signup"


def solve_turnstile(sitekey, page_url):
    """Full Turnstile solve: submit + poll."""
    # Submit
    submit = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "turnstile",
        "sitekey": sitekey,
        "pageurl": page_url,
        "json": 1,
    })

    data = submit.json()
    if data.get("status") != 1:
        raise Exception(f"Submit error: {data.get('request')}")

    task_id = data["request"]
    print(f"Task submitted: {task_id}")

    # Poll
    for _ in range(30):
        time.sleep(5)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY,
            "action": "get",
            "id": task_id,
            "json": 1,
        }).json()

        if result.get("status") == 1:
            return result["request"]

    raise TimeoutError("Solve timed out")


# --- Main flow ---
session = requests.Session()
session.headers.update({
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                  "AppleWebKit/537.36 Chrome/120.0.0.0",
    "Accept": "text/html,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.9",
})

# 1. Get page and extract sitekey
response = session.get(TARGET_URL, timeout=15)
match = re.search(r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', response.text)
if not match:
    raise ValueError("Turnstile sitekey not found")
sitekey = match.group(1)
print(f"Sitekey: {sitekey}")

# 2. Solve Turnstile
token = solve_turnstile(sitekey, TARGET_URL)
print(f"Token: {token[:50]}...")

# 3. Submit form with token
form_response = session.post(TARGET_URL, data={
    "cf-turnstile-response": token,
    "email": "[email protected]",
    "password": "SecurePass123",
})
print(f"Form status: {form_response.status_code}")

عندما يطلب النموذج معلمة action

تضيف بعض المواقع السمة data-action إلى الودجت وتتحقق من قيمتها على الخادم. إن وُجدت، مرّرها كما هي في طلب الإرسال؛ إغفالها ينتج رمزاً صحيح الشكل يرفضه الخادم.

def solve_turnstile_with_action(sitekey, page_url, action):
    """Solve Turnstile that requires an action parameter."""
    submit = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "turnstile",
        "sitekey": sitekey,
        "pageurl": page_url,
        "action": action,  # Include the action from data-action attribute
        "json": 1,
    })

    data = submit.json()
    if data.get("status") != 1:
        raise Exception(f"Submit error: {data.get('request')}")

    task_id = data["request"]

    for _ in range(30):
        time.sleep(5)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY,
            "action": "get",
            "id": task_id,
            "json": 1,
        }).json()

        if result.get("status") == 1:
            return result["request"]

    raise TimeoutError("Solve timed out")

ثلاث طرق لتمرير الرمز إلى الخادم

الحل جزء واحد من المهمة، والتسليم هو الجزء الذي يفشل فيه أغلب السكربتات — افتح HTML النموذج وحدّد أي نمط من الثلاثة يتبعه الموقع.

النمط 1: نموذج POST تقليدي

# Most common — Turnstile uses cf-turnstile-response field
response = session.post(form_url, data={
    "cf-turnstile-response": token,
    "email": "[email protected]",
})

النمط 2: واجهة JSON

response = session.post(api_url, json={
    "turnstileToken": token,
    "email": "[email protected]",
})

النمط 3: حقل باسم مخصص

# Some sites rename the field — check the form HTML
response = session.post(form_url, data={
    "cf-turnstile-response": token,
    "captcha_token": token,  # Custom duplicate field
    "action": "signup",
})

صنف جاهز للإنتاج مع إعادة المحاولة

السكربت التعليمي يكفي للتجربة، أما التشغيل اليومي فيحتاج منطق إعادة محاولة يميّز بين خطأ عابر وخطأ لا يُصلحه التكرار. الصنف التالي يعيد المحاولة عند انتهاء المهلة، ويتوقف فوراً عند أخطاء الرصيد أو مفتاح الـ API.

import re
import time
import requests

class TurnstileSolver:
    """Production-ready Turnstile solver with retry logic."""

    API_URL = "https://ocr.captchaai.com"

    def __init__(self, api_key, max_retries=3):
        self.api_key = api_key
        self.max_retries = max_retries

    def extract_sitekey(self, session, url):
        """Extract Turnstile sitekey from page."""
        response = session.get(url, timeout=15)
        match = re.search(
            r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', response.text
        )
        return match.group(1) if match else None

    def solve(self, sitekey, page_url, action=None):
        """Solve Turnstile with retry logic. Returns token string."""
        for attempt in range(1, self.max_retries + 1):
            try:
                token = self._solve_once(sitekey, page_url, action)
                return token
            except TimeoutError:
                print(f"Attempt {attempt} timed out")
            except Exception as e:
                error_str = str(e)
                if "ERROR_ZERO_BALANCE" in error_str:
                    raise  # Don't retry billing errors
                if "ERROR_WRONG_USER_KEY" in error_str:
                    raise
                print(f"Attempt {attempt} failed: {e}")

        raise Exception(f"Failed after {self.max_retries} attempts")

    def _solve_once(self, sitekey, page_url, action=None):
        """Single solve attempt."""
        params = {
            "key": self.api_key,
            "method": "turnstile",
            "sitekey": sitekey,
            "pageurl": page_url,
            "json": 1,
        }
        if action:
            params["action"] = action

        submit = requests.post(f"{self.API_URL}/in.php", data=params, timeout=30)
        submit.raise_for_status()
        data = submit.json()

        if data.get("status") != 1:
            raise Exception(f"Submit error: {data.get('request')}")

        task_id = data["request"]

        for _ in range(30):
            time.sleep(5)
            result = requests.get(f"{self.API_URL}/res.php", params={
                "key": self.api_key,
                "action": "get",
                "id": task_id,
                "json": 1,
            }, timeout=30).json()

            if result.get("status") == 1:
                return result["request"]
            if result.get("request") == "ERROR_CAPTCHA_UNSOLVABLE":
                raise Exception("CAPTCHA unsolvable")

        raise TimeoutError("Poll timed out")


# Usage
solver = TurnstileSolver("YOUR_API_KEY")
token = solver.solve("0x4AAAAAAAC3DHQhMMQ_Rxrg", "https://example.com/signup")

سيناريو تشغيلي: اختبار نموذج التسجيل قبل موسم الذروة

تخيّل فريق QA في متجر إلكتروني بالرياض يستعد لموسم الجمعة البيضاء. النموذج الذي يملكه الفريق محمي بـ Turnstile، ويحتاج قبل الإطلاق تشغيل نحو 300 حالة اختبار على بيئة staging للتأكد من أن التسجيل والدفع يعملان تحت ضغط.

الحساب مباشر: بزمن حل يقلّ عن 10 ثوانٍ، يُنجز الـ thread الواحد نحو ستّ عمليات في الدقيقة. باقة BASIC — ‎$15 شهرياً مع 5 threads — تعطي نحو 30 عملية في الدقيقة نظرياً، أي إن الدفعة كاملة تنتهي في حدود عشر دقائق قبل احتساب زمن الشبكة وتحميل الصفحات. الفريق الذي يشغّل بيئتين معاً — staging واختبار تحقّق سريع على الإنتاج — يرتاح أكثر على باقة STANDARD بسعر ‎$30 شهرياً مع 15 thread.

الفوترة على عدد الـ threads المتزامنة لا على عدد العمليات، والحلول داخل الباقة غير محدودة خلال الشهر. لذا السؤال الصحيح هو «كم عملية أحتاج في الدقيقة؟» لا «كم عملية في الشهر؟».


جدول الأعطال الشائعة

العَرَض السبب الأرجح الإصلاح
الرمز يعود بنجاح لكن الخادم يرفض النموذج مفتاح موقع قديم أو معلمة action ناقصة أعد استخراج مفتاح الموقع من الصفحة نفسها وأضف action إن وُجدت
لا أثر لـ data-sitekey في مصدر الصفحة الودجت يُحقن عبر JavaScript بعد التحميل انتقل إلى متصفح فعلي عبر Playwright أو Selenium لهذه الصفحة
استجابة 403 قبل قراءة الصفحة أصلاً ترويسات الطلب لا تشبه متصفحاً حقيقياً أرسل User-Agent وAccept-Language وأبقِ Session واحدة
الحل يتجاوز 60 ثانية ازدحام في قائمة الانتظار وقت الذروة ارفع مهلة الانتظار وأعد المحاولة بتباعد متزايد
الرمز يعمل مرة واحدة ثم يفشل الرمز يُستهلك عند أول تحقق على الخادم اطلب رمزاً جديداً لكل عملية إرسال
ERROR_ZERO_BALANCE الرصيد نفد أو الاشتراك منتهٍ راجع الرصيد في لوحة التحكم قبل إعادة التشغيل

أسئلة شائعة

هل يمكن إعادة استخدام الرمز نفسه لأكثر من طلب؟

لا. التحقق من الرمز يجري مرة واحدة على خادم الموقع، وعمره قصير بطبيعته. اطلب رمزاً جديداً مع كل عملية إرسال، ولا تحتفظ به في قاعدة بيانات أو ملف مؤقت.

لماذا لا أجد data-sitekey في مصدر الصفحة؟

لأن الودجت في هذه الحالة يُنشأ عبر JavaScript بعد التحميل. ابحث أولاً عن كلمة sitekey داخل ملفات JS المرتبطة بالصفحة أو في استدعاء turnstile.render. إن لم تظهر، فالمسار العملي هو تشغيل متصفح حقيقي — راجع دليل Playwright مع CaptchaAI في Python.

هل أحتاج خادماً وسيطاً مع requests؟

عملية الحل نفسها لا تشترط بروكسي. الحاجة إليه تظهر حين تُقيّد الصفحة الوصول جغرافياً أو تحدّ من معدل الطلبات من عنوان IP واحد.

كم thread أحتاج فعلاً؟

اقسم عدد العمليات المطلوبة في الدقيقة على ستّ عمليات لكل thread — وهو التقدير المتحفظ لزمن حل يقلّ عن 10 ثوانٍ. عشرون عملية في الدقيقة تعني أربعة threads نشطة، وباقة BASIC بسعر ‎$15 شهرياً تغطيها بخمسة threads.

هل يصلح الكود نفسه لـ Cloudflare Challenge؟

البنية العامة واحدة — إرسال ثم استطلاع — لكن Cloudflare Challenge نوع مستقل له استدعاؤه الخاص وزمن حل أعلى يقلّ عن 15 ثانية. الفرق بين النوعين وكيفية تمييزهما مشروحان في الفرق بين Cloudflare Challenge وTurnstile.


الخلاصة

المسار كله ثلاث محطات: استخرج مفتاح الموقع من HTML، أرسل المهمة إلى CaptchaAI بنوع turnstile، ثم استطلع حتى يعود الرمز ومرّره في حقل cf-turnstile-response مع بقية بيانات النموذج. أضف جلسة موحّدة الترويسات ومنطق إعادة محاولة، وستحصل على سكربت يصمد في التشغيل اليومي بمعدل نجاح مرتفع على أوضاع Turnstile كافة.

مقالات ذات صلة

أدلة ذات صلة

التعليقات غير مفعّلة لهذا المقال.