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

كشف CAPTCHA في النوافذ المنبثقة وحقن الرمز المميز

العقبة الحقيقية مع CAPTCHA الذي يظهر داخل نافذة منبثقة ليست في الحل نفسه، بل في التوقيت والموقع. مفتاح الموقع (sitekey) غير موجود في مصدر الصفحة عند تحميلها؛ فهو يُحقن ديناميكياً في اللحظة التي تُفتح فيها النافذة، ولديك ثوانٍ معدودة قبل أن تُغلق النافذة أو تنتهي صلاحية الجلسة. لذلك يحتاج سكربت الأتمتة إلى خمس خطوات مترابطة: افتح النافذة، انتظر ظهور CAPTCHA، استخرج المعلمات، أرسلها إلى CaptchaAI للحصول على الرمز، ثم احقن الرمز في المكان الصحيح قبل انتهاء المهلة.

لماذا تختلف النوافذ المنبثقة عن CAPTCHA العادي

في الصفحة التقليدية يكون عنصر .g-recaptcha حاضراً في DOM منذ اللحظة الأولى، فتقرأ مفتاح الموقع مباشرة. أما داخل النافذة المنبثقة فالوضع مختلف من ثلاث نواحٍ:

  • الظهور المتأخر: لا يُضاف عنصر CAPTCHA إلى الصفحة إلا بعد فتح النافذة، وقد يتأخر عرض الأداة نفسها ثانية أو ثانيتين بعد ذلك.
  • مفتاح موقع متغيّر: بعض المواقع تُنشئ نسخة CAPTCHA جديدة لكل فتح، فلا يصلح تخزين المفتاح مسبقاً.
  • ربط دالة رد النداء: ملء حقل الاستجابة وحده لا يكفي غالباً؛ فزر الإرسال داخل النافذة لا يُفعَّل إلا بعد استدعاء دالة رد النداء (Callback) التي تُعلِم الصفحة بأن التحقق اكتمل.

هذه الفروق الثلاثة هي ما يجعل معالجة النوافذ المنبثقة أصعب قليلاً، وهي بالضبط ما يعالجه المثالان أدناه.

أنماط ظهور CAPTCHA في النوافذ المنبثقة

قبل كتابة أي سكربت، حدِّد النمط الذي تتعامل معه، لأن المُطلِق يختلف من حالة إلى أخرى:

النمط ما يُطلقه طبيعة التحدي
نافذة تسجيل الدخول النقر على زر "تسجيل الدخول" يُحمَّل CAPTCHA داخل طبقة التراكب (overlay)
صفحة اعتراضية مضادة للبوتات تلقائياً بعد سلوك مشبوه نافذة بملء الشاشة تحجب الصفحة
تأكيد إتمام الشراء إرسال نموذج الدفع تظهر نافذة تحقق قبل إتمام العملية
تنبيه تجاوز حد الطلبات رصد عدد كبير من الطلبات نافذة تحتوي بوابة CAPTCHA
موافقة ملفات تعريف الارتباط + CAPTCHA الزيارة الأولى CAPTCHA مضمّن داخل نافذة الموافقة

خطوات المعالجة الخمس

يتبع كل مثال في هذا الدليل التسلسل نفسه:

  1. افتح النافذة، سواء بالنقر على العنصر المُطلِق أو بانتظار ظهورها تلقائياً.
  2. تأكد من أن النافذة أصبحت مرئية فعلاً عبر انتظار مُحدِّد CSS مناسب.
  3. انتظر ثانيتين حتى تكتمل أداة CAPTCHA، ثم استخرج مفتاح الموقع من DOM النافذة.
  4. أرسل المعلمات إلى CaptchaAI واستطلع النتيجة دورياً حتى يعود الرمز.
  5. احقن الرمز في حقل الاستجابة واستدعِ دالة رد النداء قبل إرسال النموذج.

في هذا المسار يحلّ CaptchaAI نوعَي CAPTCHA الأكثر شيوعاً داخل النوافذ: reCAPTCHA v2 (عبر الطريقة userrecaptcha) وCloudflare Turnstile (عبر turnstile). لهذا يبدأ المسار دائماً بخطوة كشف النوع قبل بناء منطق الحقن.

Python: معالجة النافذة المنبثقة باستخدام Playwright

يجمع المثال التالي الخطوات الخمس في دالة واحدة هي handle_modal_captcha: تفتح النافذة، تكتشف نوع CAPTCHA، تحلّه عبر CaptchaAI، ثم تحقن الرمز وتستدعي دالة رد النداء.

import requests
import time
from playwright.sync_api import sync_playwright

API_KEY = "YOUR_API_KEY"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"


def solve_captcha(sitekey, pageurl, method="userrecaptcha"):
    """Submit and poll a CAPTCHA."""
    params = {
        "key": API_KEY,
        "method": method,
        "json": 1,
    }
    if method == "userrecaptcha":
        params["googlekey"] = sitekey
        params["pageurl"] = pageurl
    elif method == "turnstile":
        params["sitekey"] = sitekey
        params["pageurl"] = 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 detect_modal_captcha(page):
    """
    Detect CAPTCHA inside a visible modal/dialog.
    Returns (sitekey, method) or (None, None).
    """
    return page.evaluate("""
        () => {
            // Find visible modals
            const modalSelectors = [
                '.modal.show',
                '.modal[style*="display: block"]',
                'dialog[open]',
                '[role="dialog"]:not([aria-hidden="true"])',
                '.overlay.visible',
                '.popup.active',
                '[class*="modal"][class*="open"]',
            ];

            let modal = null;
            for (const sel of modalSelectors) {
                const el = document.querySelector(sel);
                if (el && el.offsetParent !== null) {
                    modal = el;
                    break;
                }
            }

            // If no modal found, search entire document
            const searchRoot = modal || document;

            // Check for reCAPTCHA
            const recaptcha = searchRoot.querySelector('.g-recaptcha[data-sitekey]');
            if (recaptcha) {
                return { sitekey: recaptcha.dataset.sitekey, method: 'userrecaptcha' };
            }

            // Check for Turnstile
            const turnstile = searchRoot.querySelector('.cf-turnstile[data-sitekey]');
            if (turnstile) {
                return { sitekey: turnstile.dataset.sitekey, method: 'turnstile' };
            }

            // Check for hCaptcha
            const hcaptcha = searchRoot.querySelector('.h-captcha[data-sitekey]');
            if (hcaptcha) {
                return { sitekey: hcaptcha.dataset.sitekey, method: 'hcaptcha' };
            }

            return null;
        }
    """)


def inject_token_in_modal(page, token, method="userrecaptcha"):
    """Inject token into the CAPTCHA inside the modal."""
    if method == "userrecaptcha":
        page.evaluate("""
            (token) => {
                // Find response textarea (may be inside modal)
                const textareas = document.querySelectorAll('#g-recaptcha-response, [name="g-recaptcha-response"]');
                textareas.forEach(ta => {
                    ta.value = token;
                    ta.style.display = 'block';
                });

                // Trigger callback
                if (typeof ___grecaptcha_cfg !== 'undefined') {
                    Object.values(___grecaptcha_cfg.clients).forEach(client => {
                        Object.values(client).forEach(val => {
                            if (val && typeof val === 'object') {
                                Object.values(val).forEach(v => {
                                    if (v && typeof v.callback === 'function') v.callback(token);
                                });
                            }
                        });
                    });
                }
            }
        """, token)
    elif method == "turnstile":
        page.evaluate("""
            (token) => {
                const inputs = document.querySelectorAll('[name="cf-turnstile-response"]');
                inputs.forEach(inp => { inp.value = token; });

                if (typeof window.turnstileCallback === 'function') {
                    window.turnstileCallback(token);
                }
            }
        """, token)


def handle_modal_captcha(page, trigger_selector=None, timeout=10000):
    """
    Full workflow: trigger modal, detect CAPTCHA, solve, inject.
    """
    # Step 1: Trigger the modal if needed
    if trigger_selector:
        print(f"Clicking trigger: {trigger_selector}")
        page.click(trigger_selector)

    # Step 2: Wait for modal to become visible
    print("Waiting for modal...")
    modal_selectors = [
        ".modal.show",
        "dialog[open]",
        '[role="dialog"]:not([aria-hidden="true"])',
        ".popup.active",
    ]

    modal_visible = False
    for selector in modal_selectors:
        try:
            page.wait_for_selector(selector, timeout=timeout)
            modal_visible = True
            print(f"  Modal detected: {selector}")
            break
        except Exception:
            continue

    if not modal_visible:
        print("  No modal detected")
        return None

    # Step 3: Wait for CAPTCHA to render inside modal
    time.sleep(2)  # Brief pause for dynamic CAPTCHA loading

    captcha_info = detect_modal_captcha(page)
    if not captcha_info:
        print("  No CAPTCHA found in modal")
        return None

    sitekey = captcha_info["sitekey"]
    method = captcha_info["method"]
    print(f"  Found {method} CAPTCHA: {sitekey[:20]}...")

    # Step 4: Solve via CaptchaAI
    token = solve_captcha(sitekey, page.url, method)
    print(f"  Solved: {token[:30]}...")

    # Step 5: Inject token
    inject_token_in_modal(page, token, method)
    print("  Token injected")

    return token


def main():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False)
        page = browser.new_page()
        page.goto("https://example.com")
        page.wait_for_load_state("networkidle")

        # Handle CAPTCHA that appears in login modal
        token = handle_modal_captcha(
            page,
            trigger_selector="button#login-btn",
            timeout=10000,
        )

        if token:
            # Fill form fields inside modal
            page.fill('dialog input[name="email"]', "user@example.com")
            page.fill('dialog input[name="password"]', "password123")

            # Submit modal form
            page.click('dialog button[type="submit"]')
            page.wait_for_load_state("networkidle")

        browser.close()


main()

JavaScript: معالجة النافذة المنبثقة باستخدام Puppeteer

المنطق نفسه في بيئة Node.js باستخدام Puppeteer، وهو مناسب لمن يبني أدوات الأتمتة على JavaScript. لاحظ أن حقن الرمز يستهدف g-recaptcha-response لـ reCAPTCHA v2 وcf-turnstile-response لـ Cloudflare Turnstile.

const puppeteer = require("puppeteer");

const API_KEY = "YOUR_API_KEY";
const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";

async function solveCaptcha(sitekey, pageurl, method = "userrecaptcha") {
  const body = new URLSearchParams({ key: API_KEY, method, json: "1" });
  if (method === "userrecaptcha") { body.set("googlekey", sitekey); body.set("pageurl", pageurl); }
  else if (method === "turnstile") { body.set("sitekey", sitekey); body.set("pageurl", 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 waitForModal(page, timeout = 10000) {
  const selectors = [".modal.show", "dialog[open]", '[role="dialog"]', ".popup.active"];
  for (const sel of selectors) {
    try {
      await page.waitForSelector(sel, { visible: true, timeout });
      return sel;
    } catch {}
  }
  return null;
}

async function detectModalCaptcha(page) {
  return page.evaluate(() => {
    const checks = [
      { sel: ".g-recaptcha[data-sitekey]", method: "userrecaptcha" },
      { sel: ".cf-turnstile[data-sitekey]", method: "turnstile" },
    ];
    for (const { sel, method } of checks) {
      const el = document.querySelector(sel);
      if (el && el.dataset.sitekey) return { sitekey: el.dataset.sitekey, method };
    }
    return null;
  });
}

async function handleModalCaptcha(page, triggerSelector) {
  // Trigger modal
  if (triggerSelector) await page.click(triggerSelector);

  // Wait for modal
  const modalSel = await waitForModal(page);
  if (!modalSel) { console.log("No modal found"); return null; }
  console.log(`Modal visible: ${modalSel}`);

  // Wait for CAPTCHA render
  await new Promise((r) => setTimeout(r, 2000));

  const info = await detectModalCaptcha(page);
  if (!info) { console.log("No CAPTCHA in modal"); return null; }

  console.log(`Found ${info.method}: ${info.sitekey.substring(0, 20)}...`);
  const token = await solveCaptcha(info.sitekey, page.url(), info.method);
  console.log(`Solved: ${token.substring(0, 30)}...`);

  // Inject token
  await page.evaluate((t, method) => {
    if (method === "userrecaptcha") {
      document.querySelectorAll("#g-recaptcha-response").forEach((el) => { el.value = t; });
    } else if (method === "turnstile") {
      document.querySelectorAll('[name="cf-turnstile-response"]').forEach((el) => { el.value = t; });
    }
  }, token, info.method);

  return token;
}

(async () => {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();
  await page.goto("https://example.com", { waitUntil: "networkidle2" });

  const token = await handleModalCaptcha(page, "button#login-btn");
  if (token) {
    await page.type('dialog input[name="email"]', "user@example.com");
    await page.click('dialog button[type="submit"]');
    await page.waitForNavigation();
  }

  await browser.close();
})();

ضبط التوقيت لتفادي إغلاق النافذة

معظم إخفاقات هذا المسار سببها التوقيت لا المنطق. النافذة قد تُغلق تلقائياً، والجلسة قد تنتهي، والرمز نفسه له عمر محدود. تعامَل مع كل عامل من العوامل التالية بشكل صريح:

العامل الأثر كيفية التخفيف
مهلة الإغلاق التلقائي للنافذة قد تُغلق النافذة قبل اكتمال الحل ابدأ الحل فور اكتشاف CAPTCHA مباشرة
انتهاء الجلسة أثناء الحل تنتهي جلسة الخادم أثناء انتظار الرمز أبقِ الجلسة نشطة عبر نبضات دورية في الخلفية
تأخّر عرض CAPTCHA داخل النافذة تستغرق الأداة من 1 إلى 3 ثوانٍ للتحميل انتظر ثانيتين بعد ظهور النافذة قبل استخراج مفتاح الموقع
انتهاء صلاحية الرمز أثناء تعبئة النموذج ينتهي الرمز قبل إرسال النموذج احلّ CAPTCHA أخيراً، بعد ملء بقية الحقول

معالجة الأخطاء الشائعة

المشكلة السبب المحتمل الإجراء
النافذة تُكتشف لكن لا يُعثر على CAPTCHA الأداة تُحمَّل بشكل غير متزامن بعد فتح النافذة زِد زمن الانتظار أو استخدم MutationObserver لرصد لحظة إدراج الأداة
يُحقن الرمز لكن النافذة لا تُغلق لم تُستدعَ دالة رد النداء ابحث عن دالة رد النداء واستدعِها صراحةً بعد الحقن
مفتاح الموقع يختلف في كل مرة النافذة تُنشئ نسخة CAPTCHA ديناميكية استخرج المفتاح من DOM في كل مرة ولا تخزّنه مطلقاً
الرمز يُنشأ لكن الموقع يرفضه مفتاح الموقع أو الصفحة أو سياق الجلسة غير متطابق أعد التقاط المعلمات واستخدم الرمز داخل جلسة المتصفح نفسها
النقر على المُطلِق لا يفتح النافذة العنصر غير قابل للتفاعل أو محجوب بطبقة تراكب استخدم page.dispatchEvent أو انتظر حتى يصبح العنصر قابلاً للنقر

سيناريو عملي: نافذة الدفع أثناء موجة طلبات

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

الأسئلة الشائعة

لماذا يتغيّر مفتاح الموقع في كل مرة تُفتح فيها النافذة؟

بعض المواقع تُنشئ نسخة CAPTCHA جديدة لكل فتح للنافذة كإجراء أمني. لذلك استخرج مفتاح الموقع من DOM النافذة في كل مرة، ولا تعتمد على قيمة مخزَّنة من محاولة سابقة.

ما الفرق بين حقن الرمز في حقل الاستجابة واستدعاء دالة رد النداء؟

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

هل يختلف حقن الرمز بين reCAPTCHA v2 وCloudflare Turnstile؟

نعم. في reCAPTCHA v2 تُحقن القيمة في حقل g-recaptcha-response، بينما تستهدف Cloudflare Turnstile حقل cf-turnstile-response. لهذا تكتشف الدالة النوع أولاً ثم تختار حقل الاستجابة الصحيح ودالة رد النداء المناسبة قبل الإرسال.

كم تبلغ تكلفة حل CAPTCHA داخل النوافذ المنبثقة؟

يعتمد تسعير CaptchaAI على عدد الـ Threads المتزامنة لا على عدد عمليات الحل. تبدأ باقة BASIC من 15 دولاراً شهرياً وتشمل 5 Threads مع عدد حلول غير محدود، وهو ما يكفي لاختبار تدفق نافذة واحدة. للحجم الأكبر، تتوفر باقات أعلى مثل STANDARD بسعر 30 دولاراً (15 Thread) وADVANCE بسعر 90 دولاراً (50 Thread).


الخطوات التالية

أدلة ذات صلة

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