الفرق الجوهري بين حل CAPTCHA داخل تطبيق ويب تقدمي (PWA) وحلّه في صفحة تقليدية ليس في نوع الاختبار، بل في التوقيت. الأداة لا تصل ضمن استجابة HTML الأولى؛ يحقنها الإطار في DOM بعد أن يكتمل عرض هيكل التطبيق. لذلك فإن أي محاولة لاستخراج مفتاح الموقع مبكرًا تعود بنتيجة فارغة. الحل عمليًا خطوتان: راقب DOM حتى تظهر الأداة فعلًا، ثم أرسل مفتاح الموقع والـ URL إلى CaptchaAI واحقن الرمز الناتج في النموذج.
تخيّل متجرًا إلكترونيًا بصيغة PWA يخدم السوق الخليجي، ويضع Cloudflare Turnstile عند خطوة إتمام الشراء. المستخدم يتنقّل بين السلة والدفع دون أي إعادة تحميل للصفحة، فيظهر الاختبار فجأة داخل مسار جديد. سكربت الأتمتة الذي فحص الصفحة مرة واحدة عند التحميل لن يجد شيئًا، بينما السكربت الذي يراقب تغيّر DOM يلتقط الأداة في لحظة حقنها.
يشرح هذا الدليل كيف ترصد أداة CAPTCHA وتحلّها داخل سياق PWA باستخدام Playwright وSelenium مع CaptchaAI، في أربع خطوات عملية:
- انتظر حقن الإطار للأداة في DOM ثم استخرج مفتاح الموقع.
- تجاوز تخزين Service Worker المؤقت لتفادي مفتاح موقع قديم.
- التقط الأداة عند التنقل من جانب العميل دون إعادة تحميل الصفحة.
- طبّق المنطق نفسه في Python عبر Selenium.
لماذا يختلف التعامل مع CAPTCHA في تطبيقات PWA؟
الموقع التقليدي يضع أداة CAPTCHA داخل استجابة HTML الأولى، فتكون جاهزة للقراءة فور وصول الصفحة. تطبيق PWA يبني واجهته بـ JavaScript ويؤجّل ظهور الأداة، ويضيف طبقة Service Worker قد تعترض الطلبات. الجدول التالي يوضّح النقاط التي تقلب منطق الأتمتة:
| الجانب | موقع تقليدي | تطبيق PWA |
|---|---|---|
| ظهور أداة CAPTCHA | ضمن HTML الأولي | يعرضها JavaScript بعد تحميل الصفحة |
| التنقل بين الصفحات | إعادة تحميل كاملة | توجيه من جانب العميل دون إعادة تحميل |
| Service Worker | غير موجود | يخزّن الموارد وقد يعترض الطلبات |
| جاهزية DOM | فورية | بعد اكتمال عرض الإطار |
| طلبات الشبكة | مباشرة | قد يعترضها Service Worker |
علامات تدلّ على أن العقبة من بنية PWA
قبل أن تلوم خدمة الحل، تأكّد أن المشكلة ليست في توقيت العرض. إذا لاحظت أيًّا من هذه الإشارات فالسبب غالبًا في بنية التطبيق نفسه:
| الإشارة | دلالتها |
|---|---|
| الأداة غائبة عن HTML الأولي رغم ظهورها على الشاشة | الإطار يحقنها بعد العرض من جانب العميل |
الانتقال بين الشاشات لا يطلق page.goto() جديدًا |
التطبيق يعتمد توجيه SPA داخل PWA |
| الطلبات تُخدَم من الكاش أو Service Worker | قد تحتاج إلى تجاوز الكاش أثناء الاختبار |
| عنصر CAPTCHA يظهر ثم يُستبدل بعقدة جديدة | راقب DOM بدل الاكتفاء بفحص واحد |
الخطوة 1: انتظر العرض الديناميكي لأداة CAPTCHA
أكثر الأخطاء شيوعًا هو استخراج مفتاح الموقع قبل أن ينتهي إطار PWA من عرض الأداة، فتحصل على قيمة فارغة. الحل هو استخدام MutationObserver لمراقبة إضافة العناصر إلى DOM، مع مهلة أمان تنهي الانتظار إذا لم تظهر الأداة. السكربت التالي بـ Playwright يكتشف نوع الأداة (reCAPTCHA v2 أو Cloudflare Turnstile)، ويلتقط مفتاح الموقع والـ URL، ثم يرسلها إلى CaptchaAI ويحقن الرمز الناتج:
// pwa_captcha_detector.js — Playwright script
const { chromium } = require('playwright');
const axios = require('axios');
const API_KEY = 'YOUR_API_KEY';
async function detectCaptchaInPWA(page) {
// Wait for the PWA app shell to render
await page.waitForLoadState('networkidle');
// Use MutationObserver to detect dynamically loaded CAPTCHAs
const captchaInfo = await page.evaluate(() => {
return new Promise((resolve) => {
// Check if CAPTCHA is already present
const existing = document.querySelector('.g-recaptcha, .cf-turnstile');
if (existing) {
resolve({
type: existing.classList.contains('g-recaptcha')
? 'recaptcha_v2' : 'turnstile',
sitekey: existing.getAttribute('data-sitekey'),
pageurl: window.location.href,
});
return;
}
// Watch for CAPTCHA elements added dynamically
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType !== 1) continue;
const captcha = node.matches?.('.g-recaptcha, .cf-turnstile')
? node
: node.querySelector?.('.g-recaptcha, .cf-turnstile');
if (captcha) {
observer.disconnect();
resolve({
type: captcha.classList.contains('g-recaptcha')
? 'recaptcha_v2' : 'turnstile',
sitekey: captcha.getAttribute('data-sitekey'),
pageurl: window.location.href,
});
return;
}
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
// Timeout after 15 seconds
setTimeout(() => {
observer.disconnect();
resolve(null);
}, 15000);
});
});
return captchaInfo;
}
async function main() {
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example-pwa.com/login');
const captcha = await detectCaptchaInPWA(page);
if (!captcha) {
console.log('No CAPTCHA detected');
await browser.close();
return;
}
console.log(`Detected ${captcha.type}: ${captcha.sitekey}`);
// Solve with CaptchaAI
const token = await solveCaptcha(captcha);
console.log(`Token: ${token.substring(0, 50)}...`);
// Inject token
await injectToken(page, captcha.type, token);
// Submit form
await page.click('button[type="submit"]');
await page.waitForNavigation({ waitUntil: 'networkidle' });
console.log('Form submitted');
await browser.close();
}
async function solveCaptcha(captcha) {
const params = {
key: API_KEY,
pageurl: captcha.pageurl,
json: '1',
};
if (captcha.type === 'recaptcha_v2') {
params.method = 'userrecaptcha';
params.googlekey = captcha.sitekey;
} else {
params.method = 'turnstile';
params.sitekey = captcha.sitekey;
}
const submit = await axios.get(
'https://ocr.captchaai.com/in.php', { params }
);
if (submit.data.status !== 1) throw new Error(submit.data.request);
const taskId = submit.data.request;
for (let i = 0; i < 30; i++) {
await new Promise((r) => setTimeout(r, 5000));
const poll = await axios.get('https://ocr.captchaai.com/res.php', {
params: { key: API_KEY, action: 'get', id: taskId, json: '1' },
});
if (poll.data.status === 1) return poll.data.request;
if (poll.data.request !== 'CAPCHA_NOT_READY') {
throw new Error(poll.data.request);
}
}
throw new Error('Timeout');
}
async function injectToken(page, type, token) {
if (type === 'recaptcha_v2') {
await page.evaluate((t) => {
document.getElementById('g-recaptcha-response').value = t;
try {
const clients = ___grecaptcha_cfg.clients;
Object.keys(clients).forEach((k) => {
Object.keys(clients[k]).forEach((j) => {
if (clients[k][j]?.callback) clients[k][j].callback(t);
});
});
} catch (e) {}
}, token);
} else {
await page.evaluate((t) => {
const input = document.querySelector('[name="cf-turnstile-response"]');
if (input) input.value = t;
const cb = document.querySelector('.cf-turnstile')
?.getAttribute('data-callback');
if (cb && typeof window[cb] === 'function') window[cb](t);
}, token);
}
}
main().catch(console.error);
بعد التقاط المفتاح، تحقّق من هذه النقاط قبل الإرسال إلى CaptchaAI:
sitekeyغير فارغ ويعود من العنصر الظاهر فعلًا على الشاشة.pageurlهو عنوان المسار الذي تظهر فيه الأداة لا الصفحة الأولى.- نوع الأداة (reCAPTCHA v2 أو Turnstile) اكتُشف بشكل صحيح.
الخطوة 2: تجاوز تخزين Service Worker المؤقت
قد يخزّن Service Worker نصوص أداة CAPTCHA، فتظهر لك نسخة قديمة بمفتاح موقع لم يعد صالحًا. الحل هو اعتراض الطلبات الخاصة بموارد الأداة وإجبارها على تجاوز الكاش عبر ترويسة Cache-Control: no-cache:
// Intercept and bypass Service Worker cache for CAPTCHA scripts
await page.route('**/recaptcha/**', (route) => {
route.continue({ headers: { ...route.request().headers(), 'Cache-Control': 'no-cache' } });
});
await page.route('**/turnstile/**', (route) => {
route.continue({ headers: { ...route.request().headers(), 'Cache-Control': 'no-cache' } });
});
للتأكد من مرور الطلب فعلًا عبر الاعتراض:
- طابِق نمط المسار مع موارد الأداة (
**/recaptcha/**أو**/turnstile/**). - عطّل Service Worker أثناء الاختبار إن استمر تقديم نسخة مخزّنة.
الخطوة 3: التقاط CAPTCHA عند التنقل من جانب العميل
بما أن الانتقال إلى مسار محمي بـ CAPTCHA لا يطلق تحميلًا جديدًا للصفحة، فلن تلتقط الأداة إن اعتمدت على page.goto() وحده. اعترض history.pushState لرصد تغيّر المسار، أو انتظر مسارًا محددًا ثم شغّل الكشف من جديد على المسار الجديد:
// Monitor PWA route changes for new CAPTCHAs
await page.evaluate(() => {
const originalPushState = history.pushState;
history.pushState = function() {
originalPushState.apply(this, arguments);
window.dispatchEvent(new Event('pwa-route-change'));
};
});
page.on('console', async (msg) => {
// React to route changes if needed
});
// Or wait for specific route
await page.waitForURL('**/checkout', { waitUntil: 'networkidle' });
// Then detect CAPTCHA on the new route
const captcha = await detectCaptchaInPWA(page);
الخطوة 4: بديل بـ Python باستخدام Selenium
إذا كان سير عملك قائمًا على Python، فالمنطق نفسه ينطبق: انتظر ظهور الأداة عبر WebDriverWait، ثم استخرج مفتاح الموقع، وأرسله إلى CaptchaAI، واستطلع النتيجة كل خمس ثوانٍ، ثم احقن الرمز وأرسل النموذج.
# pwa_captcha_selenium.py
import time
import requests
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
API_KEY = "YOUR_API_KEY"
driver = webdriver.Chrome()
driver.get("https://example-pwa.com/login")
# Wait for PWA to render CAPTCHA
wait = WebDriverWait(driver, 20)
captcha_el = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".g-recaptcha, .cf-turnstile"))
)
sitekey = captcha_el.get_attribute("data-sitekey")
pageurl = driver.current_url
is_turnstile = "cf-turnstile" in captcha_el.get_attribute("class")
# Submit to CaptchaAI
params = {"key": API_KEY, "pageurl": pageurl, "json": "1"}
if is_turnstile:
params["method"] = "turnstile"
params["sitekey"] = sitekey
else:
params["method"] = "userrecaptcha"
params["googlekey"] = sitekey
resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
task_id = resp.json()["request"]
# Poll
for _ in range(30):
time.sleep(5)
poll = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get", "id": task_id, "json": "1",
})
if poll.json().get("status") == 1:
token = poll.json()["request"]
break
else:
raise TimeoutError("CAPTCHA not solved")
# Inject token
driver.execute_script(f"""
document.getElementById('g-recaptcha-response').value = '{token}';
""")
driver.find_element(By.CSS_SELECTOR, 'button[type="submit"]').click()
print("Form submitted")
driver.quit()
يتبع مسار Selenium الخطوات نفسها التي رأيتها في Playwright:
- انتظر ظهور الأداة عبر
WebDriverWaitبدل الفحص الفوري. - استطلع النتيجة كل خمس ثوانٍ حتى تعود الحالة
1. - احقن الرمز في الحقل المخفي ثم أرسل النموذج.
معالجة الأخطاء الشائعة
| المشكلة | السبب المحتمل | الحل |
|---|---|---|
| عنصر CAPTCHA لا يظهر إطلاقًا | لم يكمل التطبيق عرض المسار بعد | استخدم waitForSelector بمهلة أطول وتأكّد من اكتمال التوجيه من جانب العميل |
| مفتاح الموقع قديم بعد التنقل | Service Worker قدّم HTML مخزّنًا مؤقتًا | تجاوز الكاش لموارد الأداة كما في الخطوة 2 وأعد الكشف على المسار الجديد |
| النموذج لا يرسل الرمز رغم حقنه | معالج النموذج في SPA يقرأ من حالة المكوّن لا من DOM | حدّث حالة الإطار أيضًا (مرجع React أو خاصية تفاعلية في Vue) واستدعِ رد النداء |
| الرمز يُنشأ لكن الموقع يرفضه | عدم تطابق مفتاح الموقع أو الـ URL أو سياق الجلسة | أعد التقاط المعلمات واستخدم الرمز داخل الجلسة نفسها فورًا |
نصائح لتشغيل الحل في بيئة الإنتاج
قبل نقل السكربت إلى بيئة إنتاج تخدم حركة حقيقية، ثبّت هذه الممارسات:
- أعد الكشف على كل مسار محمي بدل الاكتفاء بكشف واحد عند التحميل.
- افصل بين مهلة الاستطلاع والأخطاء الفعلية حتى لا يُعدّ بطء الشبكة فشلًا.
- استخدم الرمز داخل الجلسة نفسها فورًا لأن صلاحيته مؤقتة.
- وسّع عدد الـ threads تدريجيًا مع نمو حركة إتمام الشراء.
الأسئلة الشائعة
لماذا لا تظهر أداة CAPTCHA في page.content() رغم أني أراها على الشاشة؟
لأن الإطار يحقن الأداة في DOM بعد اكتمال عرض هيكل التطبيق، بينما يلتقط page.content() لقطة واحدة قد تسبق الحقن. راقب DOM عبر MutationObserver أو انتظر ظهور المُحدِّد بدل الاعتماد على فحص لحظي واحد.
كيف أتعامل مع تغيّر مفتاح الموقع عند الانتقال بين مسارات التطبيق؟
كل مسار في PWA قد يحمل أداة بمفتاح موقع مختلف، والانتقال لا يطلق تحميلًا جديدًا للصفحة. راقب history.pushState أو انتظر المسار المستهدف عبر page.waitForURL، ثم أعد تشغيل منطق الكشف لالتقاط المفتاح الجديد قبل الإرسال إلى CaptchaAI.
هل اختبار hCaptcha مدعوم في CaptchaAI ضمن تطبيقات PWA؟
لا؛ لا يدعم CaptchaAI حل hCaptcha أو FunCaptcha حاليًا. لكنه يحل الأنواع الأكثر شيوعًا في تطبيقات PWA مثل reCAPTCHA v2 وv3 وCloudflare Turnstile وGeeTest v3 والصور النصية والشبكية، إضافة إلى CaptchaFox وFriendly Captcha وLemin في مرحلة beta. طريقة الحقن في DOM هي نفسها لهذه الأنواع المدعومة.
ما الخطة المناسبة لتشغيل عدة عمليات حل بالتوازي أثناء ذروة الحركة؟
تعتمد التسعير على عدد الـ threads المتزامنة مع حلول غير محدودة لكل thread. لتطبيق خفيف تكفي خطة BASIC ($15 شهريًا، 5 threads)، أما متجر PWA يواجه ذروة إتمام شراء متزامنة فقد يحتاج إلى ADVANCE ($90 شهريًا، 50 thread) لتشغيل عمليات حل متوازية دون طابور انتظار.
خطوات تالية
- ابدأ سريعًا مع CaptchaAI: حلّ أول كابتشا في 5 دقائق
- حلّ reCAPTCHA v2 عبر الـ API خطوة بخطوة
- حلّ Cloudflare Turnstile عبر الـ API
- حلّ GeeTest v3 عبر الـ API