عندما يعترض اختبار CAPTCHA مسار الأتمتة في Playwright، تحتاج إلى آلية تُرسل الاختبار إلى خدمة حل، ثم تستعيد الرمز الناتج وتحقنه في الصفحة قبل الإرسال. يبني هذا الدليل هذا المسار كاملاً مع CaptchaAI في Node.js: دالة حل موحّدة أولاً، ثم معالجات مخصّصة لـ reCAPTCHA v2 وCloudflare Turnstile وصور CAPTCHA، وأخيراً فئة أتمتة تدير تسجيل الدخول من أوله إلى آخره.
يتكرر المنطق نفسه مع أي نوع اختبار عبر أربع خطوات:
- اقرأ مفتاح الموقع (sitekey) من عناصر الصفحة.
- أرسل اسم الطريقة والمعطيات إلى CaptchaAI عبر نقطة النهاية in.php.
- استطلع النتيجة دورياً من res.php حتى يجهز الرمز.
- احقن الرمز في الحقل المناسب واستدعِ رد النداء إن وُجد.
المتطلبات الأساسية
قبل البدء، ثبّت Playwright ونزّل متصفح Chromium الذي ستُشغّل عليه الأمثلة. تدعم المكتبة أيضاً Firefox وWebKit، لكن Chromium الأقرب إلى سلوك المستخدمين الفعليين. إن لم يكن لديك مفتاح API بعد، ابدأ من دليل البدء السريع للحصول عليه.
npm install playwright
npx playwright install chromium
ضبط المتصفح لتقليل بصمة الأتمتة
بعض المواقع تفحص خصائص المتصفح للتمييز بين المستخدم الحقيقي والتشغيل الآلي. الإعداد التالي يضبط وكيل المستخدم ومقاس النافذة، ويزيل علامة navigator.webdriver التي يضيفها Playwright افتراضياً، لتعمل اختبارات الجودة المعتمدة بثبات دون أن تُوسم كحركة آلية.
const { chromium } = require("playwright");
async function createBrowser() {
const browser = await chromium.launch({
headless: false,
args: ["--disable-blink-features=AutomationControlled"],
});
const context = await browser.newContext({
userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
viewport: { width: 1920, height: 1080 },
locale: "en-US",
});
// Remove Playwright detection
await context.addInitScript(() => {
Object.defineProperty(navigator, "webdriver", { get: () => undefined });
delete navigator.__proto__.webdriver;
});
const page = await context.newPage();
return { browser, context, page };
}
دالة الحل الموحّدة عبر CaptchaAI API
قلب التكامل دالة واحدة تتولّى دورة الحل كاملة. ترسل الطلب إلى in.php، تلتقط مُعرّف المهمة، ثم تدخل حلقة استطلاع دوري كل خمس ثوانٍ على res.php حتى تعود الاستجابة بالرمز الجاهز أو برمز خطأ. وكل نوع اختبار يستدعيها لاحقاً باسم الطريقة والمعطيات الخاصة به.
const API_KEY = "YOUR_API_KEY";
async function solveCaptcha(method, params) {
// Submit
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;
// Poll
for (let i = 0; i < 30; i++) {
await new Promise((r) => setTimeout(r, 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");
}
حل reCAPTCHA v2 داخل Playwright
لحل reCAPTCHA v2، استخرج قيمة data-sitekey من الصفحة، أرسلها بطريقة userrecaptcha، ثم احقن الرمز العائد في الحقل g-recaptcha-response. الخطوة الأخيرة الحاسمة هي استدعاء دالة رد النداء التي تربطها Google بالودجت؛ من دونها لن يتفعّل زر الإرسال رغم وجود الرمز في الصفحة.
async function solveRecaptchaV2(page) {
// Extract sitekey
const sitekey = await page.evaluate(() => {
const el = document.querySelector("[data-sitekey]");
return el ? el.getAttribute("data-sitekey") : null;
});
if (!sitekey) throw new Error("Sitekey not found");
// Solve
const token = await solveCaptcha("userrecaptcha", {
googlekey: sitekey,
pageurl: page.url(),
});
// Inject
await page.evaluate((t) => {
const textarea = document.getElementById("g-recaptcha-response");
if (textarea) {
textarea.value = t;
textarea.style.display = "block";
}
// Trigger callback
if (typeof ___grecaptcha_cfg !== "undefined") {
const clients = ___grecaptcha_cfg.clients;
for (const key in clients) {
for (const prop in clients[key]) {
try {
const cb = clients[key][prop];
if (cb && typeof cb.callback === "function") cb.callback(t);
} catch {}
}
}
}
}, token);
return token;
}
حل Cloudflare Turnstile داخل Playwright
يتبع Turnstile المنطق نفسه مع اختلاف في موضع الرمز واسم الحقل. نقرأ data-sitekey من عنصر .cf-turnstile، ومعرّفات Turnstile تبدأ عادةً بـ 0x وهو ما نستفيد منه كخطة بديلة عند غياب الصنف. بعد الحل نحقن الرمز في الحقل cf-turnstile-response. يحل CaptchaAI اختبارات Turnstile عادةً في أقل من عشر ثوانٍ.
async function solveTurnstile(page) {
// Extract sitekey
const sitekey = await page.evaluate(() => {
const el = document.querySelector(".cf-turnstile[data-sitekey]");
if (el) return el.getAttribute("data-sitekey");
// Fallback: any data-sitekey starting with 0x
const all = document.querySelectorAll("[data-sitekey]");
for (const item of all) {
const key = item.getAttribute("data-sitekey");
if (key && key.startsWith("0x")) return key;
}
return null;
});
if (!sitekey) throw new Error("Turnstile sitekey not found");
// Solve
const token = await solveCaptcha("turnstile", {
sitekey,
pageurl: page.url(),
});
// Inject
await page.evaluate((t) => {
document
.querySelectorAll('[name="cf-turnstile-response"]')
.forEach((el) => (el.value = t));
}, token);
return token;
}
الكشف التلقائي عن نوع الاختبار وحله
بدل كتابة مسار منفصل لكل نوع، تفحص هذه الدالة الصفحة وتحدد ما إذا كانت تحوي reCAPTCHA أو Turnstile أو صورة CAPTCHA، ثم تختار طريقة الحل المناسبة تلقائياً. هذا مفيد للصفحات التي يتغيّر نوع اختبارها بحسب المستخدم أو المنطقة.
async function detectAndSolve(page) {
const captchaInfo = await page.evaluate(() => {
// Check reCAPTCHA
const recaptcha = document.querySelector("[data-sitekey]");
if (
recaptcha &&
(document.querySelector(".g-recaptcha") ||
document.querySelector('script[src*="recaptcha"]'))
) {
return { type: "recaptcha", sitekey: recaptcha.getAttribute("data-sitekey") };
}
// Check Turnstile
const turnstile = document.querySelector(".cf-turnstile[data-sitekey]");
if (turnstile) {
return { type: "turnstile", sitekey: turnstile.getAttribute("data-sitekey") };
}
// Check image CAPTCHA
const captchaImg = document.querySelector(
'img.captcha, img[alt*="captcha"], img[src*="captcha"]'
);
if (captchaImg) {
return { type: "image" };
}
return { type: null };
});
if (!captchaInfo.type) return null;
console.log(`Detected: ${captchaInfo.type}`);
switch (captchaInfo.type) {
case "recaptcha":
return await solveCaptcha("userrecaptcha", {
googlekey: captchaInfo.sitekey,
pageurl: page.url(),
});
case "turnstile":
return await solveCaptcha("turnstile", {
sitekey: captchaInfo.sitekey,
pageurl: page.url(),
});
case "image":
return await solveImageCaptcha(page);
default:
return null;
}
}
حل صور CAPTCHA بالتقاط لقطة
لصور CAPTCHA الكلاسيكية، نلتقط لقطة للعنصر نفسه بدل تنزيل الصورة عبر رابطها، فذلك يتجنّب مشكلات الجلسة وملفات تعريف الارتباط. نحوّل اللقطة إلى base64 ونرسلها إلى CaptchaAI الذي يعتمد على تقنية OCR للتعرّف الضوئي على الحروف، ثم نكتب الإجابة في حقل الإدخال.
async function solveImageCaptcha(page) {
const captchaImg = page.locator(
'img.captcha, img[alt*="captcha"], img[src*="captcha"]'
).first();
// Screenshot the CAPTCHA element
const imgBuffer = await captchaImg.screenshot();
const imgBase64 = imgBuffer.toString("base64");
// Solve via CaptchaAI
const answer = await solveCaptcha("base64", { body: imgBase64 });
// Type the answer
const input = page.locator(
'input[name="captcha"], input[name="code"], input.captcha-input'
).first();
await input.fill(answer);
return answer;
}
اعتراض الطلبات لالتقاط معطيات GeeTest v3
بعض الاختبارات مثل GeeTest v3 تُحمّل معطياتها عبر طلبات شبكة بعد فتح الصفحة. هنا نعترض الاستجابات ونلتقط قيمتَي gt وchallenge لحظة وصولهما، تمهيداً لتمريرهما إلى دالة الحل. للتوضيح: يدعم CaptchaAI اختبارات GeeTest v3، أما دعم GeeTest v4 فما زال قادماً ولم يتوفّر بعد.
async function interceptCaptchaRoutes(page, url) {
const captchaParams = {};
// Intercept responses
page.on("response", async (response) => {
const respUrl = response.url();
// GeeTest parameters
if (respUrl.includes("geetest") || respUrl.includes("gt=")) {
try {
const data = await response.json();
if (data.gt) {
captchaParams.type = "geetest";
captchaParams.gt = data.gt;
captchaParams.challenge = data.challenge;
}
} catch {}
}
});
await page.goto(url, { waitUntil: "networkidle" });
return captchaParams;
}
فئة الأتمتة الكاملة لتسجيل الدخول
تجمع فئة PlaywrightAutomation كل ما سبق في واجهة واحدة: تشغيل المتصفح، ملء النموذج، الكشف والحل، ثم الإرسال. الدالة loginWithCaptcha تنفّذ السيناريو كاملاً: تفتح الصفحة، تعبّئ الحقول، تحل الاختبار، تحقن الرمز، ثم تضغط زر الدخول وتعيد العنوان الناتج.
const { chromium } = require("playwright");
class PlaywrightAutomation {
#apiKey;
#browser;
#context;
#page;
constructor(apiKey) {
this.#apiKey = apiKey;
}
async start(headless = false) {
this.#browser = await chromium.launch({
headless,
args: ["--disable-blink-features=AutomationControlled"],
});
this.#context = await this.#browser.newContext({
userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36",
viewport: { width: 1920, height: 1080 },
});
await this.#context.addInitScript(() => {
Object.defineProperty(navigator, "webdriver", { get: () => undefined });
});
this.#page = await this.#context.newPage();
}
async stop() {
await this.#browser?.close();
}
async navigate(url) {
await this.#page.goto(url, { waitUntil: "networkidle" });
}
async fillForm(fields) {
for (const [selector, value] of Object.entries(fields)) {
await this.#page.fill(selector, value);
}
}
async solveCaptcha() {
return await detectAndSolve(this.#page);
}
async submit(selector = 'button[type="submit"]') {
await this.#page.click(selector);
await this.#page.waitForLoadState("networkidle");
return this.#page.url();
}
async loginWithCaptcha(url, fields, submitSelector) {
await this.navigate(url);
await this.fillForm(fields);
const token = await this.solveCaptcha();
if (token) {
// Inject token
await this.#page.evaluate((t) => {
const re = document.getElementById("g-recaptcha-response");
if (re) re.value = t;
document
.querySelectorAll('[name="cf-turnstile-response"]')
.forEach((el) => (el.value = t));
}, token);
}
return await this.submit(submitSelector);
}
get page() {
return this.#page;
}
}
// Usage
const bot = new PlaywrightAutomation("YOUR_API_KEY");
await bot.start();
try {
const result = await bot.loginWithCaptcha(
"https://example.com/login",
{
"#email": "user@example.com",
"#password": "pass123",
},
"#login-btn"
);
console.log(`Redirected to: ${result}`);
} finally {
await bot.stop();
}
سيناريو عملي من السوق العربي
لنأخذ حالة واقعية: فريق ضمان الجودة في منصة SaaS عربية أضاف Cloudflare Turnstile إلى صفحة التسجيل، فتعطّلت اختبارات الانحدار في خط الـ CI لأن الاختبار حجب الإرسال. بدل تعطيل الحماية في بيئة الاختبار — ما يبعدها عن سلوك الإنتاج — يوجّه الفريق Playwright إلى CaptchaAI ليحل Turnstile في كل تشغيل.
تناسب هذه الطريقة حالات شائعة في المنطقة:
- اختبار نماذج الدفع وإتمام الشراء في المتاجر الإلكترونية خلال مواسم الذروة مثل الجمعة البيضاء.
- مراقبة توفّر الخدمة على البوابات الحكومية والبنكية التي تعتمد reCAPTCHA.
- جمع بيانات الأسعار المعلنة من منصات عامة ضمن حدود الاستخدام المسموح بها.
تعتمد الفوترة في CaptchaAI على عدد الـ Threads المتزامنة لا على عدد عمليات الحل، وتبدأ الباقة BASIC من 15 دولاراً شهرياً مع 5 threads وحلول غير محدودة لكل thread، فتبقى التكلفة قابلة للتوقّع عند رفع حجم الاختبارات.
استكشاف الأخطاء الشائعة
أكثر المشكلات تكراراً في هذا التكامل ترتبط بتوقيت تحميل العناصر أو بخطوة حقن الرمز. يلخّص الجدول التالي أعراضها وحلولها:
| العرض | السبب | الحل |
|---|---|---|
page.evaluate تُعيد null |
العنصر لم يُحمّل بعد | استخدم waitForSelector أولاً |
| لم يُكتشف Turnstile | حُمِّل عبر JS بعد تحميل الصفحة | انتظر المحدد .cf-turnstile |
| الرمز يُحقن لكن النموذج لا يُرسَل | مشغّل رد النداء مفقود | استدعِ رد نداء reCAPTCHA صراحةً |
| المتصفح يُكتشف كأداة آلية | سكربت init مفقود | أضف تجاوز navigator.webdriver |
مهلة networkidle تنتهي |
نصوص استطلاع طويلة الأمد | استخدم domcontentloaded بدلاً منها |
Playwright مقابل Puppeteer
إن كنت تختار بين Playwright وPuppeteer لمشروع جديد، يوضح الجدول أبرز الفروق العملية:
| الميزة | Playwright | Puppeteer |
|---|---|---|
| تعدد المتصفحات | Chromium وFirefox وWebKit | Chromium فقط |
| نمط الـ API | قائم على Locator | قائم على Selector |
| الانتظار التلقائي | مدمج | انتظار يدوي |
| اعتراض الشبكة | على مستوى المسار | على مستوى الطلب |
| ضبط بصمة الأتمتة | إعدادات افتراضية جيدة | يحتاج إضافة stealth |
| TypeScript | دعم أصلي | أنواع من المجتمع |
الأسئلة الشائعة
هل يتعامل CaptchaAI مع hCaptcha وFunCaptcha؟
لا. لا يدعم CaptchaAI حالياً حل hCaptcha أو FunCaptcha (Arkose Labs). ما يغطّيه هو reCAPTCHA v2/v3 وCloudflare Turnstile وCloudflare Challenge وGeeTest v3 وصور CAPTCHA وشبكات الصور وBLS، إضافة إلى CaptchaFox وFriendly Captcha وLemin في مرحلة تجريبية (beta).
ما الفرق بين حقن رمز reCAPTCHA v2 وحقن رمز Turnstile؟
كلاهما يحقن رمزاً نصياً في الصفحة، لكن reCAPTCHA v2 يضع الرمز في g-recaptcha-response ويتطلّب غالباً استدعاء دالة رد النداء ليُفعَّل الإرسال، بينما يضع Turnstile الرمز في cf-turnstile-response ولا يحتاج عادةً إلى خطوة رد نداء إضافية.
هل يؤثر تشغيل المتصفح بلا واجهة رسومية على معدل الحل؟
لا. اضبط headless: true داخل launch(). يحل CaptchaAI الاختبار على خوادمه باستقلال عن متصفحك، لذا لا يغيّر الوضع مقطوع الرأس نتيجة الحل، وإن كانت بعض المواقع تعرض تحققاً أشد في هذا الوضع.
كم تكلفة تشغيل CaptchaAI مع مشروع أتمتة كبير الحجم؟
تعتمد الفوترة على عدد الـ Threads المتزامنة مع حلول غير محدودة لكل thread، فتبقى التكلفة ثابتة شهرياً بصرف النظر عن عدد عمليات الحل. تبدأ الباقات من BASIC بسعر 15 دولاراً شهرياً مع 5 threads، ويمكن الترقية كلما ارتفع مستوى التزامن المطلوب.
لماذا ينجح الحل أحياناً لكن لا يُرسَل النموذج؟
غالباً لأن رمز reCAPTCHA حُقن دون استدعاء دالة رد النداء التي تنتظرها الصفحة لتفعيل الزر. استدعِ رد النداء صراحةً بعد الحقن كما في مثال reCAPTCHA v2 أعلاه، أو تأكّد من أن اسم الحقل المستهدف مطابق لما تتوقّعه الصفحة.
الخلاصة
يمنحك دمج Playwright مع CaptchaAI في Node.js مسار أتمتة حديثاً: دالة حل موحّدة، وكشف تلقائي للنوع، واعتراض للطلبات، ودعم لعدة اختبارات في الصفحة الواحدة. وتختصر فئة PlaywrightAutomation دورة تسجيل الدخول في استدعاء واحد قابل لإعادة الاستخدام.
مقالات ذات صلة
- مقارنة GeeTest مقابل Cloudflare Turnstile
- دمج CaptchaAI مع Google Cloud Functions
- دليل CaptchaAI مع Playwright في Python