حالات الاستخدام

التعامل مع اختبار CAPTCHA لجمع بيانات الرواتب والتعويضات

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

يفترض هذا المسار أنك تجمع أرقاماً منشورة للعموم — نطاقات رواتب معلنة أو إحصاءات عمل حكومية — أو تعمل داخل بوابة تملكها أو لديك تصريح مكتوب بجمع بياناتها. وما يلي يشرح تطبيق القواعد الأربع على Cloudflare Turnstile وreCAPTCHA بكود Python وJavaScript وحساب واضح للتكلفة.

لماذا تظهر اختبارات CAPTCHA في منتصف جمع بيانات الرواتب

لا تعرض بوابات الرواتب التحقق عشوائياً؛ لكل نوع مصدر محفّز مختلف، ومعرفة المحفّز توفّر عليك عمليات حل لا داعي لها.

نوع المصدر نوع التحقق ما الذي يُطلقه
مواقع مقارنة الرواتب Cloudflare Turnstile تكرار عمليات البحث
مرشحات الرواتب في لوحات الوظائف reCAPTCHA v2 استعلامات متعددة عن الراتب
إحصاءات العمل الحكومية تحقق بالصورة طلبات تنزيل البيانات
صفحات التعويضات في مواقع الشركات Cloudflare Challenge تصفح صفحات بالجملة
منصات استبيانات الموارد البشرية reCAPTCHA v3 إرسال النماذج

انتبه للفرق بين السطر الأول والأخير: Turnstile يظهر كعقبة مرئية توقف الطلب، بينما يعمل reCAPTCHA v3 بالنقاط في الخلفية وقد يمرّر الطلب مع نتائج ناقصة بدل أن يرفضه. لذلك افحص محتوى الاستجابة، لا رمز الحالة 403 وحده.

سيناريو تطبيقي: مرجع رواتب لثلاث مدن عربية

افترض أن فريق موارد بشرية في الرياض يبني مرجعاً داخلياً لرواتب اثنتي عشرة وظيفة تقنية في الرياض ودبي والقاهرة من أربعة مصادر عامة: 12 × 3 × 4 = 144 استعلاماً في الدورة الواحدة، تتكرر كل ربع سنة.

عملياً تمر أول 20 إلى 30 استعلاماً دون اعتراض، ثم يبدأ Turnstile بالظهور على المصدرين الأكثر حساسية. إرسال طلب حل مع كل استعلام احترازياً يهدر الوقت، وتجاهل التحقق ينهي الدورة بجدول نصفه فارغ. الفحص المشروط يخفض عدد عمليات الحل إلى ما بين 30 و50 بدل 144.

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

جامع بيانات الرواتب في Python

تجمع الفئة التالية النمط كله في مكان واحد: جلسة requests واحدة تحفظ ملفات تعريف الارتباط، فحص مشروط للتحدي، إرسال المهمة إلى in.php، ثم استطلاع دوري للنتيجة من res.php قبل إعادة الإرسال مع الرمز.

import requests
import time
import re
from dataclasses import dataclass

@dataclass
class SalaryRecord:
    title: str
    location: str
    min_salary: float
    max_salary: float
    median_salary: float
    sample_size: int
    source: str

class SalaryCollector:
    def __init__(self, api_key):
        self.api_key = api_key
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
        })

    def collect_salary_data(self, portal_url, job_title, location):
        """Search for salary data, solving CAPTCHAs as needed."""
        response = self.session.get(portal_url, params={
            "title": job_title,
            "location": location
        })

        if self._is_turnstile_challenge(response):
            response = self._solve_turnstile_and_retry(response, portal_url)

        return self._parse_salary_data(response.text, portal_url)

    def collect_bulk(self, portal_url, job_titles, locations):
        """Collect salary data for multiple job title + location combos."""
        results = []

        for title in job_titles:
            for location in locations:
                try:
                    data = self.collect_salary_data(
                        portal_url, title, location
                    )
                    results.extend(data)
                    # Respectful delay between requests
                    time.sleep(2)
                except Exception as e:
                    print(f"Failed for {title} in {location}: {e}")

        return results

    def _is_turnstile_challenge(self, response):
        return (
            response.status_code == 403 or
            "cf-turnstile" in response.text or
            "challenges.cloudflare.com" in response.text
        )

    def _solve_turnstile_and_retry(self, response, url):
        match = re.search(r'data-sitekey="(0x[^"]+)"', response.text)
        if not match:
            raise ValueError("Turnstile sitekey not found")

        resp = requests.post("https://ocr.captchaai.com/in.php", data={
            "key": self.api_key,
            "method": "turnstile",
            "sitekey": match.group(1),
            "pageurl": 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 self.session.post(url, data={
                    "cf-turnstile-response": data["request"]
                })

        raise TimeoutError("Turnstile solve timed out")

    def _parse_salary_data(self, html, source):
        from bs4 import BeautifulSoup
        soup = BeautifulSoup(html, "html.parser")
        records = []

        def text_or_empty(node):
            return node.text.strip() if node and node.text else ""

        for row in soup.select(".salary-row, .compensation-entry, tr[data-salary]"):
            try:
                records.append(SalaryRecord(
                    title=text_or_empty(row.select_one(".job-title, .title")),
                    location=text_or_empty(row.select_one(".location")),
                    min_salary=self._parse_amount(
                        text_or_empty(row.select_one(".min-salary, .low"))
                    ),
                    max_salary=self._parse_amount(
                        text_or_empty(row.select_one(".max-salary, .high"))
                    ),
                    median_salary=self._parse_amount(
                        text_or_empty(row.select_one(".median, .mid"))
                    ),
                    sample_size=int(
                        text_or_empty(row.select_one(".count, .sample")).replace(",", "") or 0
                    ),
                    source=source
                ))
            except (AttributeError, ValueError):
                continue

        return records

    def _parse_amount(self, text):
        if not text:
            return 0.0
        cleaned = re.sub(r'[^\d.]', '', text)
        return float(cleaned) if cleaned else 0.0


# Usage
collector = SalaryCollector("YOUR_API_KEY")
data = collector.collect_bulk(
    "https://salary.example.com/search",
    job_titles=["Software Engineer", "Data Analyst", "Product Manager"],
    locations=["San Francisco", "New York", "Austin"]
)

for record in data:
    print(f"{record.title} in {record.location}: "
          f"${record.min_salary:,.0f}–${record.max_salary:,.0f} "
          f"(median: ${record.median_salary:,.0f})")

لاحظ ثلاث نقاط. أولاً، _is_turnstile_challenge يفحص ثلاث إشارات معاً بدل واحدة. ثانياً، الرمز يُرسل عبر self.session لا عبر طلب جديد، لأن فقدان الجلسة يعيد التحقق فوراً. ثالثاً، التأخير مكتوب صراحة داخل collect_bulk — وهو أرخص وسيلة لخفض عدد التحديات أصلاً.

استبدل YOUR_API_KEY بمفتاحك من لوحة التحكم؛ ومسار الإعداد الكامل في دليل البدء السريع مع CaptchaAI.

توحيد نتائج الرواتب من عدة مصادر في JavaScript

لا يعتمد مرجع الرواتب المفيد على مصدر واحد. النسخة التالية بلغة JavaScript تستعلم عن عدة مصادر لنفس المسمى الوظيفي والموقع، وتعزل فشل أي مصدر، ثم تحسب الوسيط المجمّع ونطاقه.

class SalaryAggregator {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.sources = [];
  }

  addSource(name, searchUrl) {
    this.sources.push({ name, searchUrl });
  }

  async collectForRole(jobTitle, location) {
    const results = [];

    for (const source of this.sources) {
      try {
        const data = await this.querySource(source, jobTitle, location);
        results.push({ source: source.name, ...data });
      } catch (error) {
        results.push({ source: source.name, error: error.message });
      }
    }

    return this.aggregateResults(results, jobTitle, location);
  }

  async querySource(source, jobTitle, location) {
    const url = `${source.searchUrl}?title=${encodeURIComponent(jobTitle)}&location=${encodeURIComponent(location)}`;
    const response = await fetch(url);
    const html = await response.text();

    if (html.includes('cf-turnstile') || response.status === 403) {
      return this.solveAndRetry(source.searchUrl, html, jobTitle, location);
    }

    return this.parseSalaryData(html);
  }

  async solveAndRetry(baseUrl, html, jobTitle, location) {
    const match = html.match(/data-sitekey="(0x[^"]+)"/);
    if (!match) throw new Error('Turnstile sitekey not found');

    const submitResp = await fetch('https://ocr.captchaai.com/in.php', {
      method: 'POST',
      body: new URLSearchParams({
        key: this.apiKey,
        method: 'turnstile',
        sitekey: match[1],
        pageurl: baseUrl,
        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) {
        const response = await fetch(baseUrl, {
          method: 'POST',
          body: new URLSearchParams({
            'cf-turnstile-response': data.request,
            title: jobTitle,
            location: location
          })
        });
        return this.parseSalaryData(await response.text());
      }
    }
    throw new Error('Turnstile solve timed out');
  }

  aggregateResults(results, jobTitle, location) {
    const valid = results.filter(r => !r.error && r.median);
    if (valid.length === 0) return null;

    const medians = valid.map(r => r.median);
    return {
      jobTitle,
      location,
      avgMedian: medians.reduce((a, b) => a + b, 0) / medians.length,
      sources: valid.length,
      range: { min: Math.min(...medians), max: Math.max(...medians) }
    };
  }
}

// Usage
const aggregator = new SalaryAggregator('YOUR_API_KEY');
aggregator.addSource('SalaryDB', 'https://salarydb.example.com/search');
aggregator.addSource('PayScale', 'https://payscale.example.com/lookup');

const result = await aggregator.collectForRole('Software Engineer', 'San Francisco');
console.log(`Median salary: $${result.avgMedian.toLocaleString()} (${result.sources} sources)`);

الميزة هنا عزل الأخطاء: فشل مصدر واحد يُسجَّل في حقل error ولا يوقف الدورة، وaggregateResults تحسب المتوسط من الأرقام الصالحة فقط، فلا يشوّه مصدر معطل المرجع كله.

اختر إيقاع جمع بيانات الرواتب المناسب لحجمك

النهج الحجم اليومي معدل ظهور التحقق يناسب
تسلسلي مع تأخير 100–500 استعلام منخفض المسوحات الصغيرة والدورية
تدوير الخوادم الوسيطة 500–2000 استعلام متوسط التحليل حسب المدينة أو الإقليم
جلسات متوازية متعددة 2000–10000 استعلام مرتفع مجموعات بيانات شاملة

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

كيف تُحسب التكلفة: خيوط متزامنة لا عدد عمليات حل

تقوم فوترة CaptchaAI على عدد الخيوط المتزامنة — عمليات الحل الجارية في اللحظة نفسها — لا على العدد الإجمالي. وكل خطة تشمل عمليات حل غير محدودة لكل Thread خلال الشهر، بلا رسوم لكل عملية وبلا فروق سعرية بين أنواع التحقق.

خطة BASIC بسعر 15 دولاراً شهرياً تمنحك 5 خيوط، وهي كافية لدورة تسلسلية كالسيناريو أعلاه. وخطة STANDARD بسعر 30 دولاراً تمنحك 15 خيطاً لتشغيل عدة مصادر بالتوازي، بينما توفّر ADVANCE بسعر 90 دولاراً 50 خيطاً للمسوحات الإقليمية الواسعة. وجميع الأسعار بالدولار الأمريكي كما هي في صفحة الأسعار الرسمية.

أما زمنياً، فيُحلّ Cloudflare Turnstile عادة في أقل من 10 ثوانٍ، ويستغرق reCAPTCHA v2 أقل من 60 ثانية. اضرب الرقم في عدد التحديات المتوقعة لتقدير زمن الدورة قبل اختيار عدد الخيوط.

معالجة الأعطال الشائعة أثناء الجمع

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

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

أسئلة شائعة

هل يتعامل CaptchaAI مع كل أنواع التحقق التي تستخدمها بوابات الرواتب؟

يغطي CaptchaAI الأنواع الأكثر شيوعاً هنا: reCAPTCHA v2 وv3 بنسخها المؤسسية، وCloudflare Turnstile وChallenge، وGeeTest v3، والتحقق بالصورة والشبكة، إضافة إلى ثلاثة أنواع في مرحلة beta هي CaptchaFox وFriendly Captcha وLemin. في المقابل، لا يدعم CaptchaAI حل hCaptcha ولا FunCaptcha، وGeeTest v4 ضمن الخطط القادمة وغير متاح حالياً. افحص نوع التحقق في كل مصدر قبل بناء خط الجمع.

كم يستغرق حل التحقق داخل دورة جمع الرواتب؟

Cloudflare Turnstile يُحلّ عادة في أقل من 10 ثوانٍ، وreCAPTCHA v2 في أقل من 60 ثانية. في دورة تحتوي 40 تحدياً من نوع Turnstile، يعني ذلك زمناً إضافياً أقل من سبع دقائق موزعاً على الدورة كلها — رقم يسهل استيعابه في جدولة ليلية.

هل أحتاج إلى خطة أكبر كلما أضفت مصادر جديدة؟

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

ما الذي يجب توثيقه قبل جمع بيانات التعويضات؟

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

روابط ذات صلة

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