عندما ينتقل سكربت الأتمتة من صفحة تجريبية إلى موقع إنتاجي، لا يعود حل الكابتشا سطرًا واحدًا في الكود، بل سلسلة تحديات متشابكة: كشف نوع الكابتشا، والوصول إليه داخل إطارات iframe معزولة، وتمرير الرمز إلى النموذج الصحيح، وتكرار ذلك عبر عشرات الصفحات المتوازية. يجمع هذا الدليل ستة أنماط عملية تربط Puppeteer بواجهة CaptchaAI API لتغطية هذه الحالات كما تظهر فعليًا: الوضع الخفي، واعتراض الطلبات، وحل reCAPTCHA داخل iframe، والنماذج متعددة الصفحات، والتشغيل المتوازي، والحل المعتمد على لقطة الشاشة. كل نمط قائم بذاته، والمبدأ الثابت واحد: يتحكّم Puppeteer في المتصفح، ويتولّى CaptchaAI الحل الفعلي على خوادمه، وتبقى مهمتك توصيل الطرفين بموثوقية.
المتطلبات الأساسية قبل البدء
تبدأ كل الأنماط اللاحقة من تثبيت الحزم الثلاث التالية: مكتبة Puppeteer نفسها، بالإضافة إلى puppeteer-extra وإضافة التخفي puppeteer-extra-plugin-stealth التي تخفي بصمات الأتمتة الشائعة عن مواقع الويب.
npm install puppeteer puppeteer-extra puppeteer-extra-plugin-stealth
بعد التثبيت تحتاج إلى مفتاح CaptchaAI API من لوحة التحكم لتضعه في المتغير API_KEY. احتفظ به في متغير بيئة عند الإنتاج بدلًا من كتابته داخل الكود.
إعداد Puppeteer في الوضع الخفي (Stealth)
الوضع الخفي هو خط الدفاع الأول ضد أنظمة الكشف. بدون إضافة التخفي تكشف المواقع عن Puppeteer عبر خصائص مثل navigator.webdriver، فيرتفع تكرار ظهور الكابتشا أو يُحظر الطلب قبل الحل. وتفعيل puppeteer.use(StealthPlugin()) يعالج عشرات هذه البصمات دفعةً واحدة.
const puppeteer = require("puppeteer-extra");
const StealthPlugin = require("puppeteer-extra-plugin-stealth");
puppeteer.use(StealthPlugin());
const API_KEY = "YOUR_API_KEY";
async function createBrowser() {
const browser = await puppeteer.launch({
headless: "new",
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-web-security",
"--disable-features=IsolateOrigins,site-per-process",
],
});
return browser;
}
الوسيطتان --disable-web-security و--disable-features=IsolateOrigins,site-per-process مهمّتان مع reCAPTCHA لأنهما تسهّلان الوصول إلى إطارات iframe متعددة الأصل. وبما أنهما تُضعفان عزل الأصول، استخدمهما في بيئة أتمتة معزولة لا في متصفح شخصي.
دالة مساعدة لحل الكابتشا عبر CaptchaAI
قبل أي نمط متقدم نحتاج إلى دالة واحدة تتكفّل بدورة الحل الكاملة: إرسال المهمة إلى in.php، ثم الاستطلاع الدوري عن النتيجة من res.php حتى تجهز. هذه الدالة هي القلب المشترك بين الأنماط التالية، وكل ما تفعله بقيتها هو تجهيز الوسائط الصحيحة ثم استدعاؤها.
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function solveCaptcha(method, params) {
const submitResp = await fetch("https://ocr.captchaai.com/in.php", {
method: "POST",
body: new URLSearchParams({
key: API_KEY,
method,
json: "1",
...params,
}),
});
const submitData = await submitResp.json();
if (submitData.status !== 1)
throw new Error(`Submit: ${submitData.request}`);
const taskId = submitData.request;
for (let i = 0; i < 30; i++) {
await sleep(5000);
const pollResp = await fetch(
`https://ocr.captchaai.com/res.php?${new URLSearchParams({
key: API_KEY,
action: "get",
id: taskId,
json: "1",
})}`
);
const data = await pollResp.json();
if (data.status === 1) return data.request;
if (data.request === "ERROR_CAPTCHA_UNSOLVABLE") throw new Error("Unsolvable");
}
throw new Error("Timed out");
}
تستطلع الحلقة النتيجة كل خمس ثوانٍ حتى ثلاثين مرة (مهلة قصوى 150 ثانية) قبل أن تُطلق خطأ Timed out. ومعالجة ERROR_CAPTCHA_UNSOLVABLE صراحةً تتيح إعادة المحاولة بدل الانتظار الطويل.
حل reCAPTCHA v2 داخل إطار iframe
المشكلة الأكثر شيوعًا مع reCAPTCHA أنه يُعرَض داخل إطار iframe منفصل، والنقر داخله مباشرةً غير موثوق. الحل الأنظف هو استخراج مفتاح الموقع (sitekey)، وإرساله إلى CaptchaAI، ثم حقن الرمز في حقل g-recaptcha-response داخل الصفحة الأم لا داخل الإطار.
async function solveRecaptchaInIframe(page) {
// Wait for the reCAPTCHA iframe to load
await page.waitForSelector('iframe[src*="recaptcha"]', { timeout: 10000 });
// Get the sitekey from the main page
const sitekey = await page.evaluate(() => {
// From data-sitekey attribute
const el = document.querySelector("[data-sitekey]");
if (el) return el.getAttribute("data-sitekey");
// From iframe src
const iframe = document.querySelector('iframe[src*="recaptcha"]');
if (iframe) {
const match = iframe.src.match(/k=([A-Za-z0-9_-]{40})/);
if (match) return match[1];
}
return null;
});
if (!sitekey) throw new Error("Sitekey not found");
// Solve via CaptchaAI
const token = await solveCaptcha("userrecaptcha", {
googlekey: sitekey,
pageurl: page.url(),
});
// Inject token into the main page (not the iframe)
await page.evaluate((t) => {
document.getElementById("g-recaptcha-response").value = t;
document.getElementById("g-recaptcha-response").style.display = "block";
// Trigger the callback
if (typeof ___grecaptcha_cfg !== "undefined") {
const clients = ___grecaptcha_cfg.clients;
for (const key in clients) {
const client = clients[key];
for (const prop in client) {
try {
if (client[prop] && typeof client[prop].callback === "function") {
client[prop].callback(t);
}
} catch {}
}
}
}
}, token);
return token;
}
يستخرج الكود مفتاح الموقع من data-sitekey، وإن لم يجده يلتقطه من رابط الإطار. الخطوة الحاسمة هي استدعاء دالة رد النداء (callback) بعد حقن الرمز؛ فكثير من النماذج لا تُفعِّل زر الإرسال إلا بعدها. للتفاصيل راجع دليل حل reCAPTCHA v2 عبر الـ API.
اعتراض الطلبات للكشف عن نوع الكابتشا تلقائيًا
عندما لا تعرف مسبقًا نوع الكابتشا في كل موقع، يصبح اعتراض الطلبات الشبكية أداة قوية للكشف التلقائي: راقب الطلبات الصادرة، فرابط يحتوي recaptcha/api2 يعني reCAPTCHA، ورابط نحو challenges.cloudflare.com/turnstile يعني Cloudflare Turnstile. بذلك يعمل السكربت نفسه على عدة مواقع دون تعديل يدوي.
async function interceptAndSolve(page, url) {
const captchaData = {};
// Intercept requests to detect CAPTCHA type
await page.setRequestInterception(true);
page.on("request", (request) => {
const requestUrl = request.url();
if (requestUrl.includes("recaptcha/api2")) {
captchaData.type = "recaptcha";
const match = requestUrl.match(/k=([A-Za-z0-9_-]{40})/);
if (match) captchaData.sitekey = match[1];
}
if (requestUrl.includes("challenges.cloudflare.com/turnstile")) {
captchaData.type = "turnstile";
}
if (requestUrl.includes("geetest") || requestUrl.includes("gt=")) {
captchaData.type = "geetest";
}
request.continue();
});
// Intercept responses for CAPTCHA parameters
page.on("response", async (response) => {
if (response.url().includes("geetest") || response.url().includes("register")) {
try {
const data = await response.json();
if (data.gt && data.challenge) {
captchaData.gt = data.gt;
captchaData.challenge = data.challenge;
}
} catch {}
}
});
await page.goto(url, { waitUntil: "networkidle2" });
// Now solve based on detected type
if (captchaData.type === "recaptcha" && captchaData.sitekey) {
return await solveCaptcha("userrecaptcha", {
googlekey: captchaData.sitekey,
pageurl: url,
});
}
if (captchaData.type === "turnstile") {
const sitekey = await page.evaluate(() => {
const el = document.querySelector("[data-sitekey]");
return el ? el.getAttribute("data-sitekey") : null;
});
if (sitekey) {
return await solveCaptcha("turnstile", { sitekey, pageurl: url });
}
}
if (captchaData.type === "geetest" && captchaData.gt) {
return await solveCaptcha("geetest", {
gt: captchaData.gt,
challenge: captchaData.challenge,
pageurl: url,
});
}
return null;
}
لاحظ الفارق بين معالجَي الأحداث: request يلتقط النوع والمفتاح من الروابط الصادرة، بينما response يقرأ حمولة JSON لالتقاط قيمتي gt وchallenge اللازمتين لـ GeeTest، ثم يوجّه المهمة إلى الطريقة المناسبة.
أتمتة نموذج متعدد الصفحات يحتوي على كابتشا
في تدفقات التسجيل الواقعية لا يظهر الكابتشا في صفحة واحدة بل في نهاية سلسلة خطوات: بيانات شخصية، ثم عنوان، ثم صفحة تحقق أخيرة تحمل الكابتشا. الدالة التالية تمرّ على الخطوات بالترتيب، وتملأ الحقول بمحاكاة كتابة بشرية، وتتحقق من وجود كابتشا في كل خطوة.
async function multiPageFormFlow(browser, startUrl, formSteps) {
const page = await browser.newPage();
for (let i = 0; i < formSteps.length; i++) {
const step = formSteps[i];
console.log(`Step ${i + 1}: ${step.description}`);
if (i === 0) {
await page.goto(startUrl, { waitUntil: "networkidle2" });
}
// Fill form fields
for (const [selector, value] of Object.entries(step.fields || {})) {
await page.waitForSelector(selector, { visible: true });
await page.click(selector, { clickCount: 3 }); // Select all
await page.type(selector, value, { delay: 50 }); // Human-like typing
}
// Handle dropdowns
for (const [selector, value] of Object.entries(step.selects || {})) {
await page.select(selector, value);
}
// Check for CAPTCHA
const hasCaptcha = await page.evaluate(() => {
return !!(
document.querySelector("[data-sitekey]") ||
document.querySelector(".cf-turnstile") ||
document.querySelector(".geetest_holder")
);
});
if (hasCaptcha) {
console.log("CAPTCHA detected, solving...");
const token = await interceptAndSolve(page, page.url());
if (token) {
await page.evaluate((t) => {
const el = document.getElementById("g-recaptcha-response");
if (el) el.value = t;
const cf = document.querySelector('[name="cf-turnstile-response"]');
if (cf) cf.value = t;
}, token);
}
}
// Click next/submit
if (step.submit) {
await page.click(step.submit);
await page.waitForNavigation({ waitUntil: "networkidle2" });
}
}
return page;
}
// Usage
const browser = await createBrowser();
const page = await multiPageFormFlow(browser, "https://example.com/register", [
{
description: "Personal info",
fields: { "#name": "John Doe", "#email": "john@example.com" },
submit: "#next-btn",
},
{
description: "Address",
fields: { "#address": "123 Main St", "#city": "New York" },
selects: { "#state": "NY" },
submit: "#next-btn",
},
{
description: "Verification (has CAPTCHA)",
fields: {},
submit: "#submit-btn",
},
]);
الخيار delay يحاكي إيقاع الإدخال البشري ويقلّل احتمال إطلاق تحقق إضافي. ولأن الدالة تحقن الرمز في حقلَي g-recaptcha-response وcf-turnstile-response معًا، تصلح لنماذج reCAPTCHA أو Turnstile دون معرفة النوع مسبقًا. وهو نمط مفيد لفرق ضمان الجودة في المنطقة العربية التي تختبر تدفقات تسجيل طويلة على منصات محلية للتجارة الإلكترونية أو بوابات الخدمات، حيث يقع الكابتشا في الخطوة الأخيرة.
الحل المتوازي عبر عدة متصفحات Puppeteer
عند التعامل مع مئات الروابط لا يكفي الحل التسلسلي. الدالة التالية تفتح عدة صفحات ضمن المتصفح نفسه، وتحدّ المهام المتزامنة عبر maxConcurrent، وتوزّع الروابط على عمّال يسحب كل منهم الرابط التالي من قائمة الانتظار فور فراغه.
async function parallelSolve(urls, maxConcurrent = 3) {
const browser = await createBrowser();
const results = [];
let running = 0;
const processUrl = async (url) => {
const page = await browser.newPage();
try {
await page.goto(url, { waitUntil: "networkidle2" });
// Detect and extract sitekey
const sitekey = await page.evaluate(() => {
const el = document.querySelector("[data-sitekey]");
return el ? el.getAttribute("data-sitekey") : null;
});
if (sitekey) {
const token = await solveCaptcha("userrecaptcha", {
googlekey: sitekey,
pageurl: url,
});
results.push({ url, status: "solved", token });
} else {
results.push({ url, status: "no-captcha" });
}
} catch (error) {
results.push({ url, status: "error", error: error.message });
} finally {
await page.close();
}
};
// Process with concurrency limit
const queue = [...urls];
const workers = [];
for (let i = 0; i < Math.min(maxConcurrent, urls.length); i++) {
workers.push(
(async () => {
while (queue.length > 0) {
const url = queue.shift();
if (url) await processUrl(url);
}
})()
);
}
await Promise.all(workers);
await browser.close();
return results;
}
هنا يظهر الارتباط بين تزامن المتصفحات وخطة CaptchaAI: كل صفحة تُرسل طلب حل تحجز Thread واحدًا. فإذا شغّلت عشر صفحات متوازية احتجت إلى خطة توفّر عددًا كافيًا من الـ Threads حتى لا تتحوّل الطلبات إلى طابور انتظار؛ فخطة مثل ADVANCE بسعر $90 شهريًا تتيح 50 Thread مع حلول غير محدودة لكل Thread. والنموذج قائم على عدد الـ Threads لا عمليات الحل، ما يجعل التكلفة قابلة للتوقّع عند رفع الحجم.
حل الكابتشا المعتمد على لقطة الشاشة
بعض المواقع تعرض كابتشا صوريًا بسيطًا (نص مشوّه) بدل الأنواع القياسية. هنا تلتقط صورة للعنصر فقط، وترسلها كسلسلة base64 إلى CaptchaAI، ثم تكتب الإجابة في حقل الإدخال.
async function solveScreenshotCaptcha(page, captchaSelector) {
const element = await page.$(captchaSelector);
if (!element) throw new Error("CAPTCHA element not found");
// Take a screenshot of just the CAPTCHA element
const screenshot = await element.screenshot({ encoding: "base64" });
// Solve via CaptchaAI
const answer = await solveCaptcha("base64", { body: screenshot });
// Type the answer
const input = await page.$(
'input[name="captcha"], input[name="code"], input.captcha-input'
);
if (input) {
await input.click({ clickCount: 3 });
await input.type(answer, { delay: 30 });
}
return answer;
}
الميزة أنك تلتقط العنصر المستهدف فقط عبر element.screenshot، فتصل صورة نظيفة ترفع دقة القراءة الضوئية (OCR). ومحدّد الإدخال يجرّب عدة أسماء شائعة للحقل بدل تخصيصه يدويًا لكل موقع.
استكشاف الأخطاء وإصلاحها
أكثر الأعطال شيوعًا في الإنتاج وسببها وإصلاحها.
| العَرَض | السبب | الإصلاح |
|---|---|---|
| اكتُشف Puppeteer باعتباره روبوتًا | عدم تفعيل إضافة التخفي | أضف puppeteer-extra-plugin-stealth |
| فشل حقن رمز reCAPTCHA | وجود أكثر من أداة reCAPTCHA في الصفحة | حدّد حقل g-recaptcha-response الصحيح حسب الفهرس |
| التبديل بين إطارات iframe لا يعمل | إطار iframe متعدد الأصل | حلّ عبر الـ API بدل النقر داخل الإطار |
page.goto تبقى معلّقة |
تحميل لا نهائي في الصفحة | استخدم خيار timeout مع networkidle2 |
| الصفحات المتوازية تُعطّل المتصفح | نفاد الذاكرة | قلّل التزامن أو أضف --disable-dev-shm-usage |
الأسئلة المتداولة
ما أنواع الكابتشا التي يمكن لهذا التدفق حلّها عبر CaptchaAI؟
يغطي CaptchaAI عائلة reCAPTCHA v2 وv3 (بما فيها Enterprise)، وCloudflare Turnstile وChallenge، وGeeTest v3، إضافةً إلى الكابتشا الصوري وشبكة الصور. أما hCaptcha وFunCaptcha (Arkose Labs) فغير مدعومين حاليًا.
كيف أتعامل مع حظر عنوان IP عند التشغيل المتوازي؟
عند ارتفاع كثافة الطلبات من عنوان واحد تفرض بعض المواقع تحديات إضافية أو حظرًا مؤقتًا. وزّع الحمل عبر خوادم وسيطة سكنية، وأبقِ التزامن معقولًا، وأضف تباعدًا بين الطلبات. فـ CaptchaAI يحل الكابتشا على خوادمه، لكن سمعة عنوان IP الخاص بمتصفحك تظل مسؤوليتك.
هل يبطئ الوضع الخفي عملية الحل؟
لا يؤثر الوضع الخفي في زمن الحل؛ فمهمته إخفاء بصمات الأتمتة كي لا يرفع الموقع مستوى التحدي أساسًا. والحل الفعلي يتم على خوادم CaptchaAI بمعزل عن متصفحك المحلي.
كم عدد المتصفحات المتوازية المناسب، وكيف يرتبط بعدد الـ Threads؟
يحدّه عاملان: ذاكرة جهازك وعدد الـ Threads في خطتك. تستهلك كل صفحة نحو 50 إلى 100 ميجابايت، فعلى جهاز بذاكرة 8 جيجابايت اجعل التزامن بين 5 و10 صفحات. وبما أن كل طلب حل متزامن يحجز Thread واحدًا، اختر خطة يعادل عدد Threads فيها أعلى مستوى تزامن تخطط له.
ماذا أفعل إذا انتهت صلاحية الرمز قبل إرساله؟
رموز reCAPTCHA وTurnstile قصيرة العمر (دقيقتان تقريبًا). اطلب الحل قرب لحظة الإرسال لا في بداية تدفق طويل، وفي النماذج متعددة الصفحات اجعله في الخطوة التي تحتوي الكابتشا مباشرةً قبل النقر على زر الإرسال.
الخلاصة
يمنحك دمج Puppeteer مع CaptchaAI أنماطًا جاهزة للإنتاج: الوضع الخفي، واعتراض الطلبات للكشف التلقائي، وحقن الرمز المدرك لإطارات iframe، والنماذج متعددة الصفحات، والتشغيل المتوازي. ابدأ بالدالة solveCaptcha كأساس مشترك، ثم أضِف النمط الذي يناسب حالتك.
مقالات ذات صلة
- الفرق بين GeeTest وCloudflare Turnstile
- معالجة خطأ 403 في Cloudflare Turnstile بعد إصلاح الرمز
- أنماط معالجة أخطاء رد النداء في CaptchaAI