أسرع طريقة لاختبار بوابة تسجيل جامعية محمية بـ reCAPTCHA v2 هي فصل التحقق عن منطق الاختبار: يتولى CaptchaAI إنتاج قيمة g-recaptcha-response، ويبقى سكربت الاختبار مركّزاً على ما تريد التحقق منه فعلاً — دقة عدّاد المقاعد، وترتيب قائمة الانتظار، وإغلاق نافذة السحب والإضافة في موعدها المحدد.
في جامعات المنطقة يتركّز الحمل كله في أيام قليلة من كل فصل دراسي: تُفتح البوابة في ساعة معلنة، فيدخل آلاف الطلاب في اللحظة نفسها، ويظهر التحقق البشري على صفحة الدخول وعلى البحث في المقررات معاً. الفريق الذي يريد إعادة تمثيل هذا الضغط قبل يوم الافتتاح يحتاج مساراً متكرراً يمرّ عبر التحقق بدل أن يتوقف عنده، وهذا ما يبنيه هذا الدليل خطوة بخطوة.
متى يكون تشغيل هذه الاختبارات مقبولاً
هذه الطريقة مخصّصة للبوابات التي تملكها الجامعة أو التي تحمل تصريحاً مكتوباً باختبارها. قبل تشغيل أي سيناريو، ثبّت ثلاثة أمور:
- بيئة اختبار مستقلة أو نافذة صيانة متفق عليها مع إدارة الأنظمة.
- حسابات طلاب تجريبية مخصّصة للاختبار، لا حسابات طلاب حقيقيين.
- سجل تشغيل يوضّح التوقيت وعدد المحاولات والمسؤول عنها، ليبقى الاختبار قابلاً للمراجعة لاحقاً.
خريطة نقاط التحقق داخل بوابة التسجيل
لا يظهر التحقق في مكان واحد. امسح البوابة أولاً وسجّل أين يظهر كل نوع ولماذا، لأن هذه الخريطة تحدد لاحقاً عدد عمليات الحل التي ستحتاجها كل حزمة اختبار:
| خطوة التسجيل | نوع التحقق | سبب وجوده |
|---|---|---|
| صفحة دخول الطالب | reCAPTCHA v2 | صدّ محاولات تخمين بيانات الدخول |
| البحث في المقررات | reCAPTCHA v2 | تقليل الاستعلامات الآلية المتكررة |
| إضافة مقرر أو سحبه | reCAPTCHA v2 | منع التسجيل الآلي في الشُّعب المزدحمة |
| الانضمام إلى قائمة الانتظار | reCAPTCHA v2 | حماية ترتيب القائمة من التلاعب |
| مخطّط الجدول الدراسي | Cloudflare Turnstile | حماية الأدوات التفاعلية بأقل احتكاك |
| طلب كشف الدرجات | reCAPTCHA v2 | التأكد من أن الطلب صادر عن شخص |
متى يظهر reCAPTCHA v2 أكثر أثناء الاختبار
ليست كل حالة اختبار متساوية في كلفتها. الجدول التالي يرتّب السيناريوهات الشائعة بحسب تكرار ظهور التحقق فيها:
| سيناريو الاختبار | تكرار التحقق | ما الذي يعنيه عملياً |
|---|---|---|
| الدخول بعدة حسابات تجريبية | مرتفع | كل محاولة دخول تستدعي تحققاً جديداً |
| اختبارات تسجيل متزامنة | مرتفع | طبقة الحماية تقرأ التزامن كنشاط آلي |
| بحث سريع ومتكرر في المقررات | متوسط | الاستعلامات تخضع لتحديد معدل الطلبات |
| توليد جدول دراسي مقترح | منخفض | تفاعل يتم داخل جلسة واحدة |
| التحقق من المتطلبات السابقة | منخفض | جزء من مسار التسجيل نفسه |
القاعدة العملية: صفوف "مرتفع" تحتاج ميزانية threads وزمن انتظار محسوباً، أما صفوف "منخفض" فيمكن تشغيلها كاملة داخل جلسة واحدة مُصادق عليها مسبقاً.
عميل اختبار بلغة Python يمرّ عبر reCAPTCHA v2
الصنف التالي يجمع أربع حالات في واجهة واحدة: الدخول، والبحث عن المقررات، والتسجيل في شعبة، وقراءة المقاعد المتاحة. لاحظ نقطتين: مفتاح الموقع يُستخرج من HTML الصفحة نفسها، والرمز الناتج يُرسل داخل الجلسة ذاتها التي حمّلت النموذج — الخلط بين الجلسات هو السبب الأول لفشل اختبار حصل على رمز صحيح.
import requests
import time
import re
class RegistrationTester:
def __init__(self, api_key, portal_url):
self.api_key = api_key
self.portal_url = portal_url
self.session = requests.Session()
self.session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
})
def test_login(self, student_id, password):
"""Test login flow with CAPTCHA handling."""
login_page = self.session.get(f"{self.portal_url}/login")
site_key = self._extract_site_key(login_page.text)
token = self._solve_recaptcha(site_key, f"{self.portal_url}/login")
response = self.session.post(f"{self.portal_url}/login", data={
"studentId": student_id,
"password": password,
"g-recaptcha-response": token
})
return {
"status": response.status_code,
"redirected": response.url != f"{self.portal_url}/login",
"success": "dashboard" in response.url or response.status_code == 200
}
def test_course_search(self, term, department, level=None):
"""Test course search returns correct results."""
params = {"term": term, "dept": department}
if level:
params["level"] = level
response = self.session.get(
f"{self.portal_url}/courses/search", params=params
)
if self._has_captcha(response.text):
site_key = self._extract_site_key(response.text)
token = self._solve_recaptcha(
site_key, f"{self.portal_url}/courses/search"
)
response = self.session.post(
f"{self.portal_url}/courses/search",
data={**params, "g-recaptcha-response": token}
)
courses = self._parse_courses(response.text)
return {
"count": len(courses),
"courses": courses,
"all_match_dept": all(
c["department"] == department for c in courses
)
}
def test_enrollment(self, course_id, section):
"""Test enrolling in a course section."""
enroll_page = self.session.get(
f"{self.portal_url}/enroll/{course_id}/{section}"
)
if self._has_captcha(enroll_page.text):
site_key = self._extract_site_key(enroll_page.text)
token = self._solve_recaptcha(
site_key,
f"{self.portal_url}/enroll/{course_id}/{section}"
)
else:
token = None
form_data = {
"courseId": course_id,
"section": section,
"confirm": "true"
}
if token:
form_data["g-recaptcha-response"] = token
response = self.session.post(
f"{self.portal_url}/enroll/submit",
data=form_data
)
return {
"status": response.status_code,
"enrolled": "success" in response.text.lower(),
"waitlisted": "waitlist" in response.text.lower(),
"error": self._extract_error(response.text)
}
def test_seat_availability(self, course_ids):
"""Test seat availability display for multiple courses."""
results = []
for course_id in course_ids:
response = self.session.get(
f"{self.portal_url}/courses/{course_id}"
)
if self._has_captcha(response.text):
site_key = self._extract_site_key(response.text)
token = self._solve_recaptcha(
site_key, f"{self.portal_url}/courses/{course_id}"
)
response = self.session.post(
f"{self.portal_url}/courses/{course_id}",
data={"g-recaptcha-response": token}
)
availability = self._parse_availability(response.text)
results.append({"course_id": course_id, **availability})
return results
def _has_captcha(self, html):
return "g-recaptcha" in html or "recaptcha" in html.lower()
def _extract_site_key(self, html):
match = re.search(r'data-sitekey="([^"]+)"', html)
if match:
return match.group(1)
raise ValueError("reCAPTCHA site key not found")
def _solve_recaptcha(self, site_key, page_url):
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": site_key,
"pageurl": page_url,
"json": 1
})
task_id = resp.json()["request"]
for _ in range(60):
time.sleep(3)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key,
"action": "get",
"id": task_id,
"json": 1
})
data = result.json()
if data["status"] == 1:
return data["request"]
raise TimeoutError("reCAPTCHA solve timed out")
def _parse_courses(self, html):
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
def text_or_empty(node):
return node.text.strip() if node and node.text else ""
courses = []
for row in soup.select(".course-row, tr.course"):
courses.append({
"id": row.get("data-course-id", ""),
"department": text_or_empty(row.select_one(".dept")),
"number": text_or_empty(row.select_one(".number")),
"title": text_or_empty(row.select_one(".title")),
"seats": text_or_empty(row.select_one(".seats"))
})
return courses
def _parse_availability(self, html):
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
def text_or_none(node):
return node.text.strip() if node and node.text else None
return {
"total_seats": text_or_none(soup.select_one(".total-seats")),
"enrolled": text_or_none(soup.select_one(".enrolled-count")),
"available": text_or_none(soup.select_one(".available-seats")),
"waitlist": text_or_none(soup.select_one(".waitlist-count"))
}
def _extract_error(self, html):
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
error = soup.select_one(".error-message, .alert-danger")
return error.text.strip() if error else None
إيقاع الاستطلاع الدوري هنا محافظ عمداً: ثلاث ثوانٍ بين كل استفسار والذي يليه. ووفق الأرقام المعلنة للخدمة، يُحل reCAPTCHA v2 في أقل من 60 ثانية، لذا تمنحك ستون محاولة هامشاً مريحاً حتى في أوقات الذروة، وتبقى مهلة انتهاء واضحة بدل انتظار مفتوح.
مشغّل حزمة الاختبارات في JavaScript
عند الانتقال من حالة واحدة إلى حزمة كاملة تعمل قبل كل إصدار، تحتاج مشغّلاً يجمع النتائج ويخرج بتقرير واحد. النسخة التالية تشغّل خمس حالات بالتسلسل وتلتقط الأخطاء بدل أن تتوقف الحزمة عند أول إخفاق:
class RegistrationTestSuite {
constructor(apiKey, portalUrl) {
this.apiKey = apiKey;
this.portalUrl = portalUrl;
this.results = [];
}
async runAll(testCredentials) {
const tests = [
() => this.testLogin(testCredentials),
() => this.testCourseSearch('Fall2025', 'CS'),
() => this.testEnrollment('CS101', '001'),
() => this.testDropCourse('CS101', '001'),
() => this.testWaitlistFlow('CS201', '001')
];
for (const test of tests) {
try {
const result = await test();
this.results.push({ ...result, passed: true });
} catch (error) {
this.results.push({ name: test.name, error: error.message, passed: false });
}
}
return this.generateReport();
}
async solveRecaptcha(pageUrl, html) {
const match = html.match(/data-sitekey="([^"]+)"/);
if (!match) return null;
const submitResp = await fetch('https://ocr.captchaai.com/in.php', {
method: 'POST',
body: new URLSearchParams({
key: this.apiKey,
method: 'userrecaptcha',
googlekey: match[1],
pageurl: pageUrl,
json: '1'
})
});
const { request: taskId } = await submitResp.json();
for (let i = 0; i < 60; i++) {
await new Promise(r => setTimeout(r, 3000));
const result = await fetch(
`https://ocr.captchaai.com/res.php?key=${this.apiKey}&action=get&id=${taskId}&json=1`
);
const data = await result.json();
if (data.status === 1) return data.request;
}
throw new Error('reCAPTCHA solve timed out');
}
generateReport() {
const passed = this.results.filter(r => r.passed).length;
const failed = this.results.filter(r => !r.passed).length;
return {
total: this.results.length,
passed,
failed,
passRate: `${((passed / this.results.length) * 100).toFixed(1)}%`,
details: this.results
};
}
}
// Usage
const suite = new RegistrationTestSuite('YOUR_API_KEY', 'https://register.university.edu');
const report = await suite.runAll({ studentId: 'test123', password: 'testpass' });
console.log(`Tests: ${report.passed}/${report.total} passed (${report.passRate})`);
التقرير الناتج هو ما تعلّقه في خط الإصدار: عدد الحالات الناجحة والفاشلة ونسبة النجاح في كل تشغيل. راقب هذه النسبة عبر الأيام، فانخفاضها المفاجئ يشير غالباً إلى تغيير في البوابة نفسها — نموذج جديد أو حقل إضافي — لا إلى خلل في مسار الحل.
سيناريو تطبيقي: أسبوع السحب والإضافة
لنفترض فريق ضمان جودة في جامعة إقليمية يستعد لأسبوع السحب والإضافة. الحزمة تضم أربعين حالة اختبار، وتُشغَّل ثلاث مرات يومياً على بيئة مطابقة للإنتاج، مع خمسة مسارات متوازية تحاكي ازدحام الصباح الأول. مجموع عمليات التحقق يقارب مئة وعشرين عملية في اليوم.
لأن CaptchaAI يحاسب على عدد الـ threads المتزامنة لا على عدد عمليات الحل، فإن ما يحدد الخطة هو مستوى التوازي لا حجم الحزمة: خطة STANDARD بسعر $30 شهرياً وبـ 15 thread تستوعب خمسة مسارات متوازية مع هامش واسع، بينما تناسب خطة ADVANCE بسعر $90 شهرياً وبـ 50 thread الفرق التي تشغّل عدة بيئات في الوقت نفسه. عمليات الحل داخل الخطة غير محدودة، أي أن مضاعفة عدد التشغيلات اليومية لا ترفع الفاتورة ما دام مستوى التوازي ثابتاً — وهذه تحديداً هي الحالة التي تتكرر في أسابيع الذروة الأكاديمية.
أخطاء متكررة أثناء التشغيل وكيفية معالجتها
| العَرَض | السبب الغالب | الإجراء |
|---|---|---|
| تحقق جديد مع كل تسجيل دخول | الاختبار ينشئ جلسة جديدة في كل مرة | صادق مرة واحدة وشغّل بقية الحالات داخل الجلسة نفسها |
| فشل التسجيل رغم قبول الرمز | انتهاء صلاحية رمز CSRF بين تحميل النموذج وإرساله | اقرأ رمز CSRF الطازج مع كل عملية إرسال |
data-sitekey غير موجود في HTML |
البوابة تبني النموذج عبر JavaScript بعد التحميل | افتح الصفحة بأداة أتمتة متصفح واقرأ DOM بعد اكتمال العرض |
| اختفاء بيانات الاختبار بين التشغيلات | البوابة تنظّف تسجيلات البيئة التجريبية دورياً | خصّص بيئة اختبار ثابتة وأعد تهيئة البيانات في بداية كل تشغيل |
| ارتفاع زمن الحل في أوقات الذروة | مستوى التوازي أعلى من عدد الـ threads المتاح | وزّع الحالات على دُفعات أو ارفع الخطة قبل موسم التسجيل |
الأسئلة الشائعة
هل يحل CaptchaAI كل أنواع التحقق التي قد تستخدمها البوابة؟
تغطي الخدمة عائلة reCAPTCHA v2 وv3، وCloudflare Turnstile وCloudflare Challenge، وGeeTest v3، وكابتشا الصور والشبكات النصية، وBLS. أما hCaptcha وFunCaptcha فغير مدعومين، وGeeTest v4 ما زال ضمن الدعم القادم ولا يصح الاعتماد عليه اليوم. وهناك ثلاثة أنواع في مرحلة beta هي CaptchaFox وFriendly Captcha وLemin. حدّد نوع التحقق في البوابة قبل بناء الحزمة، لا بعدها.
كم يستغرق حل التحقق داخل دورة اختبار واحدة؟
وفق الأرقام المعلنة، يُحل reCAPTCHA v2 في أقل من 60 ثانية، ويمر Cloudflare Turnstile في أقل من 10 ثوانٍ. اضبط مهلة انتهاء أعلى قليلاً من هذا السقف، واحسب الزمن الإضافي ضمن ميزانية تشغيل الحزمة بدل معاملته كإخفاق في الاختبار.
لماذا يعود مفتاح الموقع فارغاً في بعض صفحات البوابة؟
لأن كثيراً من بوابات التسجيل الحديثة تعرض النموذج عبر JavaScript بعد تحميل الصفحة، فلا يجد التعبير النمطي قيمة data-sitekey في HTML الأولي. افتح الصفحة عندها بأداة أتمتة متصفح، واقرأ DOM بعد اكتمال العرض، ثم مرّر مفتاح الموقع إلى دالة الحل نفسها دون تغيير بقية المسار.
كيف أحافظ على تكلفة ثابتة خلال موسم التسجيل؟
ثبّت مستوى التوازي أولاً، فهو ما يحدد الخطة، ثم أطلق التشغيلات اليومية بحرية لأن عمليات الحل داخل الـ thread غير محدودة. واقصر الحل على الخطوات التي تحتاجه فعلاً: صفحة الدخول وإرسال التسجيل، لا كل طلب في مسار البحث والتصفح.