حالات الاستخدام

حل CAPTCHA مع Puppeteer وNode.js باستخدام CaptchaAI

عندما يعترض اختبار CAPTCHA سكربت Puppeteer، الأسلوب العملي هو إخراج الحل خارج المتصفح: يستخرج سكربتك مفتاح الموقع، ويحل CaptchaAI التحدي من جانب الخادم، ثم يعيد رمزاً تحقنه في الحقل المخفي وترسل النموذج. يعمل هذا المسار مع reCAPTCHA v2 وCloudflare Turnstile بالمنطق نفسه، سواء شغّلت المتصفح بلا واجهة (headless) أو بواجهة مرئية.

تخيّل فريق بيانات في الرياض يراقب أسعار منصة تجارة إلكترونية عبر Puppeteer فتصادفه صفحة تسجيل دخول محمية بـ reCAPTCHA: بدل توقّف السكربت عند التحدي، يرسل الفريق مفتاح الموقع إلى CaptchaAI ويكمل الجلسة تلقائياً دون تدخل بشري. هذا هو الفارق بين خط بيانات يتوقف كل بضع دقائق وآخر يعمل دون انقطاع.

ما تحتاجه قبل البدء

قبل كتابة أي سطر، تأكد من توفر بيئة Node.js حديثة ومكتبتين فقط: Puppeteer لقيادة المتصفح، وAxios لإجراء طلبات HTTP نحو CaptchaAI. المفتاح الأخير الذي تحتاجه هو مفتاح الـ API الخاص بحسابك، وهو ما يربط سكربتك بالخدمة.

المتطلبات التفاصيل
Node.js 16+ مع npm
Puppeteer npm install puppeteer
Axios npm install axios
مفتاح CaptchaAI API منcaptchaai.com

المنطق في أربع خطوات

يقوم التكامل بأكمله على تقسيم واضح للأدوار: Puppeteer يتعامل مع المتصفح والصفحة، وCaptchaAI يتعامل مع التحدي نفسه. تجري الدورة على النحو التالي:

  1. ينتقل Puppeteer إلى الصفحة المحمية باختبار CAPTCHA.
  2. يستخرج سكربتك مفتاح الموقع (sitekey) من الـ DOM.
  3. يحل CaptchaAI التحدي من جانب الخادم ويعيد الرمز.
  4. يحقن سكربتك الرمز ويرسل النموذج.

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

متى تلجأ إلى خدمة حل خارجية؟

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

الخطوة 1: بناء وحدة الحل

اعزل منطق الاتصال بـ CaptchaAI في وحدة مستقلة حتى يبقى سكربت Puppeteer نظيفاً. تُرسل الدالة المهمة أولاً إلى in.php فتستقبل معرّف مهمة، ثم تستطلع النتيجة عبر res.php حتى يجهز الرمز أو تنتهي المحاولات. يضبط الثابتان POLL_INTERVAL وMAX_ATTEMPTS الفاصل بين كل استفسار وسقف المحاولات قبل مهلة الانتهاء. هناك دالتان بالنمط نفسه: واحدة لـ reCAPTCHA v2 (method=userrecaptcha) وأخرى لـ Cloudflare Turnstile (method=turnstile)، والفرق الوحيد هو اسم المعلمة وقيمة method.

// solver.js
const axios = require("axios");

const API_KEY = "YOUR_API_KEY";
const POLL_INTERVAL = 5000;
const MAX_ATTEMPTS = 60;

async function solveRecaptchaV2(siteKey, pageUrl) {
  // Submit task
  const submitResp = await axios.get("https://ocr.captchaai.com/in.php", {
    params: {
      key: API_KEY,
      method: "userrecaptcha",
      googlekey: siteKey,
      pageurl: pageUrl,
    },
  });

  if (!submitResp.data.startsWith("OK|")) {
    throw new Error(`Submit failed: ${submitResp.data}`);
  }

  const taskId = submitResp.data.split("|")[1];
  console.log(`Task submitted: ${taskId}`);

  // Poll for result
  for (let i = 0; i < MAX_ATTEMPTS; i++) {
    await new Promise((r) => setTimeout(r, POLL_INTERVAL));

    const result = await axios.get("https://ocr.captchaai.com/res.php", {
      params: { key: API_KEY, action: "get", id: taskId },
    });

    if (result.data === "CAPCHA_NOT_READY") continue;
    if (result.data.startsWith("OK|")) {
      return result.data.split("|")[1];
    }
    throw new Error(`Solve failed: ${result.data}`);
  }
  throw new Error("Solve timed out");
}

async function solveTurnstile(siteKey, pageUrl) {
  const submitResp = await axios.get("https://ocr.captchaai.com/in.php", {
    params: {
      key: API_KEY,
      method: "turnstile",
      sitekey: siteKey,
      pageurl: pageUrl,
    },
  });

  if (!submitResp.data.startsWith("OK|")) {
    throw new Error(`Submit failed: ${submitResp.data}`);
  }

  const taskId = submitResp.data.split("|")[1];

  for (let i = 0; i < MAX_ATTEMPTS; i++) {
    await new Promise((r) => setTimeout(r, POLL_INTERVAL));
    const result = await axios.get("https://ocr.captchaai.com/res.php", {
      params: { key: API_KEY, action: "get", id: taskId },
    });
    if (result.data === "CAPCHA_NOT_READY") continue;
    if (result.data.startsWith("OK|")) return result.data.split("|")[1];
    throw new Error(`Solve failed: ${result.data}`);
  }
  throw new Error("Solve timed out");
}

module.exports = { solveRecaptchaV2, solveTurnstile };

الخطوة 2: تهيئة Puppeteer بإعداد التخفي

اضبط المتصفح لتقليل مؤشرات الأتمتة قبل زيارة الصفحة. تؤدي الإعدادات أدناه ثلاث مهام: تعطّل خاصية AutomationControlled التي تكشف عن القيادة الآلية، وتضبط وكيل مستخدم يشبه متصفحاً حقيقياً، وتخفي علامة navigator.webdriver التي تقرأها كثير من نصوص الحماية. تكفي هذه التهيئة لمعظم الصفحات، أما المواقع الأكثر صرامة فقد تتطلب إضافة متخصصة مثل puppeteer-extra-plugin-stealth. تذكّر أن الحل نفسه يتم على خوادم CaptchaAI؛ دور هذه الإعدادات هو إبقاء الجلسة مستقرة حتى لحظة الحقن.

const puppeteer = require("puppeteer");

async function createBrowser() {
  const browser = await puppeteer.launch({
    headless: "new",
    args: [
      "--no-sandbox",
      "--disable-setuid-sandbox",
      "--disable-blink-features=AutomationControlled",
    ],
  });

  const page = await browser.newPage();
  await page.setUserAgent(
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
  );

  // Hide automation indicators
  await page.evaluateOnNewDocument(() => {
    Object.defineProperty(navigator, "webdriver", { get: () => false });
  });

  return { browser, page };
}

الخطوة 3: حل reCAPTCHA وحقن الرمز في الصفحة

الآن اربط الطرفين. بعد تحميل الصفحة، استخرج قيمة data-sitekey من عنصر .g-recaptcha، وهي المعرّف الذي يحتاجه CaptchaAI ليعرف أي تحدٍ يحل. أرسلها إلى وحدة الحل، وانتظر الرمز، ثم اكتبه داخل الحقل المخفي g-recaptcha-response قبل الضغط على زر الإرسال. الحقن الفوري مهم لأن رموز reCAPTCHA محدودة الصلاحية؛ أي تأخير طويل قد يجعل الموقع يرفض الرمز رغم صحته. وإن ظهر التحدي بعد التحميل الأولي، فاسبق الاستخراج بـ page.waitForSelector('.g-recaptcha').

const { solveRecaptchaV2 } = require("./solver");

async function scrapeWithCaptcha(url) {
  const { browser, page } = await createBrowser();

  try {
    await page.goto(url, { waitUntil: "networkidle2" });

    // Extract site key
    const siteKey = await page.$eval(
      ".g-recaptcha",
      (el) => el.getAttribute("data-sitekey")
    );
    console.log("Site key:", siteKey);

    // Solve with CaptchaAI
    const token = await solveRecaptchaV2(siteKey, url);
    console.log("Token received:", token.substring(0, 50));

    // Inject token
    await page.evaluate((token) => {
      document.getElementById("g-recaptcha-response").innerHTML = token;
      document.getElementById("g-recaptcha-response").style.display = "";
    }, token);

    // Submit the form
    await page.click('button[type="submit"]');
    await page.waitForNavigation({ waitUntil: "networkidle2" });

    // Scrape the content
    const content = await page.content();
    console.log("Page loaded successfully");
    return content;
  } finally {
    await browser.close();
  }
}

الخطوة 4: التعامل مع دوال رد النداء (Callback)

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

// Trigger the reCAPTCHA callback
await page.evaluate((token) => {
  // Method 1: Direct callback
  if (typeof ___grecaptcha_cfg !== "undefined") {
    const clients = ___grecaptcha_cfg.clients;
    Object.keys(clients).forEach((key) => {
      const client = clients[key];
      // Find the callback function
      const findCallback = (obj) => {
        for (const prop in obj) {
          if (typeof obj[prop] === "function") {
            obj[prop](token);
            return true;
          }
          if (typeof obj[prop] === "object" && obj[prop] !== null) {
            if (findCallback(obj[prop])) return true;
          }
        }
        return false;
      };
      findCallback(client);
    });
  }
}, token);

المثال الكامل القابل للتشغيل

يجمع المثال التالي كل ما سبق في ملف واحد قابل للتشغيل مباشرة. استبدل YOUR_API_KEY بمفتاحك الفعلي، وعدّل عنوان الصفحة ومحدّد الزر (#submit-btn) بما يناسب موقعك. لاحظ أن دالة الحل هنا تستطلع في حلقة مفتوحة؛ في الإنتاج يُفضّل إضافة سقف للمحاولات كما في وحدة الخطوة الأولى.

const puppeteer = require("puppeteer");
const axios = require("axios");

const API_KEY = "YOUR_API_KEY";

async function solveCaptcha(siteKey, pageUrl) {
  const submit = await axios.get("https://ocr.captchaai.com/in.php", {
    params: {
      key: API_KEY,
      method: "userrecaptcha",
      googlekey: siteKey,
      pageurl: pageUrl,
    },
  });
  const taskId = submit.data.split("|")[1];

  while (true) {
    await new Promise((r) => setTimeout(r, 5000));
    const result = await axios.get("https://ocr.captchaai.com/res.php", {
      params: { key: API_KEY, action: "get", id: taskId },
    });
    if (result.data === "CAPCHA_NOT_READY") continue;
    if (result.data.startsWith("OK|")) return result.data.split("|")[1];
    throw new Error(result.data);
  }
}

(async () => {
  const browser = await puppeteer.launch({
    headless: "new",
    args: ["--disable-blink-features=AutomationControlled"],
  });
  const page = await browser.newPage();

  try {
    await page.goto("https://example.com/login", {
      waitUntil: "networkidle2",
    });

    // Get the site key
    const siteKey = await page.$eval(".g-recaptcha", (el) =>
      el.getAttribute("data-sitekey")
    );

    // Solve
    const token = await solveCaptcha(siteKey, page.url());

    // Inject and submit
    await page.evaluate((t) => {
      document.getElementById("g-recaptcha-response").innerHTML = t;
    }, token);

    await page.click("#submit-btn");
    await page.waitForNavigation();

    console.log("Done:", page.url());
  } finally {
    await browser.close();
  }
})();

reCAPTCHA مقابل Turnstile: فروق التنفيذ

رغم وحدة المنطق، يختلف النوعان في تفاصيل صغيرة. عند الاستخراج، تقرأ data-sitekey من .g-recaptcha لـ reCAPTCHA، ومن .cf-turnstile لـ Turnstile. وعند الإرسال، تستخدم method=userrecaptcha مع googlekey للأول، وmethod=turnstile مع sitekey للثاني. أما الحقن، فيذهب رمز reCAPTCHA إلى الحقل g-recaptcha-response، ورمز Turnstile إلى الحقل cf-turnstile-response. باستثناء هذه الأسماء، تبقى دورة الإرسال ثم الاستطلاع ثم الحقن متطابقة، ما يجعل إضافة نوع جديد مسألة دقائق.

أخطاء شائعة وكيفية معالجتها

تعود أغلب الأعطال إلى التوقيت أو إلى عنصر لم يُحمّل بعد. يربط الجدول التالي كل حالة متكررة بسببها وبالإجراء المباشر:

المشكلة السبب الإجراء
فشل page.$eval يتم تحميل اختبار CAPTCHA بعد العرض الأولي استخدم page.waitForSelector('.g-recaptcha')
الرمز المميز لا يعمل انتهت قبل التقديم حقن مباشرة بعد الاستلام
يكتشف الموقع Puppeteer تكوين التخفي مفقود استخدم puppeteer-extra-plugin-stealth
Navigation timeout لم يتم التنقل في الصفحة بعد الإرسال تحقق مما إذا كان الموقع يستخدم AJAX بدلاً من نشر النموذج

اختيار الوضع headless أو المرئي

هل أشغّل المتصفح بلا واجهة (headless) أم بواجهة مرئية؟ بما أن CaptchaAI يحل التحدي من جانب الخادم، فإن الوضع بلا واجهة يعمل بكفاءة تامة في الإنتاج ويستهلك موارد أقل. أما الوضع المرئي فأبقِه للتطوير وتصحيح الأخطاء حين تحتاج إلى رؤية ما يجري. القاعدة العملية: طوّر بواجهة مرئية، وانشر بلا واجهة.

توسيع النطاق والتشغيل المتوازي

عند الانتقال من صفحة واحدة إلى آلاف الصفحات، يصبح عدد الطلبات المتزامنة هو العامل الحاسم في التكلفة. يعتمد تسعير CaptchaAI على عدد الـ Threads المتزامنة لا على عدد عمليات الحل، مع حل غير محدود لكل Thread طوال الشهر. تبدأ خطة BASIC من 15 دولاراً بخمسة Threads، وترتفع ADVANCE إلى 90 دولاراً بخمسين Thread، وصولاً إلى VIP-3 بسعر 7,500 دولار وخمسة آلاف Thread. اختر العدد بما يوازي صفحات Puppeteer التي تشغّلها معاً عبر Promise.all().

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

كم Thread أحتاج للحل المتوازي عبر Promise.all؟

كل عملية حل قيد التنفيذ تشغل Thread واحداً حتى تنتهي، ثم يتحرر للمهمة التالية. إن شغّلت عشر صفحات Puppeteer معاً وكلها تنتظر رموزاً في اللحظة نفسها، فأنت بحاجة إلى عشرة Threads على الأقل. خطة ADVANCE بخمسين Thread تغطي معظم أحمال الاستخراج المتوسطة.

ماذا عن hCaptcha وFunCaptcha؟

هذان النوعان غير مدعومين حالياً في CaptchaAI، فلا تبنِ مسارك عليهما. الأنواع المتاحة تشمل reCAPTCHA بكل إصداراته وCloudflare Turnstile وChallenge وGeeTest v3 والكابتشا الصورية، إضافة إلى CaptchaFox وFriendly Captcha وLemin في مرحلة تجريبية (beta). وGeeTest v4 قيد الإعداد وليس متوفراً بعد.

لماذا يُرفض الرمز أحياناً رغم نجاح الحل؟

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

هل يعمل هذا المسار مع صفحات تعتمد على AJAX؟

نعم، لكن انتبه إلى ما بعد الإرسال. إذا أرسل الموقع النموذج عبر طلب في الخلفية بدل إعادة تحميل الصفحة، فلن يعمل page.waitForNavigation()؛ استبدله بانتظار عنصر النتيجة أو استجابة الشبكة عبر page.waitForResponse().

أدلة ذات صلة

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