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

التعامل مع Shadow DOM CAPTCHA: الوصول إلى العناصر في مكونات الويب

لا يمكن لـ document.querySelector القياسي الوصول إلى جذور الظل. عندما يتم عرض عنصر واجهة المستخدم CAPTCHA داخل ظل DOM لمكون الويب، فإن البرنامج النصي المعتاد لاستخراج مفتاح الموقع الخاص بك يُرجع null. يوجد اختبار CAPTCHA - ولا يستطيع محددك رؤيته.

قبل افتراض أن المشكلة من Shadow DOM، تأكد أولًا أنك لا تتعامل مع iframe خارجي أو تحميل متأخر بواسطة JavaScript. الفرق مهم لأن أسلوب الاستخراج يختلف: جذور الظل تحتاج عبورًا متكررًا للشجرة، أما iframe فيحتاج الانتقال إلى سياق مختلف بالكامل.

كيف تميّز Shadow DOM عن iframe أو التحميل المتأخر؟

الحالة العلامة العملية ما الذي تفعله؟
Shadow DOM العنصر ظاهر بصريًا لكن querySelector يعيد null، وهناك shadowRoot على العنصر الأب استخدم selector يخترق جذور الظل أو recursion عبر shadowRoot
iframe الأداة داخل إطار فرعي مستقل بدّل السياق إلى الإطار أولًا
تحميل متأخر العنصر لا يوجد في البداية ثم يظهر بعد حدث أو طلب شبكة أضف انتظارًا أو مراقبة DOM قبل الاستخراج

لماذا تنتهي اختبارات CAPTCHA في Shadow DOM

السيناريو السبب
مكون تسجيل الدخول المخصص مغلفة كمكون ويب قابل لإعادة الاستخدام
أداة نموذج الطرف الثالث يقوم بائع القطعة بتغليف النموذج بالكامل في جذر الظل
بنية الواجهة الأمامية الصغيرة يستخدم كل تطبيق صغير ظل DOM معزولًا
مكونات نظام التصميم CAPTCHA مضمن في عنصر مكتبة المكونات

الكشف عن Shadow DOM CAPTCHAs

قبل الحل، تأكد من وجود اختبار CAPTCHA داخل جذر الظل:

// In browser DevTools console
// Regular query returns null even though CAPTCHA is visible
document.querySelector('.cf-turnstile');  // null

// Check for shadow hosts
document.querySelectorAll('*').forEach(el => {
  if (el.shadowRoot) {
    const captcha = el.shadowRoot.querySelector('.cf-turnstile, .g-recaptcha');
    if (captcha) {
      console.log('Found CAPTCHA in shadow root of:', el.tagName, el.id || el.className);
      console.log('Sitekey:', captcha.dataset.sitekey);
    }
  }
});

بايثون: ثقب Playwright شادو دوم

يتعامل محدد ثقب >> الخاص بPlaywright وlocator API مع Shadow DOM أصليًا:

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_turnstile(sitekey, pageurl):
    """Submit and poll a Turnstile CAPTCHA."""
    resp = requests.post(SUBMIT_URL, data={
        "key": API_KEY,
        "method": "turnstile",
        "sitekey": sitekey,
        "pageurl": pageurl,
        "json": 1,
    }, 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 extract_from_shadow_dom(page):
    """Extract CAPTCHA sitekey from shadow DOM elements."""

    # Method 1: Playwright's piercing selector (>>)
    # This automatically crosses shadow boundaries
    turnstile = page.locator("css=.cf-turnstile >> visible=true").first
    if turnstile.count() > 0:
        sitekey = turnstile.get_attribute("data-sitekey")
        if sitekey:
            return sitekey

    # Method 2: JavaScript evaluation to pierce all shadow roots
    sitekey = page.evaluate("""
        () => {
            function findInShadowRoots(root) {
                // Check direct children
                const turnstile = root.querySelector('.cf-turnstile');
                if (turnstile && turnstile.dataset.sitekey) {
                    return turnstile.dataset.sitekey;
                }

                const recaptcha = root.querySelector('.g-recaptcha');
                if (recaptcha && recaptcha.dataset.sitekey) {
                    return recaptcha.dataset.sitekey;
                }

                // Recurse into nested shadow roots
                for (const el of root.querySelectorAll('*')) {
                    if (el.shadowRoot) {
                        const found = findInShadowRoots(el.shadowRoot);
                        if (found) return found;
                    }
                }
                return null;
            }
            return findInShadowRoots(document);
        }
    """)

    return sitekey


def inject_token_shadow_dom(page, token, captcha_type="turnstile"):
    """Inject solved token into shadow DOM CAPTCHA element."""
    if captcha_type == "turnstile":
        page.evaluate(f"""
            (token) => {{
                function findAndInject(root) {{
                    // Find the response input inside Turnstile
                    const input = root.querySelector('[name="cf-turnstile-response"]');
                    if (input) {{
                        input.value = token;
                        return true;
                    }}

                    // Recurse into shadow roots
                    for (const el of root.querySelectorAll('*')) {{
                        if (el.shadowRoot && findAndInject(el.shadowRoot)) {{
                            return true;
                        }}
                    }}
                    return false;
                }}
                findAndInject(document);

                // Also try callback if defined
                if (typeof window.turnstileCallback === 'function') {{
                    window.turnstileCallback(token);
                }}
            }}
        """, token)
    elif captcha_type == "recaptcha":
        page.evaluate(f"""
            (token) => {{
                function findAndInject(root) {{
                    const textarea = root.querySelector('#g-recaptcha-response');
                    if (textarea) {{
                        textarea.value = token;
                        textarea.style.display = 'block';
                        return true;
                    }}
                    for (const el of root.querySelectorAll('*')) {{
                        if (el.shadowRoot && findAndInject(el.shadowRoot)) {{
                            return true;
                        }}
                    }}
                    return false;
                }}
                findAndInject(document);

                if (typeof ___grecaptcha_cfg !== 'undefined') {{
                    Object.entries(___grecaptcha_cfg.clients).forEach(([_, client]) => {{
                        Object.entries(client).forEach(([_, val]) => {{
                            if (val && typeof val === 'object') {{
                                Object.entries(val).forEach(([_, v]) => {{
                                    if (v && v.callback) v.callback(token);
                                }});
                            }}
                        }});
                    }});
                }}
            }}
        """, token)


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

        # Extract sitekey from shadow DOM
        sitekey = extract_from_shadow_dom(page)
        if not sitekey:
            print("No CAPTCHA found in shadow DOM or regular DOM")
            browser.close()
            return

        print(f"Found sitekey: {sitekey}")

        # Solve via CaptchaAI
        token = solve_turnstile(sitekey, page.url)
        print(f"Solved: {token[:40]}...")

        # Inject token back into shadow DOM
        inject_token_shadow_dom(page, token, "turnstile")
        print("Token injected into shadow DOM")

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

        browser.close()


main()

JavaScript: Puppeteer الظل DOM اجتياز

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 solveTurnstile(sitekey, pageurl) {
  const params = new URLSearchParams({
    key: API_KEY, method: "turnstile", sitekey, pageurl, json: "1",
  });
  const resp = await (await fetch(SUBMIT_URL, { method: "POST", body: params })).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 extractSitekeyFromShadowDOM(page) {
  return page.evaluate(() => {
    function searchShadowRoots(root) {
      const selectors = [".cf-turnstile", ".g-recaptcha", ".h-captcha"];
      for (const sel of selectors) {
        const el = root.querySelector(sel);
        if (el && el.dataset.sitekey) return el.dataset.sitekey;
      }
      for (const el of root.querySelectorAll("*")) {
        if (el.shadowRoot) {
          const found = searchShadowRoots(el.shadowRoot);
          if (found) return found;
        }
      }
      return null;
    }
    return searchShadowRoots(document);
  });
}

async function injectTokenShadowDOM(page, token) {
  await page.evaluate((t) => {
    function inject(root) {
      const input = root.querySelector('[name="cf-turnstile-response"]');
      if (input) { input.value = t; return true; }
      const textarea = root.querySelector("#g-recaptcha-response");
      if (textarea) { textarea.value = t; return true; }
      for (const el of root.querySelectorAll("*")) {
        if (el.shadowRoot && inject(el.shadowRoot)) return true;
      }
      return false;
    }
    inject(document);
  }, token);
}

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

  const sitekey = await extractSitekeyFromShadowDOM(page);
  if (!sitekey) {
    console.log("No CAPTCHA found in shadow DOM");
    await browser.close();
    return;
  }

  console.log(`Sitekey: ${sitekey}`);
  const token = await solveTurnstile(sitekey, page.url());
  console.log(`Solved: ${token.substring(0, 40)}...`);

  await injectTokenShadowDOM(page, token);
  await page.click('button[type="submit"]');
  await page.waitForNavigation();

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

اعتبارات عمق الظل DOM

العمق مثال النهج
1 مستوى <custom-form> #shadow-root > .cf-turnstile استعلام جذر الظل المباشر
2+ المستويات <app-shell> #shadow > <login-form> #shadow > .cf-turnstile اجتياز العودية
فتح جذر الظل يمكن الوصول إلى el.shadowRoot النهج القياسي - استخدم العودية
جذر الظل المغلق el.shadowRoot ترجع فارغة لا يمكن أن يخترق. استخدم page.evaluate مع تجاوز attachShadow({mode:'open'}) أو عرض الاعتراض

استكشاف الأخطاء وإصلاحها

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

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

أدلة ذات صلة

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