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

التسجيل المنظم لعمليات CAPTCHA

السجل المفيد في عمليات CAPTCHA هو سطر JSON واحد لكل حدث، يحمل خمسة حقول تحسم أغلب الأسئلة: event وtask_id وcaptcha_type وsolve_time_ms وerror. بهذه الحقول تعرف خلال ثوانٍ كم مهمة أُرسلت، وكم منها عاد برمز صالح، وأي رمز خطأ تكرّر، وأين ضاع الوقت بين الإرسال والاستطلاع. أما سطر مثل Error solving captcha فلا يجيب عن أي سؤال منها.

الفرق ليس في كمية ما تسجّله بل في شكله: النص العادي يُقرأ بالعين، وJSON يُقرأ بالاستعلام. يغطّي هذا الدليل التنفيذين — structlog في Python وpino في Node.js — ثم يضيف إليهما تصفية فورية بـ jq وتنبيهاً تلقائياً عند ارتفاع نسبة الفشل.


ما الذي يتغيّر حين تصبح سجلات CAPTCHA قابلة للاستعلام

نص عادي JSON منظم
Captcha solved in 12.3s {"event":"captcha_solved","task_id":"abc123","type":"recaptcha_v2","solve_time_ms":12300}
يحتاج قراءة يدوية يُقرأ آلياً بأي أداة
بحث بـ grep فقط تصفية بأي حقل: النوع أو الخطأ أو الزمن
أحداث متفرّقة بلا رابط task_id يربط الإرسال ← الاستطلاع ← حقن الرمز

الفائدة تظهر عند أول تحقيق: بدل قراءة آلاف الأسطر بحثاً عن كلمة، تسأل الملف سؤالاً محدداً — كم مهمة recaptcha_v2 زاد زمن حلها على ثلاثين ثانية أمس؟ — فتحصل على رقم.


صمّم مخطط حقول السجل قبل أول سطر كود

أكثر أخطاء التسجيل كلفةً يقع قبل كتابة الكود: حقل اسمه id في Python وtaskId في Node.js، فلا تتطابق البيانات عند تجميعها. ثبّت المخطط أولاً والتزم به في اللغتين:

الحقل النوع ما يمثّله
event نص اسم الحدث: captcha_submitted، captcha_solved
task_id نص معرّف المهمة من CaptchaAI — أساس الربط
captcha_type نص recaptcha_v2، turnstile، image وغيرها
site_url نص عنوان الصفحة المستهدفة
solve_time_ms عدد صحيح الزمن الكلي من الإرسال حتى الرمز
poll_attempts عدد صحيح عدد طلبات الاستطلاع حتى النتيجة
error نص رمز الخطأ كما تعيده الخدمة
token_length عدد صحيح طول الرمز المُعاد — لا الرمز نفسه

ثلاث قواعد تسمية تختصر عليك إعادة العمل لاحقاً:

  • اكتب أسماء الأحداث بصيغة snake_case وبفعل يصف ما حدث فعلاً: captcha_solved لا solving_captcha.
  • احتفظ بأسماء الحقول وقيمها بالحروف اللاتينية حتى في المشاريع العربية بالكامل؛ العربية مكانها عناوين لوحة التحكم لا قيم السجل.
  • وحّد الطوابع الزمنية على UTC بصيغة ISO 8601، واترك التحويل المحلي لطبقة العرض.

الخطوة 1: هيّئ structlog ليُخرج JSON في Python

structlog يكتب JSON مباشرة إلى المخرج القياسي، بلا معالج إضافي ولا تنسيق يدوي:

import structlog
import time

structlog.configure(
    processors=[
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.add_log_level,
        structlog.processors.JSONRenderer(),
    ],
    logger_factory=structlog.PrintLoggerFactory(),
)

log = structlog.get_logger()

بهذا الإعداد يصير كل نداء تسجيل سطر JSON مكتملاً بطابع زمني ومستوى، جاهزاً لأي أداة تجميع.

سجّل دورة حياة الحل من الإرسال حتى الرمز

المفتاح هنا هو log.bind(): تربط الحقول الثابتة — نوع CAPTCHA وعنوان الصفحة ومفتاح الموقع مقتطعاً — مرة واحدة فترثها كل الأحداث التالية. وبمجرد صدور معرّف المهمة اربطه أيضاً ليظهر في كل سطر بعده:

import requests

API_KEY = "YOUR_API_KEY"


def solve_captcha(captcha_type, sitekey, page_url, proxy=None):
    solve_log = log.bind(
        captcha_type=captcha_type,
        site_url=page_url,
        sitekey=sitekey[:12] + "...",
    )

    # Submit
    start = time.time()
    solve_log.info("captcha_submit_start")

    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": page_url,
        "json": "1",
    }).json()

    if resp["status"] != 1:
        solve_log.error("captcha_submit_failed", error=resp["request"])
        return None

    task_id = resp["request"]
    submit_ms = int((time.time() - start) * 1000)
    solve_log = solve_log.bind(task_id=task_id)
    solve_log.info("captcha_submitted", submit_ms=submit_ms)

    # Poll
    for attempt in range(24):
        time.sleep(5)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY, "action": "get", "id": task_id, "json": "1"
        }).json()

        if result["status"] == 1:
            solve_ms = int((time.time() - start) * 1000)
            solve_log.info(
                "captcha_solved",
                solve_time_ms=solve_ms,
                poll_attempts=attempt + 1,
                token_length=len(result["request"]),
            )
            return result["request"]

        if result["request"] != "CAPCHA_NOT_READY":
            solve_log.error(
                "captcha_solve_failed",
                error=result["request"],
                poll_attempts=attempt + 1,
            )
            return None

    solve_log.warning("captcha_solve_timeout", poll_attempts=24)
    return None

لاحظ ما لا يُسجَّل: الرمز نفسه لا يدخل السجل، ومفتاح الموقع يُقتطع، ومحاولات الاستطلاع تُلخَّص في poll_attempts بدل تسجيلها واحدة واحدة.

ثلاثة أسطر فقط لكل مهمة ناجحة:

{"event":"captcha_submit_start","captcha_type":"recaptcha_v2","site_url":"https://example.com","sitekey":"6Le-wvkSAAAA...","timestamp":"2025-07-15T10:30:00Z","level":"info"}
{"event":"captcha_submitted","task_id":"71845302","submit_ms":245,"timestamp":"2025-07-15T10:30:00Z","level":"info"}
{"event":"captcha_solved","task_id":"71845302","solve_time_ms":18230,"poll_attempts":4,"token_length":580,"timestamp":"2025-07-15T10:30:18Z","level":"info"}

الخطوة 2: كرّر المخطط نفسه في Node.js عبر pino

pino يقدّم النمط ذاته بأسماء مختلفة: child بدل bind. هيّئه ليكتب طوابع ISO بدل الأرقام الافتراضية:

const pino = require('pino');

const log = pino({
  level: 'info',
  timestamp: pino.stdTimeFunctions.isoTime,
});

اربط كل مهمة بسجل فرعي خاص بها

كل مهمة تحصل على child logger مستقل، فيبقى taskId ملازماً لأحداثها حتى انتهاء المهلة:

const axios = require('axios');

const API_KEY = 'YOUR_API_KEY';

async function solveCaptcha(captchaType, sitekey, pageUrl) {
  const taskLog = log.child({
    captchaType,
    siteUrl: pageUrl,
    sitekey: sitekey.substring(0, 12) + '...',
  });

  const start = Date.now();
  taskLog.info('captcha_submit_start');

  const submit = await axios.post('https://ocr.captchaai.com/in.php', null, {
    params: {
      key: API_KEY, method: 'userrecaptcha',
      googlekey: sitekey, pageurl: pageUrl, json: 1,
    },
  });

  if (submit.data.status !== 1) {
    taskLog.error({ error: submit.data.request }, 'captcha_submit_failed');
    return null;
  }

  const taskId = submit.data.request;
  const boundLog = taskLog.child({ taskId });
  boundLog.info({ submitMs: Date.now() - start }, 'captcha_submitted');

  for (let attempt = 1; attempt <= 24; attempt++) {
    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) {
      boundLog.info({
        solveTimeMs: Date.now() - start,
        pollAttempts: attempt,
        tokenLength: poll.data.request.length,
      }, 'captcha_solved');
      return poll.data.request;
    }

    if (poll.data.request !== 'CAPCHA_NOT_READY') {
      boundLog.error({ error: poll.data.request, pollAttempts: attempt }, 'captcha_solve_failed');
      return null;
    }
  }

  boundLog.warn({ pollAttempts: 24 }, 'captcha_solve_timeout');
  return null;
}

الفرق الوحيد الذي يستحق الانتباه هو ترتيب الوسائط: pino يستقبل كائن الحقول أولاً ثم نص الرسالة، وstructlog يعكس الترتيب. أبقِ أسماء الحقول متطابقة بين اللغتين ليقرأ استعلام واحد مخرجات الاثنين.


من الملف إلى الإجابة: تصفية الأحداث بـ jq

سجل JSON يجعل jq بديلاً كافياً عن لوحة مراقبة كاملة في المشاريع الصغيرة والمتوسطة:

# With jq
cat captcha.log | jq 'select(.level == "error" and .event == "captcha_solve_failed")'

ومن هنا تبني الباقي بأسطر مشابهة: عدّ الأخطاء حسب رمزها، أو استخرج solve_time_ms لحساب الوسيط، أو تتبّع task_id واحداً عبر مراحله لإعادة بناء قصة مهمة فشلت.


نبّه نفسك قبل أن ينبّهك المستخدمون

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

# Count errors vs successes in a rolling window
from collections import deque

class ErrorRateMonitor:
    def __init__(self, window_size=100, threshold=0.2):
        self.results = deque(maxlen=window_size)
        self.threshold = threshold

    def record(self, success):
        self.results.append(success)
        if len(self.results) >= 50:
            error_rate = 1 - sum(self.results) / len(self.results)
            if error_rate > self.threshold:
                log.warning(
                    "captcha_error_rate_high",
                    error_rate=round(error_rate, 3),
                    window=len(self.results),
                )

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


سيناريو محلي: نافذة حجز مواعيد تفتح في التاسعة صباحاً

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

  • ارتفاع solve_time_ms وpoll_attempts معاً مع بقاء error فارغاً: المهام تنتظر دورها ولا تفشل، والقيد في عدد الـ threads لا في الخدمة.
  • ثبات poll_attempts مع تكرار رمز خطأ بعينه: المشكلة في الطلب — مفتاح موقع قديم أو عنوان صفحة تغيّر بعد تحديث البوابة.
  • فارق ثلاث ساعات بالضبط بين أوقات الأحداث وأوقات الشكاوى: أنت تقارن UTC بالتوقيت المحلي، وهو سبب متكرّر لتحقيقات بلا نتيجة.

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


أخطاء شائعة في سجلات CAPTCHA وكيف تعالجها

المشكلة السبب العلاج
حجم السجل ينمو بسرعة تسجيل كل محاولة استطلاع سجّل الإرسال والنجاح والفشل فقط
تعذّر ربط الأحداث task_id غير مرتبط مبكراً اربطه فور صدوره بـ log.bind() أو log.child()
السجلات غير قابلة للبحث تنسيق نص عادي حوّلها إلى JSON بـ structlog أو pino
بيانات حساسة في السجل تسجيل مفتاح الـ API كاملاً لا تسجّل المفاتيح، واقتطع مفتاح الموقع
أوقات لا تطابق الشكاوى خلط التوقيت المحلي بـ UTC سجّل بـ UTC وحوّل عند العرض
رسائل خطأ بلا سياق تسجيل نص الاستثناء وحده أضف captcha_type وsite_url لكل خطأ

الأسئلة الشائعة

كم يوماً أحتفظ بسجلات مهام CAPTCHA؟

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

كيف أربط سجل الحل بطلب المستخدم داخل تطبيقي؟

أضف معرّف الطلب لديك — request_id أو trace_id — إلى الحقول المرتبطة عند إنشاء السجل الفرعي، تماماً كما تربط task_id. عندها تقودك أي تذكرة دعم من رقم الطلب إلى مهمة CAPTCHA المرتبطة به والعكس.

هل أسجّل مفتاح الـ API أو مفتاح الموقع؟

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

ما الحقول التي تكشف أن القيد في عدد الـ threads لا في الحل؟

راقب poll_attempts وsolve_time_ms مع بقاء error فارغاً: ارتفاعهما بلا أخطاء يعني انتظاراً في التزامن، أما ارتفاع الأخطاء مع ثبات زمن الحل فيشير إلى خلل في الطلب أو الصفحة.


ابنِ سير عمل CAPTCHA قابلاً للملاحظة مع CaptchaAI

احصل على مفتاح الـ API من captchaai.com وشغّل أول مهمة بسجل JSON كامل.


أدلة ذات صلة

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