المشكلة الحقيقية مع reCAPTCHA المخبأة داخل إطار iframe ليست في حلّها، بل في العثور على الإطار الصحيح وإعادة الرمز إليه. العملية كلها أربع حركات دقيقة:
- انتقل عبر شجرة الإطارات حتى تصل إلى الإطار الذي يعرض الأداة.
- اقرأ مفتاح الموقع من سمة
srcأوdata-sitekey. - أرسل المهمة إلى CaptchaAI واستطلع النتيجة حتى يجهز الرمز.
- احقن الرمز الناتج في الإطار نفسه الذي حمل الأداة.
أما استخدام عنوان الصفحة الرئيسية أو الحقن في الإطار الخطأ فهو السبب الأول لرفض الموقع للرمز رغم صحّته. هذا الدليل يغطّي الحالة الأكثر إرباكًا: أداة CAPTCHA تعيش داخل إطار للدفع، وهذا الإطار نفسه بداخل إطار مشروط. سنعالجها في Selenium وPuppeteer، ثم ننتقل إلى ثلاثة مستويات تداخل أو أكثر.
كيف تتداخل الإطارات حول أداة CAPTCHA
في بوابات حجز المواعيد والتأشيرات ومنصّات التجارة الإلكترونية شائعة الاستخدام في المنطقة العربية، تُحمَّل أداة reCAPTCHA غالبًا داخل إطار دفع خارجي مستضاف على نطاق مختلف عن نطاق الصفحة الأم. البنية المتداخلة النموذجية تبدو هكذا:
Main page
└── iframe#payment-frame (cross-origin)
└── iframe[src*="recaptcha/api2/anchor"] (Google-hosted)
└── reCAPTCHA widget
خريطة سريعة لموقع المفتاح تختصر معظم الأخطاء:
- يوجد في الإطار الخارجي الذي يعرض الأداة، ضمن سمة
data-sitekeyأو المعلمةk=داخل قيمةsrc. - لا يوجد في إطار Google الأعمق، فذلك مجرد التحدّي المرئي ولا تتفاعل معه مباشرة.
- يبقى مقروءًا من الإطار الأب حتى لو كان إطار الدفع متعدد الأصل (cross-origin)، لأن قراءة سمة
srcلا تتطلب تنفيذ JavaScript داخل الإطار.
وهذا بالضبط ما يجعل الاعتماد على سمة src أكثر موثوقية من محاولة الدخول برمجيًا إلى الإطار الأعمق.
طرق تنفيذ الحل عمليًا
نغطّي مسارين حسب أداتك: التبديل اليدوي في Selenium، والتعامل مع الإطارات ككائنات في Puppeteer. التسلسل المنطقي واحد في الحالتين، والفارق في طريقة الوصول إلى الإطار.
Selenium: التبديل اليدوي بين الإطارات
مع Selenium يجب التبديل يدويًا إلى كل إطار قبل قراءة عناصره. التسلسل الذي ينفّذه الكود التالي ثابت:
- الدخول إلى إطار الدفع الخارجي عبر
switch_to.frame. - العثور على إطار reCAPTCHA بداخله وقراءة المفتاح من
src. - إرسال المهمة إلى CaptchaAI مع
googlekeyوpageurl. - الاستطلاع الدوري للنتيجة حتى يجهز الرمز.
- حقن الرمز في الإطار نفسه ثم العودة إلى الصفحة الرئيسية.
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
import requests
import re
import time
API_KEY = "YOUR_API_KEY"
driver = webdriver.Chrome()
driver.get("https://example.com/checkout")
# Step 1: Switch into the outer iframe
wait = WebDriverWait(driver, 15)
outer_iframe = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, "iframe#payment-frame"))
)
driver.switch_to.frame(outer_iframe)
# Step 2: Find the reCAPTCHA iframe and extract sitekey
recaptcha_iframe = wait.until(
EC.presence_of_element_located(
(By.CSS_SELECTOR, 'iframe[src*="recaptcha/api2/anchor"]')
)
)
src = recaptcha_iframe.get_attribute("src")
sitekey = re.search(r"k=([A-Za-z0-9_-]+)", src).group(1)
page_url = driver.current_url
print(f"Sitekey: {sitekey}")
print(f"Page URL: {page_url}")
# Step 3: Solve with CaptchaAI
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"json": "1"
})
task_id = resp.json()["request"]
token = None
for _ in range(24):
time.sleep(5)
poll = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get", "id": task_id, "json": "1"
}).json()
if poll["status"] == 1:
token = poll["request"]
break
if poll["request"] != "CAPCHA_NOT_READY":
raise Exception(poll["request"])
print(f"Token: {token[:50]}...")
# Step 4: Inject token (still inside the outer iframe)
driver.execute_script("""
document.querySelector('textarea[name="g-recaptcha-response"]').value = arguments[0];
if (typeof grecaptcha !== 'undefined') {
grecaptcha.getResponse = function() { return arguments[0]; };
}
""", token)
# Step 5: Switch back to main page
driver.switch_to.default_content()
print("Token injected, switched back to main frame")
انتبه إلى تفصيلين حاسمين: الحقن يحدث قبل العودة إلى الصفحة الرئيسية، لأن حقل textarea[name="g-recaptcha-response"] يعيش داخل إطار الأداة نفسه؛ ثم switch_to.default_content() يعيدك إلى الأعلى كي تتابع إرسال النموذج. حلقة الاستطلاع الدوري تنتظر حتى status == 1، وأي استجابة غير CAPCHA_NOT_READY تعني خطأ يجب إيقافه فورًا بدل الاستمرار في المحاولة.
الناتج المتوقع:
Sitekey: 6Le-SITEKEY-abc123
Page URL: https://example.com/checkout
Token: 03AGdBq26ZfPxL...
Token injected, switched back to main frame
Puppeteer: التعامل مع الإطارات ككائنات
على عكس Selenium، يتعامل Puppeteer مع الإطارات ككائنات مستقلة عبر contentFrame()، فلا حاجة لتبديل السياق ذهابًا وإيابًا. تحصل على مقبض الإطار، تعمل بداخله مباشرة، وتبقى الصفحة الأم في متناولك طوال الوقت.
const puppeteer = require('puppeteer');
const axios = require('axios');
const API_KEY = 'YOUR_API_KEY';
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://example.com/checkout', {
waitUntil: 'networkidle2'
});
// Step 1: Find the payment iframe
const paymentFrameHandle = await page.waitForSelector('iframe#payment-frame');
const paymentFrame = await paymentFrameHandle.contentFrame();
// Step 2: Find reCAPTCHA iframe inside the payment frame
const recaptchaFrameHandle = await paymentFrame.waitForSelector(
'iframe[src*="recaptcha/api2/anchor"]'
);
const src = await paymentFrame.evaluate(
el => el.getAttribute('src'), recaptchaFrameHandle
);
const sitekey = src.match(/k=([A-Za-z0-9_-]+)/)[1];
const pageUrl = page.url();
console.log(`Sitekey: ${sitekey}`);
// Step 3: Solve with CaptchaAI
const submitResp = await axios.post('https://ocr.captchaai.com/in.php', null, {
params: {
key: API_KEY,
method: 'userrecaptcha',
googlekey: sitekey,
pageurl: pageUrl,
json: 1
}
});
const taskId = submitResp.data.request;
let token = null;
for (let i = 0; i < 24; i++) {
await new Promise(r => setTimeout(r, 5000));
const pollResp = await axios.get('https://ocr.captchaai.com/res.php', {
params: { key: API_KEY, action: 'get', id: taskId, json: 1 }
});
if (pollResp.data.status === 1) {
token = pollResp.data.request;
break;
}
if (pollResp.data.request !== 'CAPCHA_NOT_READY') {
throw new Error(pollResp.data.request);
}
}
console.log(`Token: ${token.substring(0, 50)}...`);
// Step 4: Inject token into the payment frame
await paymentFrame.evaluate((tkn) => {
const textarea = document.querySelector(
'textarea[name="g-recaptcha-response"]'
);
textarea.value = tkn;
const callback = document.querySelector('.g-recaptcha')
?.getAttribute('data-callback');
if (callback && typeof window[callback] === 'function') {
window[callback](tkn);
}
}, token);
console.log('Token injected into payment iframe');
})();
نقطتان تستحقّان الانتباه هنا:
contentFrame()يعيد كائن إطار مستقل، فتعمل بداخله دون أن تفقد الوصول إلى الصفحة الأم.- استدعاء
data-callbackبعد الحقن ضروري في كثير من النماذج، إذ لا يتفعّل زر الإرسال إلا بعد تشغيل دالة رد النداء، ومجرد ملءtextareaقد لا يكفي وحده.
الإطارات المتداخلة بعمق ثلاثة مستويات أو أكثر
حين يزيد التداخل عن مستويين، اربط عمليات الدخول إلى الإطارات واحدة تلو الأخرى حتى تصل إلى السياق الذي يحمل الأداة. القاعدة نفسها: استخرج المفتاح من الإطار الأخير، ثم تراجع للأعلى بالترتيب.
في Selenium
# Main → iframe-1 → iframe-2 → CAPTCHA
driver.switch_to.frame(driver.find_element(By.ID, "iframe-1"))
driver.switch_to.frame(driver.find_element(By.ID, "iframe-2"))
# Now extract sitekey from this context
# To go back up one level:
driver.switch_to.parent_frame()
# To go back to main:
driver.switch_to.default_content()
استخدم switch_to.parent_frame() للصعود مستوى واحدًا فقط، وswitch_to.default_content() للعودة إلى الصفحة الجذر دفعة واحدة. الخلط بينهما مصدر شائع للأخطاء عندما يفشل إرسال النموذج بعد الحقن.
في Puppeteer
const frame1 = await (await page.$('iframe#iframe-1')).contentFrame();
const frame2 = await (await frame1.$('iframe#iframe-2')).contentFrame();
// Extract sitekey from frame2
الأسئلة الشائعة
ما الفرق بين switch_to.frame في Selenium وcontentFrame في Puppeteer؟
في Selenium يبدّل switch_to.frame السياق النشط بالكامل، فكل استعلام لاحق يجري داخل الإطار حتى تعود صراحةً. أما contentFrame في Puppeteer فيعيد كائن إطار مستقل تتعامل معه مباشرة دون أن تفقد الوصول إلى الصفحة الأم. عمليًا، Puppeteer أوضح مع التداخل العميق لأنك لا تتتبّع «أين أنت الآن».
لماذا يرفض الموقع الرمز رغم أنه صحيح؟
في الغالب لأن قيمة pageurl المُرسلة إلى CaptchaAI لا تطابق عنوان الإطار الذي حُمِّلت فيه الأداة، أو لأن الرمز حُقن في الإطار الخطأ. تأكّد أن pageurl هو عنوان الصفحة التي تعرض reCAPTCHA فعليًا، وأن الحقن يستهدف textarea[name="g-recaptcha-response"] داخل إطار الأداة نفسه.
هل أحتاج إذنًا قبل أتمتة نموذج فيه reCAPTCHA؟
نعم. هذه الأساليب موجّهة لاختبار الجودة الموثوق وأتمتة الأنظمة التي تملك صلاحية التعامل معها أو حصلت على إذن أصحابها. راجع شروط استخدام الموقع المستهدف قبل تشغيل أي أتمتة على بيئة إنتاج.
كم يكلّف حل reCAPTCHA v2 عبر CaptchaAI؟
يعتمد التسعير على عدد الخيوط المتزامنة (Threads) لا على عدد عمليات الحل، مع حلول غير محدودة لكل خيط شهريًا. تبدأ الباقات من BASIC بسعر $15 شهريًا و5 خيوط، وترتفع إلى STANDARD بسعر $30 و15 خيطًا، فما فوق حسب حجم عملك. راجع صفحة التسعير على captchaai.com للاطلاع على كل الباقات.
أخطاء شائعة عند التعامل مع الإطارات وكيفية تفاديها
تذكّر أن أغلب حالات الفشل مصدرها التنقل بين الإطارات واختيار عنوان URL، لا الاتصال بالخدمة نفسها.
معظم حالات فشل الحل ليست في الاتصال بـ CaptchaAI، بل في تفاصيل التنقل بين الإطارات واختيار عنوان URL الصحيح:
| الخطأ | ما الذي يحدث | الإصلاح |
|---|---|---|
استخدام عنوان الصفحة الرئيسية كـ pageurl |
يرفض الموقع الرمز المحلول | استخدم عنوان URL للإطار الذي يعرض أداة CAPTCHA |
| عدم العودة إلى الإطار الأب | تفشل الإجراءات اللاحقة | استدعِ switch_to.parent_frame() أو switch_to.default_content() بعد الحقن |
| استخراج المفتاح من إطار خاطئ | ERROR_WRONG_GOOGLEKEY |
المفتاح موجود على الإطار الذي يحتوي .g-recaptcha، لا على إطار تحدّي Google الداخلي |
| محاولة تنفيذ JS داخل إطار متعدد الأصل | SecurityError في وحدة التحكم |
لا يمكن استخدام execute_script داخل إطار cross-origin؛ اقرأ المفتاح من سمة src بدلاً من ذلك |
ابدأ بحلّ reCAPTCHA داخل أي عمق من الإطارات
أنشئ مفتاح API الخاص بك على captchaai.com وطبّق الخطوات أعلاه على أول نموذج فيه إطار متداخل.