التكاملات

دمج Crawlee مع CaptchaAI لحل CAPTCHA أثناء استخراج البيانات

يتوقّف زاحف Crawlee عن التقدّم لحظة ظهور اختبار reCAPTCHA v2 على الصفحة. الحل ليس إيقاف الأتمتة، بل إضافة طبقة لحل CAPTCHA داخل دالة requestHandler: يستقبل CaptchaAI مفتاح الموقع عبر واجهة HTTP، ويعيد رمز g-recaptcha-response جاهزاً للحقن في النموذج. يشرح هذا الدليل كيف تربط CaptchaAI بأنواع زواحف Crawlee الثلاثة — CheerioCrawler وPlaywrightCrawler والزاحف المدرك للجلسة — مع الحفاظ على تزامن عالٍ واستئناف تلقائي للطلبات الفاشلة.

Crawlee إطار عمل حديث لـ Node.js من Apify، صُمّم للزحف واسع النطاق. لا يحمل حلّاً مدمجاً لاختبارات CAPTCHA، لكنه يوفّر كل ما يحيط بعملية الحل، فيبقى عليك ربط الخدمة فقط.


لماذا يُعدّ Crawlee بيئة مناسبة لحل CAPTCHA

قبل كتابة أي سطر، من المفيد إدراك أن Crawlee يتكفّل بالبنية التحتية التي كنت ستبنيها يدوياً حول أي خدمة حل — الجلسات وإعادة المحاولة والبروكسي وقائمة الانتظار:

الميزة في Crawlee ما الذي تقدّمه عند دمج CaptchaAI
إدارة الجلسات المدمجة بصمات ثابتة تبقى صالحة بعد حل الاختبار وحقن الرمز
إعادة المحاولة التلقائية استئناف الطلبات الفاشلة بعد اكتمال الحل دون كود إضافي
تدوير الخوادم الوسيطة يعمل جنباً إلى جنب مع البروكسي لتقليل تكرار ظهور الاختبارات
قائمة انتظار الطلبات جدولة عمليات الحل بالتوازي مع استخراج البيانات

كيف يحل CaptchaAI اختبار reCAPTCHA في أربع خطوات

يتبع الدمج المنطق نفسه بصرف النظر عن نوع الزاحف الذي تستخدمه:

  1. الاكتشاف: يفحص الزاحف الصفحة عن عنصر يحمل السمة data-sitekey لاستخراج مفتاح الموقع.
  2. الإرسال: تُرسَل حمولة الطلب إلى in.php بالأسلوب userrecaptcha مع مفتاح الموقع وعنوان الصفحة.
  3. الاستطلاع الدوري: يُستفسَر عن النتيجة من res.php كل بضع ثوانٍ حتى تتحوّل الحالة من CAPCHA_NOT_READY إلى الرمز الجاهز.
  4. الحقن: يوضَع الرمز في الحقل g-recaptcha-response ويُرسَل النموذج، فيكمل الزاحف استخراج البيانات.

التكامل الأساسي مع CheerioCrawler

const { CheerioCrawler } = require('crawlee');
const https = require('https');

const API_KEY = process.env.CAPTCHAAI_API_KEY;

async function solveCaptcha(sitekey, pageurl) {
    // Submit task
    const submitData = new URLSearchParams({
        key: API_KEY,
        method: 'userrecaptcha',
        googlekey: sitekey,
        pageurl: pageurl,
        json: '1',
    });

    const submitResp = await fetch('https://ocr.captchaai.com/in.php', {
        method: 'POST',
        body: submitData,
    });
    const submitResult = await submitResp.json();

    if (submitResult.status !== 1) {
        throw new Error(`Submit error: ${submitResult.request}`);
    }

    const taskId = submitResult.request;

    // Poll for result
    await new Promise(r => setTimeout(r, 15000));

    for (let i = 0; i < 24; i++) {
        const pollResp = await fetch(
            `https://ocr.captchaai.com/res.php?key=${API_KEY}&action=get&id=${taskId}&json=1`
        );
        const pollResult = await pollResp.json();

        if (pollResult.status === 1) return pollResult.request;
        if (pollResult.request !== 'CAPCHA_NOT_READY') {
            throw new Error(`Solve error: ${pollResult.request}`);
        }

        await new Promise(r => setTimeout(r, 5000));
    }

    throw new Error('Solve timeout');
}

// Crawlee spider with CAPTCHA handling
const crawler = new CheerioCrawler({
    maxConcurrency: 5,
    requestHandlerTimeoutSecs: 180,

    async requestHandler({ request, $, log }) {
        // Check if page has CAPTCHA
        const captchaDiv = $('[data-sitekey]');

        if (captchaDiv.length > 0) {
            const sitekey = captchaDiv.attr('data-sitekey');
            log.info(`CAPTCHA found on ${request.url}, solving...`);

            const token = await solveCaptcha(sitekey, request.url);
            log.info('CAPTCHA solved, submitting form');

            // Submit form with token
            const formData = new URLSearchParams({
                'g-recaptcha-response': token,
            });

            const resp = await fetch(request.url, {
                method: 'POST',
                body: formData,
            });
            const html = await resp.text();
            // Parse the result page...
        }

        // Extract data
        const title = $('title').text();
        const data = $('table tr').map((i, row) => ({
            col1: $(row).find('td:eq(0)').text().trim(),
            col2: $(row).find('td:eq(1)').text().trim(),
        })).get();

        log.info(`Scraped ${data.length} rows from ${request.url}`);
    },

    failedRequestHandler({ request, log }) {
        log.error(`Failed: ${request.url}`);
    },
});

// Run
(async () => {
    await crawler.run([
        'https://example.com/page1',
        'https://example.com/page2',
    ]);
})();

يعرّف هذا المثال دالة solveCaptcha القابلة لإعادة الاستخدام في جميع الأمثلة اللاحقة. تبدأ بإرسال المهمة إلى in.php، ثم تنتظر خمس عشرة ثانية قبل أول استطلاع، وتكرّر الاستفسار من res.php حتى أربع وعشرين مرة بفاصل خمس ثوانٍ. داخل requestHandler يبحث CheerioCrawler عن السمة data-sitekey؛ فإن وُجدت، يُحَل الاختبار ويُرسَل الرمز مع النموذج قبل متابعة استخراج الصفوف. اضبط maxConcurrency بما يتناسب مع عدد الـ Threads المتاحة في خطتك حتى لا تتجاوز الطلبات المتزامنة سعة الحساب.


معالجة الصفحات الديناميكية مع PlaywrightCrawler

const { PlaywrightCrawler } = require('crawlee');

const crawler = new PlaywrightCrawler({
    maxConcurrency: 3,
    requestHandlerTimeoutSecs: 180,
    launchContext: {
        launchOptions: {
            headless: true,
            args: ['--disable-blink-features=AutomationControlled'],
        },
    },

    async requestHandler({ request, page, log }) {
        await page.goto(request.url, { waitUntil: 'networkidle' });

        // Check for reCAPTCHA
        const sitekey = await page.evaluate(() => {
            const el = document.querySelector('[data-sitekey]');
            return el ? el.getAttribute('data-sitekey') : null;
        });

        if (sitekey) {
            log.info(`CAPTCHA detected, solving for ${request.url}`);

            const token = await solveCaptcha(sitekey, request.url);

            // Inject token
            await page.evaluate((t) => {
                const ta = document.querySelector('[name="g-recaptcha-response"]');
                if (ta) {
                    ta.style.display = 'block';
                    ta.value = t;
                }
                // Trigger callback
                const widget = document.querySelector('.g-recaptcha');
                if (widget) {
                    const cb = widget.getAttribute('data-callback');
                    if (cb && typeof window[cb] === 'function') {
                        window[cb](t);
                    }
                }
            }, token);

            await page.click('button[type="submit"]');
            await page.waitForNavigation({ waitUntil: 'networkidle' });
        }

        // Extract data
        const title = await page.title();
        const content = await page.textContent('body');
        log.info(`Page: ${title}, length: ${content.length}`);
    },
});

يفيد PlaywrightCrawler عندما تُعرَض الصفحة أو مفتاح الموقع عبر JavaScript، إذ يشغّل متصفحاً حقيقياً بدل الاكتفاء بتحليل HTML الثابت. بعد الحصول على الرمز، يحقنه المثال في حقل g-recaptcha-response، ثم يستدعي دالة data-callback إن كانت الصفحة تعتمد عليها، وأخيراً ينقر زر الإرسال وينتظر انتقال الصفحة. هذا النمط ضروري للمواقع التي لا يظهر فيها النموذج إلا بعد تنفيذ سكربتات العميل — وهو ما يعجز عنه CheerioCrawler.


حل CAPTCHA المرتبط بالجلسة

const { CheerioCrawler, Session } = require('crawlee');

const crawler = new CheerioCrawler({
    useSessionPool: true,
    sessionPoolOptions: {
        maxPoolSize: 10,
        sessionOptions: {
            maxUsageCount: 50,
        },
    },

    async requestHandler({ request, $, session, log }) {
        // If blocked, solve CAPTCHA and mark session as usable
        if ($('.captcha-container').length > 0) {
            const sitekey = $('[data-sitekey]').attr('data-sitekey');
            const token = await solveCaptcha(sitekey, request.url);

            // Store token in session for subsequent requests
            session.userData = session.userData || {};
            session.userData.captchaToken = token;
            session.userData.tokenTime = Date.now();

            log.info('CAPTCHA solved, session updated');
        }

        // Normal scraping
        const items = $('div.item').map((i, el) => ({
            name: $(el).find('.name').text().trim(),
            price: $(el).find('.price').text().trim(),
        })).get();

        log.info(`Found ${items.length} items`);
    },
});

يربط هذا النمط الرمز بالجلسة عبر session.userData، فتعيد الطلبات اللاحقة استخدام الجلسة نفسها بدل حل اختبار جديد في كل مرة. مع useSessionPool وmaxPoolSize، يوزّع Crawlee الطلبات على مجموعة من الجلسات، ويتيح لك تخزين وقت الحل للتحقق من صلاحية الرمز قبل الاعتماد عليه. النتيجة عدد أقل من عمليات الحل، وهو ما ينعكس مباشرة على استهلاك الـ Threads والتكلفة الشهرية.


ضبط التزامن والتكلفة في بيئة الإنتاج

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

تخيّل فريقاً في الرياض يراقب أسعار العقارات على موقع إعلانات عام يعرض reCAPTCHA v2 بشكل متقطّع. إذا ضبطت maxConcurrency على 5 وكانت كل صفحة قد تحتاج إلى حل، فإن خطة BASIC — 15 دولاراً شهرياً مقابل 5 threads — تكفي للبداية. ومع توسّع الزحف إلى عشرات الصفحات بالتوازي، تنتقل إلى STANDARD — 30 دولاراً و15 thread — أو ADVANCE — 90 دولاراً و50 thread. جميع الأسعار بالدولار الأمريكي، والحل غير المحدود داخل الخطة يجعل التكلفة الشهرية ثابتة ويمكن التنبؤ بها مهما ارتفع حجم الاستخراج.

عند الانتقال إلى الإنتاج، راقب ثلاثة مؤشرات لضبط الأداء والتكلفة:

  • معدل ظهور الاختبار: نسبة الصفحات التي تعرض reCAPTCHA فعلاً، فهي التي تحدّد عدد الـ Threads المطلوبة لا إجمالي الروابط.
  • زمن الحل: متوسط الفترة بين الإرسال ووصول الرمز، ويوجّه ضبط requestHandlerTimeoutSecs كي لا تُقطَع الطلبات مبكراً.
  • معدل النجاح: نسبة الطلبات التي اكتملت دون استثناء، ومنها تعرف متى تعتمد على آلية إعادة المحاولة المدمجة.

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

كم عدد الـ Threads التي أحتاجها لتشغيل Crawlee بتزامن عالٍ؟

يعتمد العدد على قيمة maxConcurrency وعلى نسبة الصفحات التي تعرض اختباراً فعلياً. كقاعدة عملية، اجعل عدد الـ Threads مساوياً لأعلى عدد من عمليات الحل المتزامنة المتوقعة؛ فإن كان maxConcurrency يساوي 5 وكل صفحة قد تحتاج حلاً، فابدأ بخطة تضمن 5 threads على الأقل.

هل يحل CaptchaAI أنواع CAPTCHA أخرى غير reCAPTCHA v2 داخل Crawlee؟

نعم. تدعم الخدمة reCAPTCHA v2 وv3، وCloudflare Turnstile وCloudflare Challenge، وGeeTest v3، والصور وOCR، والشبكات الصورية، وBLS. أما hCaptcha وFunCaptcha فغير مدعومين حالياً، لذا خطّط لبديل إن واجهتهما أثناء الزحف. يكفي تغيير قيمة method في دالة الإرسال للانتقال بين الأنواع المدعومة.

كيف أتعامل مع انتهاء مهلة الحل أو رمز خطأ من الخدمة؟

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

هل يمكنني تشغيل Crawlee مع CaptchaAI على Apify؟

نعم. انشر مشروع Crawlee بوصفه Actor على Apify، واستدعِ CaptchaAI عبر طلبات HTTP كما في الأمثلة أعلاه. خزّن مفتاح الـ API كمتغيّر بيئة على Apify بدل كتابته داخل الكود.


أدلة ذات صلة


أضِف حل CAPTCHA إلى مشروع Crawlee خلال دقائق — احصل على مفتاح CaptchaAI الآن.

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