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

بناء قائمة انتظار حل اختبار CAPTCHA في Node.js

Node.js عبارة عن خيط مفرد ولكنه يتفوق في التزامن I/O - وهو مثالي لحل اختبار CAPTCHA حيث تنتظر استجابات واجهة برمجة التطبيقات (API). يغطي هذا الدليل أنماط قائمة الانتظار بدءًا من Promise.all البسيط وحتى أنظمة العمل على مستوى الإنتاج.


دفعة بسيطة: Promise.allSettled

const API_KEY = "YOUR_API_KEY";

function sleep(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

async function solveSingle(method, params) {
  const submitResp = await fetch("https://ocr.captchaai.com/in.php", {
    method: "POST",
    body: new URLSearchParams({ key: API_KEY, method, json: "1", ...params }),
  });
  const submitData = await submitResp.json();
  if (submitData.status !== 1) throw new Error(submitData.request);
  const taskId = submitData.request;

  for (let i = 0; i < 30; i++) {
    await sleep(5000);
    const pollResp = await fetch(
      `https://ocr.captchaai.com/res.php?${new URLSearchParams({
        key: API_KEY,
        action: "get",
        id: taskId,
        json: "1",
      })}`
    );
    const data = await pollResp.json();
    if (data.status === 1) return data.request;
    if (data.request === "ERROR_CAPTCHA_UNSOLVABLE") throw new Error("Unsolvable");
  }
  throw new Error("Timed out");
}

// Solve all at once
async function solveBatch(tasks) {
  const results = await Promise.allSettled(
    tasks.map((task) => solveSingle(task.method, task.params))
  );

  return results.map((result, i) => ({
    taskId: tasks[i].id,
    status: result.status,
    value: result.status === "fulfilled" ? result.value : null,
    error: result.status === "rejected" ? result.reason.message : null,
  }));
}

// Usage
const tasks = Array.from({ length: 10 }, (_, i) => ({
  id: i,
  method: "userrecaptcha",
  params: { googlekey: `KEY_${i}`, pageurl: `https://example.com/${i}` },
}));

const results = await solveBatch(tasks);
console.log(`Solved: ${results.filter((r) => r.status === "fulfilled").length}/10`);

قائمة انتظار محدودة التزامن

التحكم في عدد حلول اختبار CAPTCHA التي يتم تشغيلها بالتوازي:

class ConcurrencyQueue {
  constructor(maxConcurrent = 5) {
    this.maxConcurrent = maxConcurrent;
    this.running = 0;
    this.queue = [];
    this.results = [];
  }

  add(fn) {
    return new Promise((resolve, reject) => {
      this.queue.push({ fn, resolve, reject });
      this.#process();
    });
  }

  async #process() {
    if (this.running >= this.maxConcurrent || this.queue.length === 0) return;

    this.running++;
    const { fn, resolve, reject } = this.queue.shift();

    try {
      const result = await fn();
      resolve(result);
    } catch (error) {
      reject(error);
    } finally {
      this.running--;
      this.#process();
    }
  }

  async addBatch(fns) {
    return Promise.allSettled(fns.map((fn) => this.add(fn)));
  }
}

// Usage
const queue = new ConcurrencyQueue(5);

const tasks = Array.from({ length: 20 }, (_, i) => () =>
  solveSingle("userrecaptcha", {
    googlekey: `KEY_${i}`,
    pageurl: `https://example.com/${i}`,
  })
);

const results = await queue.addBatch(tasks);
const solved = results.filter((r) => r.status === "fulfilled");
console.log(`Solved: ${solved.length}/${results.length}`);

قائمة الانتظار المستندة إلى EventEmitter

لتتبع التقدم في الوقت الحقيقي:

const { EventEmitter } = require("events");

class CaptchaQueue extends EventEmitter {
  #apiKey;
  #maxConcurrent;
  #pending;
  #active;

  constructor(apiKey, maxConcurrent = 5) {
    super();
    this.#apiKey = apiKey;
    this.#maxConcurrent = maxConcurrent;
    this.#pending = [];
    this.#active = 0;
    this.stats = { submitted: 0, solved: 0, failed: 0 };
  }

  submit(id, method, params) {
    this.#pending.push({ id, method, params });
    this.stats.submitted++;
    this.emit("submitted", { id, total: this.stats.submitted });
    this.#drain();
  }

  async #drain() {
    while (this.#active < this.#maxConcurrent && this.#pending.length > 0) {
      const task = this.#pending.shift();
      this.#active++;
      this.#solve(task).finally(() => {
        this.#active--;
        this.#drain();
        if (this.#active === 0 && this.#pending.length === 0) {
          this.emit("complete", this.stats);
        }
      });
    }
  }

  async #solve(task) {
    try {
      const token = await solveSingle(task.method, task.params);
      this.stats.solved++;
      this.emit("solved", { id: task.id, token, stats: { ...this.stats } });
    } catch (error) {
      this.stats.failed++;
      this.emit("failed", { id: task.id, error: error.message, stats: { ...this.stats } });
    }
  }
}

// Usage
const queue = new CaptchaQueue("YOUR_API_KEY", 5);

queue.on("submitted", ({ id, total }) => {
  console.log(`Submitted #${id} (total: ${total})`);
});

queue.on("solved", ({ id, stats }) => {
  console.log(`Solved #${id} — ${stats.solved}/${stats.submitted}`);
});

queue.on("failed", ({ id, error }) => {
  console.log(`Failed #${id}: ${error}`);
});

queue.on("complete", (stats) => {
  const rate = ((stats.solved / stats.submitted) * 100).toFixed(1);
  console.log(`Done: ${stats.solved}/${stats.submitted} (${rate}%)`);
});

// Submit tasks
for (let i = 0; i < 15; i++) {
  queue.submit(i, "userrecaptcha", {
    googlekey: `KEY_${i}`,
    pageurl: `https://example.com/${i}`,
  });
}

قائمة الانتظار ذات الأولوية

class PriorityQueue {
  #items = [];

  enqueue(item, priority) {
    this.#items.push({ item, priority });
    this.#items.sort((a, b) => a.priority - b.priority);
  }

  dequeue() {
    return this.#items.shift()?.item;
  }

  get length() {
    return this.#items.length;
  }
}

class PriorityCaptchaQueue {
  #apiKey;
  #maxConcurrent;
  #queue;
  #active;
  #results;

  constructor(apiKey, maxConcurrent = 5) {
    this.#apiKey = apiKey;
    this.#maxConcurrent = maxConcurrent;
    this.#queue = new PriorityQueue();
    this.#active = 0;
    this.#results = new Map();
  }

  submit(id, method, params, priority = 5) {
    return new Promise((resolve, reject) => {
      this.#queue.enqueue({ id, method, params, resolve, reject }, priority);
      this.#drain();
    });
  }

  async #drain() {
    while (this.#active < this.#maxConcurrent && this.#queue.length > 0) {
      const task = this.#queue.dequeue();
      this.#active++;

      solveSingle(task.method, task.params)
        .then((token) => {
          this.#results.set(task.id, { status: "solved", token });
          task.resolve(token);
        })
        .catch((err) => {
          this.#results.set(task.id, { status: "error", error: err.message });
          task.reject(err);
        })
        .finally(() => {
          this.#active--;
          this.#drain();
        });
    }
  }
}

// Usage: high-priority checkout, low-priority scraping
const pq = new PriorityCaptchaQueue("YOUR_API_KEY", 3);

// Priority 1 (highest) — checkout
const checkoutToken = pq.submit(
  "checkout_1",
  "turnstile",
  { sitekey: "KEY", pageurl: "https://shop.com/checkout" },
  1
);

// Priority 5 (normal) — product scraping
for (let i = 0; i < 5; i++) {
  pq.submit(
    `product_${i}`,
    "userrecaptcha",
    { googlekey: "KEY", pageurl: `https://shop.com/p/${i}` },
    5
  );
}

إعادة محاولة قائمة الانتظار مع معالجة الأحرف الميتة

class RetryQueue {
  #apiKey;
  #maxRetries;
  #results;
  #deadLetter;

  constructor(apiKey, maxRetries = 3) {
    this.#apiKey = apiKey;
    this.#maxRetries = maxRetries;
    this.#results = [];
    this.#deadLetter = [];
  }

  async processBatch(tasks, maxConcurrent = 5) {
    const queue = tasks.map((t) => ({ ...t, attempts: 0 }));

    while (queue.length > 0) {
      const batch = queue.splice(0, maxConcurrent);
      const results = await Promise.allSettled(
        batch.map((task) => this.#solveWithRetry(task))
      );

      for (let i = 0; i < results.length; i++) {
        const result = results[i];
        const task = batch[i];

        if (result.status === "fulfilled") {
          this.#results.push({ id: task.id, token: result.value });
        } else {
          task.attempts++;
          if (task.attempts < this.#maxRetries) {
            queue.push(task); // Retry
            console.log(`Retry ${task.attempts}/${this.#maxRetries}: ${task.id}`);
          } else {
            this.#deadLetter.push({
              id: task.id,
              error: result.reason.message,
              attempts: task.attempts,
            });
          }
        }
      }
    }

    return {
      solved: this.#results,
      failed: this.#deadLetter,
    };
  }

  async #solveWithRetry(task) {
    return solveSingle(task.method, task.params);
  }
}

لوحة المراقبة

class QueueMonitor {
  #startTime;
  #solveTimes;

  constructor() {
    this.#startTime = Date.now();
    this.#solveTimes = [];
    this.counts = { submitted: 0, solving: 0, solved: 0, failed: 0 };
  }

  recordSubmit() {
    this.counts.submitted++;
    this.counts.solving++;
  }

  recordSolved(solveTime) {
    this.counts.solving--;
    this.counts.solved++;
    this.#solveTimes.push(solveTime);
  }

  recordFailed() {
    this.counts.solving--;
    this.counts.failed++;
  }

  report() {
    const elapsed = (Date.now() - this.#startTime) / 1000;
    const avgTime =
      this.#solveTimes.length > 0
        ? this.#solveTimes.reduce((a, b) => a + b, 0) / this.#solveTimes.length
        : 0;
    const throughput = this.counts.solved / (elapsed / 60);
    const successRate =
      this.counts.solved + this.counts.failed > 0
        ? (this.counts.solved / (this.counts.solved + this.counts.failed)) * 100
        : 0;

    return {
      elapsed: `${elapsed.toFixed(0)}s`,
      submitted: this.counts.submitted,
      solving: this.counts.solving,
      solved: this.counts.solved,
      failed: this.counts.failed,
      avgSolveTime: `${(avgTime / 1000).toFixed(1)}s`,
      throughput: `${throughput.toFixed(1)}/min`,
      successRate: `${successRate.toFixed(1)}%`,
    };
  }
}

استكشاف الأخطاء وإصلاحها

أعراض السبب إصلاح
ترفض كل الوعود في وقت واحد تم الوصول إلى حد معدل API انخفاض maxConcurrent
الذاكرة تنمو مع مرور الوقت النتائج تتراكم معالجة ونتائج واضحة بشكل دوري
تستنزف قائمة الانتظار ولكن المهام تبقى مكالمة drain() مفقودة بعد الانتهاء تحقق من مشغل التصريف في الكتلة النهائية
ERROR_NO_SLOT_AVAILABLE هناك عدد كبير جدًا من استدعاءات واجهة برمجة التطبيقات المتزامنة أضف تأخيرًا بين عمليات الإرسال
تمتلئ قائمة انتظار الرسائل الميتة الأخطاء المستمرة تحقق من أنواع الأخطاء - قد تحتاج إلى إصلاح المعلمة

الأسئلة المتداولة

كم عدد الحلول المتزامنة التي يجب أن أقوم بتشغيلها؟

ابدأ بـ 5-10 ثم قم بالزيادة بناءً على خطة CaptchaAI الخاصة بك. انتبه إلى ERROR_NO_SLOT_AVAILABLE كإشارة الخانق.

هل يجب علي استخدام مكتبة مثل p-queue أو Bull؟

بالنسبة لحالات الاستخدام البسيطة، تكون الأنماط المضمنة أعلاه كافية. استخدم bull أو bullmq لقوائم الانتظار المستمرة المدعومة من Redis في إعدادات الإنتاج متعددة الخوادم.

كيف أتعامل مع الضغط الخلفي في قائمة الانتظار؟

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


ملخص

يتفوق Node.js في I/O المتزامن - وهو مثالي لأنظمة قائمة انتظار CAPTCHA ذاتCaptchaAI. استخدم Promise.allSettled للدفعات البسيطة، وEventEmitter لتتبع التقدم، وقوائم الانتظار ذات الأولوية للتدفقات الحيوية للأعمال، وأعد محاولة قوائم الانتظار للحصول على الموثوقية.

مقالات ذات صلة


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

أدلة ذات صلة

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