الدروس التطبيقية

اختبار CaptchaAI قبل الترحيل الكامل: دليل التشغيل الموازي

أفضل طريقة للحكم على مزوّد CAPTCHA جديد ليست قراءة جدول أسعار، بل تشغيله أسبوعاً واحداً بجانب مزوّدك الحالي على حركة المرور نفسها. التشغيل الموازي يرسل التحدي ذاته إلى الطرفين في اللحظة نفسها، فتخرج بأرقام من بيئتك أنت لا من صفحة تسويقية: معدل الحل، وزمن الاستجابة، وصلاحية التوكن على الموقع المستهدف، والتكلفة الفعلية لكل حل ناجح.

يغطي هذا الدليل البنية المطلوبة، وسكربتات Python وNode.js جاهزة للنسخ، والمقاييس التي تستحق التسجيل، وخطة تحوّل من أربع مراحل.

ملاحظة: الأرقام التي تظهر في أي مقارنة — بما فيها مقارنتك أنت — مرتبطة ببيئة تشغيل محددة. تختلف النتائج بحسب نوع CAPTCHA والموقع المستهدف وجودة البروكسي وساعة اليوم، لذا اعتمد على قياساتك الخاصة قبل أي قرار ترحيل.

ما الذي يكشفه التشغيل الموازي ولا تُظهره جداول المقارنة

جداول المقارنة الجاهزة تقيس متوسطات عامة على مواقع اختبار نموذجية، بينما لحِملك بصمة مختلفة: مفاتيح الموقع لديك، وتوزيع الأنواع بين reCAPTCHA v2 وv3 وCloudflare Turnstile وصور OCR، وسمعة عناوين IP التي تخرج منها طلباتك، والتوزيع الجغرافي لخوادمك.

وهناك بُعد لا تلتقطه أي مقارنة خارجية: مسار التكامل لديك. قد يحل المزوّد التحدي بسرعة ممتازة ثم يفشل الطلب النهائي لأن التوكن وصل بعد نافذة صلاحيته أو لأن حقنه في النموذج يقع في خطوة خاطئة. التشغيل الموازي يقيس المسار كاملاً حتى قبول الموقع المستهدف، لا الخدمة وحدها.

وهناك فائدة إدارية: جدول من 200 محاولة موثّقة يجعل قرار الترحيل قابلاً للدفاع عنه أمام من يوقّع على الميزانية.

بنية مسار الاختبار: موجّه واحد ومجمّع مقاييس واحد

البنية بسيطة عمداً: طبقة توجيه واحدة تستقبل طلب الحل، وتنسخه إلى المزوّدين، ثم تكتب نتيجة كل منهما في مجمّع مقاييس مشترك. تطبيقك لا يعرف شيئاً عن التجربة، وهذا ما يبقي الاختبار نظيفاً.

                    ┌──────────────┐
                    │ Your App     │
                    └──────┬───────┘
                           │
                    ┌──────▼───────┐
                    │ CAPTCHA      │
                    │ Router       │
                    └──┬───────┬───┘
                       │       │
              ┌────────▼──┐ ┌──▼────────┐
              │ Current   │ │ CaptchaAI │
              │ Provider  │ │           │
              └────────┬──┘ └──┬────────┘
                       │       │
                    ┌──▼───────▼──┐
                    │ Metrics     │
                    │ Collector   │
                    └─────────────┘

تنفيذ Python: واجهة موحّدة لكل مزوّد

أول خطوة عملية هي إخفاء الفروق بين المزوّدين خلف صنف واحد. الخدمتان تتحدثان بروتوكول in.php / res.php نفسه، فيكفي تغيير نقطة النهاية ومفتاح الـ API. الصنف يقيس الزمن من الإرسال حتى وصول الحل، ويعيد رمز الخطأ كما هو — فهو نصف التحليل لاحقاً.

تجريد المزوّد خلف صنف واحد

import os
import time
import requests
from dataclasses import dataclass, field
from typing import Optional
from concurrent.futures import ThreadPoolExecutor


@dataclass
class SolveResult:
    provider: str
    success: bool
    solution: Optional[str] = None
    error: Optional[str] = None
    elapsed: float = 0.0
    cost: float = 0.0


class CaptchaProvider:
    def __init__(self, name, submit_url, result_url, api_key):
        self.name = name
        self.submit_url = submit_url
        self.result_url = result_url
        self.api_key = api_key
        self.session = requests.Session()

    def solve_recaptcha(self, sitekey, pageurl):
        start = time.time()

        resp = self.session.post(self.submit_url, data={
            "key": self.api_key,
            "method": "userrecaptcha",
            "googlekey": sitekey,
            "pageurl": pageurl,
            "json": 1
        })
        data = resp.json()
        if data.get("status") != 1:
            return SolveResult(
                provider=self.name, success=False,
                error=data.get("request"), elapsed=time.time() - start
            )

        captcha_id = data["request"]

        for _ in range(60):
            time.sleep(5)
            result = self.session.get(self.result_url, params={
                "key": self.api_key, "action": "get",
                "id": captcha_id, "json": 1
            }).json()

            if result.get("status") == 1:
                return SolveResult(
                    provider=self.name, success=True,
                    solution=result["request"], elapsed=time.time() - start
                )
            if result.get("request") != "CAPCHA_NOT_READY":
                return SolveResult(
                    provider=self.name, success=False,
                    error=result.get("request"), elapsed=time.time() - start
                )

        return SolveResult(
            provider=self.name, success=False,
            error="TIMEOUT", elapsed=time.time() - start
        )

مشغّل المقارنة: طلبان في كل جولة

المشغّل يفتح خيطين لكل جولة ويرسل التحدي نفسه إلى الطرفين في التوقيت ذاته. التزامن مقصود: لو أرسلت إلى مزوّد ثم إلى الآخر بعد دقيقة، عكس الفارق ازدحام الشبكة لا أداء الخدمة. وفي النهاية يبني التقرير معدل النجاح ومتوسط الزمن وأقصر وأطول محاولة لكل طرف.

class ParallelTestRunner:
    def __init__(self, primary, challenger):
        self.primary = primary
        self.challenger = challenger
        self.results = {"primary": [], "challenger": []}

    def run_test(self, sitekey, pageurl, num_runs=20):
        print(f"Running {num_runs} parallel solves...")

        for i in range(num_runs):
            with ThreadPoolExecutor(max_workers=2) as executor:
                primary_future = executor.submit(
                    self.primary.solve_recaptcha, sitekey, pageurl
                )
                challenger_future = executor.submit(
                    self.challenger.solve_recaptcha, sitekey, pageurl
                )

                primary_result = primary_future.result()
                challenger_result = challenger_future.result()

            self.results["primary"].append(primary_result)
            self.results["challenger"].append(challenger_result)

            print(f"  Run {i+1}/{num_runs}: "
                  f"{self.primary.name}={'OK' if primary_result.success else 'FAIL'} "
                  f"({primary_result.elapsed:.1f}s) | "
                  f"{self.challenger.name}={'OK' if challenger_result.success else 'FAIL'} "
                  f"({challenger_result.elapsed:.1f}s)")

        return self.generate_report()

    def generate_report(self):
        report = {}
        for label, results in self.results.items():
            total = len(results)
            successes = sum(1 for r in results if r.success)
            times = [r.elapsed for r in results if r.success]
            errors = [r.error for r in results if not r.success]

            report[label] = {
                "provider": results[0].provider if results else "unknown",
                "total": total,
                "successes": successes,
                "success_rate": (successes / total * 100) if total else 0,
                "avg_time": sum(times) / len(times) if times else 0,
                "min_time": min(times) if times else 0,
                "max_time": max(times) if times else 0,
                "errors": errors
            }

        return report


# Usage
current = CaptchaProvider(
    name="CurrentProvider",
    submit_url="https://current-provider.com/in.php",
    result_url="https://current-provider.com/res.php",
    api_key="current_key"
)

captchaai = CaptchaProvider(
    name="CaptchaAI",
    submit_url="https://ocr.captchaai.com/in.php",
    result_url="https://ocr.captchaai.com/res.php",
    api_key=os.environ["CAPTCHAAI_API_KEY"]
)

runner = ParallelTestRunner(primary=current, challenger=captchaai)
report = runner.run_test(
    sitekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
    pageurl="https://example.com/form",
    num_runs=20
)

for label, stats in report.items():
    print(f"\n{stats['provider']}:")
    print(f"  Success rate: {stats['success_rate']:.1f}%")
    print(f"  Avg time: {stats['avg_time']:.1f}s")
    print(f"  Min/Max: {stats['min_time']:.1f}s / {stats['max_time']:.1f}s")
    if stats['errors']:
        print(f"  Errors: {stats['errors']}")

تقسيم حركة المرور: ابدأ بـ 10% مع مسار رجوع

بعد أرقام المختبر، انقل التجربة إلى الإنتاج بنسبة صغيرة. الموزّع التالي يرسل عُشر الطلبات إلى CaptchaAI ويعود إلى المزوّد الأساسي عند الفشل، فلا يشعر المستخدم النهائي بالتجربة:

import random


class TrafficSplitter:
    def __init__(self, primary, challenger, challenger_pct=10):
        self.primary = primary
        self.challenger = challenger
        self.challenger_pct = challenger_pct

    def solve(self, sitekey, pageurl):
        if random.randint(1, 100) <= self.challenger_pct:
            result = self.challenger.solve_recaptcha(sitekey, pageurl)
            if not result.success:
                # Fall back to primary on failure
                return self.primary.solve_recaptcha(sitekey, pageurl)
            return result
        return self.primary.solve_recaptcha(sitekey, pageurl)


# Start with 10%, increase as confidence builds
splitter = TrafficSplitter(current, captchaai, challenger_pct=10)
result = splitter.solve(sitekey="...", pageurl="...")

نفس المقارنة داخل Node.js

إذا كانت أدوات الأتمتة لديك مبنية على Puppeteer أو Playwright، شغّل المقارنة في العملية نفسها. النسخة التالية تستخدم Axios وPromise.all لإطلاق الطلبين معاً مع ملخص في النهاية:

const axios = require("axios");

class CaptchaProvider {
  constructor(name, submitUrl, resultUrl, apiKey) {
    this.name = name;
    this.submitUrl = submitUrl;
    this.resultUrl = resultUrl;
    this.apiKey = apiKey;
  }

  async solveRecaptcha(sitekey, pageurl) {
    const start = Date.now();
    try {
      const submit = await axios.post(this.submitUrl, null, {
        params: { key: this.apiKey, method: "userrecaptcha", googlekey: sitekey, pageurl, json: 1 },
      });
      if (submit.data.status !== 1) {
        return { provider: this.name, success: false, error: submit.data.request, elapsed: (Date.now() - start) / 1000 };
      }

      const captchaId = submit.data.request;
      for (let i = 0; i < 60; i++) {
        await new Promise((r) => setTimeout(r, 5000));
        const poll = await axios.get(this.resultUrl, {
          params: { key: this.apiKey, action: "get", id: captchaId, json: 1 },
        });
        if (poll.data.status === 1) {
          return { provider: this.name, success: true, solution: poll.data.request, elapsed: (Date.now() - start) / 1000 };
        }
        if (poll.data.request !== "CAPCHA_NOT_READY") {
          return { provider: this.name, success: false, error: poll.data.request, elapsed: (Date.now() - start) / 1000 };
        }
      }
      return { provider: this.name, success: false, error: "TIMEOUT", elapsed: (Date.now() - start) / 1000 };
    } catch (err) {
      return { provider: this.name, success: false, error: err.message, elapsed: (Date.now() - start) / 1000 };
    }
  }
}

async function parallelTest(current, captchaai, sitekey, pageurl, runs = 20) {
  const results = { current: [], captchaai: [] };

  for (let i = 0; i < runs; i++) {
    const [currentResult, captchaaiResult] = await Promise.all([
      current.solveRecaptcha(sitekey, pageurl),
      captchaai.solveRecaptcha(sitekey, pageurl),
    ]);

    results.current.push(currentResult);
    results.captchaai.push(captchaaiResult);

    console.log(`Run ${i + 1}/${runs}: ${current.name}=${currentResult.success ? "OK" : "FAIL"} ` +
      `(${currentResult.elapsed.toFixed(1)}s) | ${captchaai.name}=${captchaaiResult.success ? "OK" : "FAIL"} ` +
      `(${captchaaiResult.elapsed.toFixed(1)}s)`);
  }

  for (const [label, data] of Object.entries(results)) {
    const successes = data.filter((r) => r.success).length;
    const times = data.filter((r) => r.success).map((r) => r.elapsed);
    const avgTime = times.length ? times.reduce((a, b) => a + b, 0) / times.length : 0;
    console.log(`\n${label}: ${successes}/${runs} success (${((successes / runs) * 100).toFixed(1)}%), avg ${avgTime.toFixed(1)}s`);
  }
}

// Run
const currentProvider = new CaptchaProvider("CurrentProvider", "https://current-provider.com/in.php", "https://current-provider.com/res.php", "current_key");
const captchaai = new CaptchaProvider("CaptchaAI", "https://ocr.captchaai.com/in.php", "https://ocr.captchaai.com/res.php", process.env.CAPTCHAAI_API_KEY);

parallelTest(currentProvider, captchaai, "SITE_KEY", "https://example.com", 20);

المقاييس التي يجب تسجيلها في كل جولة

قرّر ما تقيسه قبل أول جولة، وإلا انتهيت بسجلات لا تجيب عن سؤالك. ستة مقاييس تكفي:

المقياس طريقة القياس
معدل النجاح successful_solves / total_attempts × 100
متوسط وقت الحل الزمن من لحظة الإرسال حتى استلام الحل
وقت الحل عند P95 النسبة المئوية 95 لأوقات الحل — تكشف الذيل الطويل الذي يخفيه المتوسط
معدل الخطأ حسب النوع احسب كل رمز خطأ على حدة بدل تجميعها في رقم واحد
التكلفة لكل حل إجمالي الإنفاق الشهري ÷ عدد الحلول الناجحة
صلاحية التوكن هل قبل الموقع المستهدف فعلاً التوكن الذي أعادته الخدمة؟

المقياس الأخير يُهمَل غالباً وهو الأهم: سجّل نتيجة الطلب النهائي، لا نتيجة استدعاء res.php وحده.

خطة تحوّل من أربع مراحل

المرحلة المدة توزيع حركة المرور الهدف
1. التحقق التقني يوم واحد 0% مباشر، تشغيل موازٍ فقط التأكد من توافق الـ API وحقول الطلب
2. اختبار الظل 3 أيام 5% إلى CaptchaAI مع مسار رجوع بناء خط أساس للمقاييس
3. التوسّع التدريجي أسبوع 25% ← 50% ← 75% مراقبة الأخطاء عند كل مستوى
4. التحويل الكامل 100% CaptchaAI إيقاف اشتراك المزوّد القديم

لا تختصر المرحلة الثانية: معظم مفاجآت الترحيل تظهر عند 5% من الحمل الحقيقي، لا في مختبر من 20 جولة.

سيناريو من السوق العربي: منصة حجوزات تعمل على مدار الساعة

تخيّل فريقاً في القاهرة أو الرياض يدير منصة حجوزات ونماذج تسجيل تتعامل مع reCAPTCHA v2 وCloudflare Turnstile، وجزء من حملها صور OCR بسيطة. ذروة الطلب عندهم مساءً بتوقيت المنطقة، وهي بالضبط الساعة التي لا تغطيها أي مقارنة منشورة.

الخطة العملية: شغّل المقارنة 3 أيام على ثلاث نوافذ زمنية — الصباح، والذروة المسائية، وبعد منتصف الليل — وافصل الأرقام حسب نوع CAPTCHA. غالباً ما يتغيّر ترتيب المزوّدين بين نافذة وأخرى، وهذا وحده يبرّر الاختبار.

وعلى جانب التكلفة، انتبه لاختلاف نموذج التسعير: CaptchaAI يحاسب على عدد الـ threads المتزامنة لا على كل عملية حل، مع حلول غير محدودة ضمن الخطة — ADVANCE بـ 90 دولاراً شهرياً و50 thread، وBASIC بـ 15 دولاراً و5 threads للفرق الصغيرة. لذلك احسب «التكلفة لكل حل ناجح» على حجمك الفعلي وقارنها بفاتورة المزوّد الحالي للحجم نفسه.

أخطاء متكرّرة أثناء التشغيل الموازي

المشكلة السبب المرجّح الإجراء
التوكن يُنشأ لكن الموقع المستهدف يرفضه عدم تطابق مفتاح الموقع أو عنوان الصفحة أو سياق الجلسة أعد التقاط المعاملات، واستخدم التوكن داخل جلسة HTTP أو المتصفح نفسها فوراً
الاستطلاع الدوري ينتهي بمهلة فاصل زمني ضيق أو معالجة أخطاء أكثر صرامة من اللازم استطلع كل 5 إلى 10 ثوانٍ، وافصل انتهاء المهلة عن الأخطاء الفعلية وسجّل السبب
السكربت ينجح محلياً ويفشل في الإنتاج اختلاف زمن الشبكة أو غياب حقن الرمز في السلسلة الفعلية شغّل الاختبار من خوادم الإنتاج وتحقّق من المسار كاملاً حتى الطلب الأخير

أسئلة شائعة

هل يضاعف التشغيل الموازي فاتورتي الشهرية؟

ليس بالضرورة، فنموذجا التسعير مختلفان. CaptchaAI يحاسب على الـ threads المتزامنة مع حلول غير محدودة ضمن الخطة، فأسبوع الاختبار يقع داخل اشتراك شهري ثابت. أما مزوّد يحاسب لكل حل فسيرتفع بنده وحده خلال الفترة — وهي كلفة صغيرة مقابل ترحيل فاشل.

ما الحد الأدنى من المحاولات قبل أن أثق بالنتيجة؟

50 محاولة لكل مزوّد هي الحد الأدنى المعقول، و100 محاولة أو أكثر موزّعة على أوقات مختلفة من اليوم تعطي صورة أثبت. عيّنة من 10 محاولات تقيس حظّك في تلك اللحظة لا أداء الخدمة.

هل أستطيع إدراج hCaptcha أو GeeTest v4 في المقارنة؟

لا. CaptchaAI لا يدعم hCaptcha ولا FunCaptcha، وGeeTest v4 مدرج ضمن ما هو قادم وليس متاحاً اليوم. المقارنة العادلة تكون على الأنواع المدعومة فعلياً: عائلة reCAPTCHA بإصداراتها، وCloudflare Turnstile وCloudflare Challenge، وGeeTest v3، وصور OCR والشبكات المصوّرة، وBLS. أما CaptchaFox وFriendly Captcha وLemin فمتاحة في مرحلة beta، فعامِلها على هذا الأساس عند القياس.

هل أُبقي المزوّد القديم بعد التحويل الكامل؟

أبقِ حسابه مفتوحاً بأقل خطة ممكنة لأسبوعين، مع مسار رجوع يُفعَّل بمتغيّر بيئة واحد. بعد أسبوعين من مقاييس مستقرة، ألغِ الاشتراك واحذف مسار الرجوع.


الخطوة التالية

اقرأ أيضاً

التعليقات غير مفعّلة لهذا المقال.