التكاملات

دمج undetected-chromedriver مع CaptchaAI في Python

المعادلة تُختصر في سطر واحد: undetected-chromedriver يقلّل عدد التحديات التي تُعرض على سكربتك، وCaptchaAI يحلّ ما يظهر منها فعلاً. الاكتفاء بطبقة واحدة ينتهي إلى النتيجة نفسها — سكربت يتوقف عند أول صفحة دخول، وتقرير فشل ينتظرك صباحاً.

المكتبة تصحّح ChromeDriver الخاص بـ Selenium: تطابق إصدار Chrome تلقائياً، وتزيل علامة navigator.webdriver، وتعدّل بصمات المتصفح التي تفحصها المواقع. لكن قواعد الفحص تتغيّر باستمرار، فيبقى المسار العملي: قلّل الاحتكاك عبر المكتبة، وعالج ما تبقّى عبر API الحل. هذا الدليل يجمع الاثنين في سكربت Python واحد يعمل مع reCAPTCHA v2 وCloudflare Turnstile.


متى تستحق تركيبة undetected-chromedriver وCaptchaAI العناء؟

المكتبة وحدها تكفي في المواقع ذات الفحوص الخفيفة، وCaptchaAI وحده يكفي مع طلبات HTTP المباشرة دون متصفح. أما الجمع بينهما فضروري في ثلاث حالات:

  • تدفقات تحتاج جلسة حقيقية: دخول، أو سلة شراء، أو لوحة تحكم تعتمد على ملفات تعريف الارتباط وJavaScript.
  • تشغيل دوري طويل: سكربت يعمل كل ساعة على النطاق نفسه، فيبدأ التحدي بالظهور بعد عشرات الجولات.
  • عرض مشروط للتحدي: الموقع لا يعرضه لكل زائر، بل عند إشارة بعينها — عنوان IP جديد، أو إيقاع تنقّل غير معتاد.

دور المكتبة تقليل مرات ظهور التحدي، ودور CaptchaAI ألّا يتوقف السكربت حين يظهر رغم ذلك.


قائمة التحقق قبل أول تشغيل

المتطلب التفاصيل
مفتاح CaptchaAI API من لوحة التحكم على captchaai.com
Python 3.8 أو أحدث يفضَّل داخل بيئة افتراضية مستقلة
متصفح Chrome مثبَّت على الجهاز الذي يشغّل السكربت

سطر واحد يكفي للتثبيت:

pip install undetected-chromedriver requests

مكتبة requests وحدها تكفي للتعامل مع نقطتَي النهاية in.php وres.php.


تهيئة جلسة Chrome عبر undetected-chromedriver

ابدأ بدالة صغيرة تُنشئ نسخة Chrome مضبوطة مسبقاً، حتى لا تتكرر الخيارات في كل سكربت:

import undetected_chromedriver as uc
import requests
import time


def create_stealth_browser():
    """Create an undetected Chrome browser instance."""
    options = uc.ChromeOptions()
    options.add_argument("--no-sandbox")
    options.add_argument("--window-size=1920,1080")

    driver = uc.Chrome(options=options)
    return driver

خياران يستحقان الانتباه: --no-sandbox لازم داخل حاويات Docker وخوادم Linux بلا واجهة رسومية، و--window-size يتفادى أبعاد النافذة الافتراضية التي تميّز جلسات الأتمتة. واترك المكتبة تختار برنامج التشغيل المطابق لإصدار Chrome بدل مسار يدوي يتعطّل مع أول تحديث.


المسار العملي لحل reCAPTCHA v2

ثلاث خطوات لا تتغيّر مهما تغيّر الموقع: التقط مفتاح الموقع، أرسله إلى الخدمة، ثم أعِد الرمز إلى الصفحة.

الخطوة 1: التقط مفتاح الموقع من الصفحة

المفتاح موجود غالباً في السمة data-sitekey، ولهذا نبدأ منها. وإن كان الودجت داخل iframe، نستخرجه من معامل k= في رابط الإطار:

API_KEY = "YOUR_API_KEY"


def extract_recaptcha_sitekey(driver):
    """Extract reCAPTCHA v2 sitekey from the page."""
    try:
        element = driver.find_element("css selector", "[data-sitekey]")
        return element.get_attribute("data-sitekey")
    except Exception:
        # Try finding in iframe src
        iframes = driver.find_elements("css selector", "iframe[src*='recaptcha']")
        for iframe in iframes:
            src = iframe.get_attribute("src")
            if "k=" in src:
                return src.split("k=")[1].split("&")[0]
    return None

القيمة None تعني عادةً أن الصفحة لم تكتمل بعد؛ عالجها بانتظار ظهور العنصر لا بانتظار وقت ثابت.

الخطوة 2: أرسل المهمة إلى CaptchaAI واستطلع النتيجة

يذهب الطلب إلى in.php بالطريقة userrecaptcha، ثم يبدأ فحص دوري على res.php حتى يعود الرمز جاهزاً:

def solve_recaptcha_v2(sitekey, pageurl):
    """Submit reCAPTCHA v2 to CaptchaAI and return the token."""
    submit = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": pageurl,
        "json": 1
    }).json()

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

    task_id = submit["request"]

    time.sleep(20)
    for _ in range(30):
        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") != "CAPCHA_NOT_READY":
            raise RuntimeError(f"Solve error: {result['request']}")
        time.sleep(5)

    raise TimeoutError("Solve timed out")

الاستجابة CAPCHA_NOT_READY ليست خطأ — إنها الحالة الطبيعية أثناء المعالجة، وأي قيمة أخرى تعني توقفاً حقيقياً يستحق raise. أما time.sleep(20) فاختيار مقصود: CaptchaAI يحل reCAPTCHA v2 في أقل من 60 ثانية، ولا فائدة من إغراق res.php في الثواني الأولى. واقرأ YOUR_API_KEY من متغير بيئة، لا من ملف ترفعه إلى GitHub.

الخطوة 3: احقن الرمز وشغّل رد النداء

هنا يقع الخطأ الأكثر تكراراً: وضع الرمز في الحقل المخفي g-recaptcha-response ثم رفض النموذج للإرسال، لأن بعض الصفحات تنتظر استدعاء دالة رد النداء التي سجّلها الودجت:

def inject_recaptcha_token(driver, token):
    """Inject the solved token into the page and submit."""
    driver.execute_script(f'''
        document.getElementById("g-recaptcha-response").innerHTML = "{token}";
        document.getElementById("g-recaptcha-response").style.display = "block";
    ''')

    # If there's a callback function, trigger it
    driver.execute_script(f'''
        if (typeof ___grecaptcha_cfg !== 'undefined') {{
            var clients = ___grecaptcha_cfg.clients;
            for (var key in clients) {{
                var client = clients[key];
                if (client && client.callback) {{
                    client.callback("{token}");
                }}
            }}
        }}
    ''')

الجزء الثاني يمرّ على عملاء ___grecaptcha_cfg ويستدعي callback إن وُجد؛ وإذا اعتمد النموذج على زر إرسال عادي فالجزء الأول كافٍ.


مثال متكامل: تسجيل دخول محمي بـ reCAPTCHA v2

السكربت التالي يجمع الخطوات الثلاث في سيناريو واحد — فتح صفحة الدخول، تعبئة الحقول، الحل، ثم الإرسال:

import undetected_chromedriver as uc
import requests
import time

API_KEY = "YOUR_API_KEY"


def solve_recaptcha(sitekey, pageurl):
    submit = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY, "method": "userrecaptcha",
        "googlekey": sitekey, "pageurl": pageurl, "json": 1
    }).json()

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

    task_id = submit["request"]
    time.sleep(20)

    for _ in range(30):
        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") != "CAPCHA_NOT_READY":
            raise RuntimeError(f"Solve error: {result['request']}")
        time.sleep(5)
    raise TimeoutError("Solve timed out")


def main():
    driver = uc.Chrome()

    try:
        # Navigate to target page
        driver.get("https://example.com/login")
        time.sleep(3)

        # Fill in form fields
        driver.find_element("id", "username").send_keys("user")
        driver.find_element("id", "password").send_keys("pass")

        # Extract sitekey
        element = driver.find_element("css selector", "[data-sitekey]")
        sitekey = element.get_attribute("data-sitekey")
        pageurl = driver.current_url
        print(f"Sitekey: {sitekey}")

        # Solve CAPTCHA
        token = solve_recaptcha(sitekey, pageurl)
        print(f"Token: {token[:50]}...")

        # Inject token
        driver.execute_script(
            f'document.getElementById("g-recaptcha-response").innerHTML = "{token}";'
        )

        # Submit form
        driver.find_element("id", "submit-btn").click()
        time.sleep(3)

        print(f"Current URL: {driver.current_url}")
    finally:
        driver.quit()


if __name__ == "__main__":
    main()

إنهاء الجلسة داخل finally يمنع تراكم عمليات Chrome المعلّقة عند فشل أي خطوة — وهو أكثر أسباب استهلاك الذاكرة في التشغيل الليلي.


سيناريو من السوق العربي: فحص ليلي لبوابة عملاء

خذ فريق هندسة في القاهرة يشغّل فحصاً ليلياً على بوابة عملاء تابعة لشركته: دخول بحساب اختبار، قراءة آخر فاتورة، ثم خروج. صفحة الدخول محمية بـ reCAPTCHA v2، وصفحة الدفع تستخدم Cloudflare Turnstile. قبل الدمج كان الفحص يفشل في ثلث الليالي بتقرير "فشل غير حقيقي" يستهلك ساعة من وقت الفريق صباحاً.

بعد إضافة الطبقتين صار المسار أبسط: جلسة undetected-chromedriver واحدة لكل بيئة — staging وproduction — ونداء واحد إلى CaptchaAI عند ظهور أي تحدٍّ. الفحص يعمل بين الثانية والرابعة فجراً بتوقيت القاهرة، فلا يزاحم المستخدمين الحقيقيين، ولأن الصفحتين تحملان نوعين مختلفين من التحديات يكفي تبديل الطريقة في نداء واحد.


التعامل مع Cloudflare Turnstile في الجلسة نفسها

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

def solve_turnstile(sitekey, pageurl):
    submit = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY, "method": "turnstile",
        "sitekey": sitekey, "pageurl": pageurl, "json": 1
    }).json()

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

    task_id = submit["request"]
    time.sleep(10)

    for _ in range(30):
        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") != "CAPCHA_NOT_READY":
            raise RuntimeError(f"Solve error: {result['request']}")
        time.sleep(5)
    raise TimeoutError("Solve timed out")


# Inject Turnstile token
def inject_turnstile_token(driver, token):
    driver.execute_script(f'''
        var input = document.querySelector('[name="cf-turnstile-response"]');
        if (input) input.value = "{token}";
    ''')

الفارق الأول في التوقيت: Turnstile يُحل عادةً في أقل من 10 ثوانٍ، ولذلك يبدأ الفحص الدوري بعد time.sleep(10) بدل 20. والفارق الثاني في اسم الحقل — لا تخلط بين cf-turnstile-response وg-recaptcha-response، فالحقن في الحقل الخطأ يعطي فشلاً صامتاً.


احسب الـ threads قبل اختيار الخطة

الفوترة هنا على عدد الـ threads المتزامنة لا على عدد عمليات الحل، وكل خطة تشمل عمليات حل غير محدودة خلال الشهر. القاعدة العملية: عدد الـ threads ≈ عدد الجلسات التي قد تصطدم بتحدٍّ في اللحظة نفسها.

  • BASIC — $15 شهرياً مع 5 threads: فحص ليلي أو سكربت مراقبة على بضع بيئات.
  • STANDARD — $30 شهرياً مع 15 thread: عدة سكربتات متوازية أو خط CI بمهام متزامنة.
  • ADVANCE — $90 شهرياً مع 50 thread: أسطول متصفحات موزّع على عدة عقد.

ابدأ من الأصغر وراقب زمن الانتظار في حلقة الاستطلاع؛ ارتفاعه المستمر هو مؤشر الترقية.


أخطاء شائعة أثناء التشغيل

العَرَض السبب المرجّح الإجراء
عدم تطابق إصدار Chrome لا يوجد برنامج تشغيل مطابق حدّث Chrome أو مرّر version_main
التحدي يظهر رغم الإعدادات فحص بصمات متقدم متوقع — مرّره إلى CaptchaAI
فشل حقن الرمز معرّف خاطئ أو رد نداء لم يُستدعَ راجع تنفيذ reCAPTCHA في الصفحة
WebDriverException تعطّل Chrome داخل الحاوية أضف --no-sandbox و--disable-dev-shm-usage
ERROR_ZERO_BALANCE الرصيد غير كافٍ اشحن الرصيد قبل تشغيل الدفعة
ERROR_WRONG_USER_KEY صيغة المفتاح غير صحيحة انسخ المفتاح كاملاً من لوحة التحكم

أسئلة شائعة

هل أحتاج خادماً وسيطاً إلى جانب undetected-chromedriver؟

ليس دائماً. لكن إن عمل السكربت من عنوان IP ثابت عشرات المرات يومياً، فسمعة عنوان IP تصبح العامل الأهم. وزّع وتيرة التشغيل على اليوم أولاً، ثم فكّر في وكيل سكني.

لماذا ينتظر الكود 20 ثانية قبل أول استفسار عن النتيجة؟

لأن الحل يستغرق وقتاً فعلياً: reCAPTCHA v2 في أقل من 60 ثانية، وCloudflare Turnstile في أقل من 10 ثوانٍ. الانتظار الأولي يوفّر دورات فحص فارغة على res.php، ثم تتولى الحلقة بفاصل خمس ثوانٍ بقية الانتظار.

الرمز عاد بنجاح لكن النموذج لا يُرسَل — أين المشكلة؟

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

هل يعمل الوضع بلا واجهة رسومية؟

نعم، عبر options.add_argument("--headless=new"). لكن التشغيل بلا واجهة يرفع احتمال ظهور التحدي مقارنة بجلسة عادية، فخصّص له مسار حل واضحاً منذ البداية.

كيف أوزّع هذا على عدة عقد أو داخل Selenium Grid؟

المكتبة تصحّح ملف برنامج التشغيل محلياً، فلا يكفي توجيه الجلسة إلى عقدة بعيدة. جهّز البرنامج المصحَّح في صورة كل عقدة، واجعل نداء CaptchaAI مركزياً لمراقبة استهلاك الـ threads من مكان واحد.


ابدأ من مفتاح الـ API

أنشئ حساباً على captchaai.com، انسخ مفتاح الـ API، وشغّل مثال تسجيل الدخول أعلاه بعد استبدال YOUR_API_KEY ورابط الصفحة المستهدفة.


أدلة ذات صلة

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