دليل puppeteer-extra مع Stealth Plugin والبروكسي: بناء مكتبة كشف غير قابلة للرصد

تعلم كيف تتجاوز كشف البوتات باستخدام puppeteer-extra مع stealth plugin وبروكسي سكني. تشفير البصمات، تدوير IP، وأنماط التوسع للزحف الإنتاجي.

دليل puppeteer-extra مع Stealth Plugin والبروكسي: بناء مكتبة كشف غير قابلة للرصد

إذا سبق لك أن شغّلت Puppeteer في بيئة إنتاجية، فأنت تعرف الإحباط: الصفحة تعمل محلياً بدون مشاكل، لكن على الموقع المستهدف تحصل على كابتشا لا نهائية أو خطأ 403. السبب؟ المتصفحات المؤتمتة تُكشف في ثوانٍ. في هذا الدليل العملي، سنبني مكتبة Puppeteer anti-detection كاملة باستخدام puppeteer-extra stealth proxy مع Puppeteer residential proxies من ProxyHat.

لماذا يُكشف Puppeteer الخام؟

المتصفح الذي يُشغّله Puppeteer ليس متصفحاً عادياً — إنه Chromium مُعدَّل بـ --remote-debugging-port وعدة علامات كشف واضحة:

  • navigator.webdriver = true — هذه الخاصية تُضبط تلقائياً في أي متصفح يتحكم فيه بروتوكول DevTools. أي سكربت كشف يتحقق منها أولاً.
  • مصفوفة plugins فارغة أو غير متسقة — Chromium المؤتمت لا يُحمّل إضافات النظام العادية، مما يجعل navigator.plugins يبدو مختلفاً عن متصفح حقيقي.
  • آثار iframe chromedriver — بعض المواقع تحقن iframe مخفي وتتحقق من خصائص مثل window.cdc_adoQpoasnfa76pfcZLmcfl_Array أو متغيرات مشابهة يُنشئها ChromeDriver.
  • قيم WebGL و Canvas قابلة للتنبؤ — المتصفحات المؤتمتة تعمل عادةً على خوادم بدون GPU حقيقي، مما يُنتج بصمات renderer مختلفة تماماً.
  • سلوك الماوس واللوحة المفاتيح — أحداث Puppeteer تفتقر إلى الخصائص العضوية (تأخيرات، حركات طبيعية).

المواقع المتقدمة مثل Cloudflare و Datadome و PerimeterX تجمع هذه الإشارات معاً في نموذج تحليل سلوكي. لا يكفي إصلاح إشارة واحدة — تحتاج لمعالجة الكل.

puppeteer-extra مع Stealth Plugin: ما الذي يُصلح؟

puppeteer-extra هو غلاف حول Puppeteer يدعم نظام إضافات (plugins). الإضافة الأهم هي puppeteer-extra-plugin-stealth التي تُطبّق سلسلة من الترقيعات:

الترقيعما الذي يفعلهالأهمية
navigator.webdriverيعيد تعريفها إلى undefinedحرجة
Chrome runtimeيُخفي window.chrome.runtimeعالية
iframe contentWindowيُخفي آثار cdc_ من ChromeDriverحرجة
Plugin Arrayيُنشئ مصفوفة plugins واقعيةمتوسطة
Languagesيُطابق navigator.languages مع Accept-Languageمتوسطة
WebGL Vendor/Rendererيُعيّن vendor/renderer متسقينعالية
Media Codecsيُفعل دعم الترميز المتوقعمنخفضة
User-Agent Overrideيُزيل علامات HeadlessChromeحرجة

لنبدأ بالإعداد الأساسي:

const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');

// تفعيل إضافة التخفي
puppeteer.use(StealthPlugin());

(async () => {
  const browser = await puppeteer.launch({
    headless: 'new',
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '--disable-blink-features=AutomationControlled'
    ]
  });

  const page = await browser.newPage();
  await page.setViewport({ width: 1920, height: 1080 });

  // التحقق: هل navigator.webdriver مُخفى؟
  const isDetected = await page.evaluate(() => navigator.webdriver);
  console.log('Webhook detected:', isDetected); // يجب أن يكون undefined

  await page.goto('https://bot.sannysoft.com/');
  await page.screenshot({ path: 'stealth-check.png' });

  await browser.close();
})();

هذا يُصلح معظم الإشارات، لكنه لا يكفي بمفرده. بصمة IP هي الإشارة الأوضح: إذا كان عنوان IP من مركز بيانات معروف، فكل ترقيعات Stealth لا قيمة لها.

دمج Stealth مع البروكسي السكني: أقوى مكدس ضد الكشف

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

مع Puppeteer residential proxies من ProxyHat، تحصل على عناوين IP سكنية حقيقية من مزودي إنترنت شرعيين:

const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');

puppeteer.use(StealthPlugin());

// إعداد بروكسي ProxyHat السكني مع استهداف جغرافي
const PROXY_HOST = 'gate.proxyhat.com';
const PROXY_PORT = 8080;
const PROXY_USER = 'user-country-US';
const PROXY_PASS = 'your-password';

const proxyUrl = `http://${PROXY_USER}:${PROXY_PASS}@${PROXY_HOST}:${PROXY_PORT}`;

(async () => {
  const browser = await puppeteer.launch({
    headless: 'new',
    args: [
      `--proxy-server=${proxyUrl}`,
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '--disable-blink-features=AutomationControlled'
    ]
  });

  const page = await browser.newPage();

  // التحقق من عنوان IP
  await page.goto('https://httpbin.org/ip');
  const ipData = await page.evaluate(() =>
    JSON.parse(document.body.innerText)
  );
  console.log('Proxy IP:', ipData.origin);

  await browser.close();
})();

ملاحظة مهمة: Puppeteer لا يدعم مصادقة البروكسي بشكل أصلي عبر --proxy-server. إذا كان البروكسي يتطلب مصادقة، استخدم إضافة proxy-chain لإنشاء نفق محلي، أو مرر بيانات الاعتماد في عنوان URL كما هو موضح أعلاه مع دعم Chromium لمخطط http://user:pass@host:port.

مقارنة أنواع البروكسي لمكافحة الكشف

نوع البروكسيمعدل الكشفالسرعةالتكلفةالاستخدام الأمثل
مركز بياناتعالي جداًسريعمنخفضمهام لا تتطلب تخفي
سكني (دوار)منخفضمتوسطمتوسطجمع بيانات عام
سكني (جلسة ثابتة)منخفض جداًمتوسطمتوسطتسجيل دخول، سلات
موبايلالأدنىمتوسطعاليتطبيقات موبايل، حسابات حساسة

تخصيص البصمات: Canvas و WebGL لكل جلسة

Stealth plugin يُصلح القيم الافتراضية، لكن المواقع المتقدمة تبحث عن بصمات Canvas و WebGL الفريدة. إذا أطلقت 100 متصفح بنفس بصمة Canvas، فأنت تُنشئ نمطاً واضحاً. الحل: توليد بصمة عشوائية لكل جلسة.

const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
const crypto = require('crypto');

puppeteer.use(StealthPlugin());

// توليد بذرة عشوائية فريدة لكل جلسة
function generateFingerprintSeed() {
  return crypto.randomBytes(16).toString('hex');
}

// حقن بصمة Canvas مخصصة قبل أي سكربت في الصفحة
async function injectCanvasFingerprint(page, seed) {
  await page.evaluateOnNewDocument((s) => {
    // إضافة ضوضاء عشوائية إلى toDataURL
    const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
    HTMLCanvasElement.prototype.toDataURL = function (type) {
      const ctx = this.getContext('2d');
      if (ctx) {
        // ضوضاء غير مرئية بصرية لكنها تغير الهاش
        const imgData = ctx.getImageData(0, 0, this.width, this.height);
        const noise = parseInt(s.slice(0, 8), 16) % 5;
        for (let i = 0; i < imgData.data.length; i += 4) {
          imgData.data[i] = Math.min(255, imgData.data[i] + noise);
        }
        ctx.putImageData(imgData, 0, 0);
      }
      return origToDataURL.apply(this, [type]);
    };

    // تعديل WebGL renderer ليتوافق مع GPU حقيقي
    const getParameter = WebGLRenderingContext.prototype.getParameter;
    WebGLRenderingContext.prototype.getParameter = function (param) {
      // UNMASKED_RENDERER_WEBGL
      if (param === 0x9286) {
        return 'ANGLE (NVIDIA, NVIDIA GeForce GTX 1660 SUPER, OpenGL 4.5)';
      }
      // UNMASKED_VENDOR_WEBGL
      if (param === 0x9285) {
        return 'Google Inc. (NVIDIA)';
      }
      return getParameter.call(this, param);
    };
  }, seed);
}

(async () => {
  const browser = await puppeteer.launch({
    headless: 'new',
    args: [
      '--proxy-server=http://user-country-DE:pass@gate.proxyhat.com:8080',
      '--no-sandbox',
      '--disable-blink-features=AutomationControlled'
    ]
  });

  const page = await browser.newPage();
  const seed = generateFingerprintSeed();
  await injectCanvasFingerprint(page, seed);

  await page.goto('https://browserleaks.com/canvas');
  console.log('Canvas seed:', seed);

  await browser.close();
})();

هذا النهج يضمن أن كل جلسة تحمل بصمة Canvas فريدة لكنها واقعية. المفتاح هو أن الضوضاء يجب أن تكون صغيرة بما يكفي لعدم التأثير على العرض، وكبيرة بما يكفي لتغيير الهاش الناتج.

تدوير البروكسي لكل سياق متصفح

في الإنتاج، تحتاج لعشرات الجلسات المتوازية، كل واحدة ببروكسي مختلف. Puppeteer يدعم BrowserContext — وهو متصفح افتراضي معزول داخل نفس العملية — لكنه لا يدعم بروكسي مختلف لكل سياق بشكل أصلي.

الحل الأمثل: إطلاق عدة نسخ متصفح، كل واحدة ببروكسي خاص، وإدارتها في تجمع (pool):

const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
const crypto = require('crypto');

puppeteer.use(StealthPlugin());

// تكوين البروكسي لكل جلسة
function buildProxyConfig(country, session) {
  return {
    url: `http://user-country-${country}-session-${session}:pass@gate.proxyhat.com:8080`,
    country,
    session
  };
}

// تجمع متصفحات مع تدوير IP
class BrowserPool {
  constructor(maxBrowsers = 5) {
    this.maxBrowsers = maxBrowsers;
    this.browsers = [];
    this.available = [];
  }

  async acquire(country = 'US') {
    // إعادة استخدام متصفح متاح
    if (this.available.length > 0) {
      return this.available.pop();
    }

    // إنشاء متصفح جديد إذا لم نصل للحد الأقصى
    if (this.browsers.length < this.maxBrowsers) {
      const sessionId = crypto.randomBytes(8).toString('hex');
      const proxy = buildProxyConfig(country, sessionId);

      const browser = await puppeteer.launch({
        headless: 'new',
        args: [
          `--proxy-server=${proxy.url}`,
          '--no-sandbox',
          '--disable-setuid-sandbox',
          '--disable-blink-features=AutomationControlled'
        ]
      });

      const entry = { browser, proxy, inUse: true };
      this.browsers.push(entry);
      return entry;
    }

    throw new Error('تجمع المتصفحات ممتلئ — انتظر حتى يتوفر متصفح');
  }

  release(entry) {
    entry.inUse = false;
    this.available.push(entry);
  }

  async closeAll() {
    for (const entry of this.browsers) {
      await entry.browser.close();
    }
    this.browsers = [];
    this.available = [];
  }
}

// استخدام التجمع
(async () => {
  const pool = new BrowserPool(5);

  const tasks = ['US', 'DE', 'GB', 'FR', 'JP'].map(async (country) => {
    const entry = await pool.acquire(country);
    const page = await entry.browser.newPage();

    try {
      await page.goto('https://httpbin.org/ip');
      const ip = await page.evaluate(() =>
        JSON.parse(document.body.innerText).origin
      );
      console.log(`${country}: ${ip} via ${entry.proxy.session}`);
    } finally {
      await page.close();
      pool.release(entry);
    }
  });

  await Promise.all(tasks);
  await pool.closeAll();
})();

لاحظ كيف نستخدم علم -session-{id} في اسم المستخدم لـ ProxyHat. هذا يضمن أن كل متصفح يحصل على IP مختلف ويحتفظ به طوال عمر الجلسة (sticky session).

أنماط التوسع: حاويات وأسطول متصفحات

عندما يتجاوز الحجم 20+ جلسة متوازية، يصبح إدارة المتصفحات في عملية واحدة غير عملية. الحل: حاويات Docker مع منسق.

بنية الأسطول

  • Coordinator — خدمة Node.js تتلقى المهام وتوزعها على العمال
  • Workers — حاويات Docker كل منها يُشغّل 3-5 متصفحات
  • Redis Queue — طابور مهام مع إعادة المحاولة
  • ProxyHat API — بروكسي سكني مع تدوير تلقائي
// worker.js — يُشغّل داخل حاوية Docker
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
const Redis = require('ioredis');
const crypto = require('crypto');

puppeteer.use(StealthPlugin());

const CONCURRENCY = parseInt(process.env.CONCURRENCY || '3');
const COUNTRY = process.env.COUNTRY || 'US';
const REDIS_URL = process.env.REDIS_URL || 'redis://redis:6379';

const redis = new Redis(REDIS_URL);

async function processTask(task) {
  const sessionId = crypto.randomBytes(8).toString('hex');
  const proxyUrl = `http://user-country-${COUNTRY}-session-${sessionId}:${process.env.PROXY_PASS}@gate.proxyhat.com:8080`;

  const browser = await puppeteer.launch({
    headless: 'new',
    args: [
      `--proxy-server=${proxyUrl}`,
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '--disable-dev-shm-usage',
      '--disable-blink-features=AutomationControlled',
      '--disable-gpu'
    ]
  });

  const page = await browser.newPage();
  await page.setDefaultTimeout(30000);

  try {
    await page.goto(task.url, { waitUntil: 'networkidle2' });
    const data = await page.evaluate(() => ({
      title: document.title,
      html: document.documentElement.outerHTML
    }));

    // تخزين النتيج
    await redis.rpush('results', JSON.stringify({
      taskId: task.id,
      data,
      sessionId,
      timestamp: Date.now()
    }));

    return true;
  } catch (err) {
    // إعادة المهام الفاشلة مع تأخير
    await redis.lpush('task-queue', JSON.stringify({
      ...task,
      retries: (task.retries || 0) + 1
    }));
    console.error(`Task ${task.id} failed:`, err.message);
    return false;
  } finally {
    await browser.close();
  }
}

// حلقة العامل الرئيسية
async function startWorker() {
  const workers = [];

  for (let i = 0; i < CONCURRENCY; i++) {
    workers.push((async () => {
      while (true) {
        const task = await redis.blpop('task-queue', 0);
        if (task) {
          const parsed = JSON.parse(task[1]);
          if ((parsed.retries || 0) < 3) {
            await processTask(parsed);
          }
        }
      }
    })());
  }

  await Promise.race(workers);
}

startWorker().catch(console.error);
# docker-compose.yml — أسطول عمال المتصفحات
version: '3.8'

services:
  redis:
    image: redis:7-alpine
    ports:
      - '6379:6379'

  coordinator:
    build:
      context: .
      dockerfile: Dockerfile.coordinator
    depends_on:
      - redis
    environment:
      - REDIS_URL=redis://redis:6379

  worker-us:
    build:
      context: .
      dockerfile: Dockerfile.worker
    depends_on:
      - redis
    environment:
      - REDIS_URL=redis://redis:6379
      - COUNTRY=US
      - CONCURRENCY=3
      - PROXY_PASS=${PROXYHAT_PASSWORD}
    deploy:
      replicas: 3
    shm_size: '2gb'

  worker-de:
    build:
      context: .
      dockerfile: Dockerfile.worker
    depends_on:
      - redis
    environment:
      - REDIS_URL=redis://redis:6379
      - COUNTRY=DE
      - CONCURRENCY=3
      - PROXY_PASS=${PROXYHAT_PASSWORD}
    deploy:
      replicas: 2
    shm_size: '2gb'

إدارة الموارد

كل متصفح Chromium يستهلك تقريباً 200-400 ميجابايت من الذاكرة. مع 3 عمال × 3 متصفحات = 9 متصفحات لكل عقدة، تحتاج ~4 جيجابايت RAM. نصائح:

  • استخدم --disable-dev-shm-usage لتجنب مشاكل الذاكرة المشتركة في Docker
  • اضبط shm_size: '2gb' في كل حاوية عامل
  • أغلق المتصفحات فور انتهاء المهمة — لا تتركها مفتوحة
  • راقب استخدام الذاكرة وأضف graceful shutdown عند تجاوز 80%
  • حدد setDefaultTimeout(30000) لمنع التعليق

الاعتبارات الأخلاقية

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

  • احترم robots.txt وحدود المعدل المعقولة
  • لا تُنشئ حسابات مزيفة أو تتحايل على أنظمة مكافحة الاحتيال
  • التزم بـ GDPR و CCPA عند معالجة بيانات شخصية
  • راجع شروط الخدمة قبل الكشط واسأل نفسك: هل هذا عادل؟

إذا كان مشروعك يتطلب تخفي لغرض غير أخلاقي، أعد التفكير. المواقع تستثمر بكثافة في مكافحة الاحتيال، والانتهاكات تؤدي لعواقب قانونية.

النقاط الرئيسية

النقاط الرئيسية:

  • Puppeteer الخام يُكشف فوراً عبر navigator.webdriver، مصفوفة plugins، وآثار ChromeDriver
  • Stealth plugin يُصلح 10+ إشارات كشف لكنه لا يكفي بدون بروكسي سكني
  • البروكسي السكني من ProxyHat يُخفي هوية مركز البيانات — أقوى إشارة كشف
  • تخصيص بصمات Canvas/WebGL لكل جلسة يمنع ربط الجلسات المتعددة
  • استخدم -session-{id} في اسم مستخدم ProxyHat لجلسات IP ثابتة
  • في التوسع، استخدم تجمع متصفحات أو حاويات Docker مع Redis كمنسق
  • كل متصفح يستهلك 200-400 ميجابايت — خطط لمواردك بناءً على ذلك
  • التخفي أداة للكشط المشروع — استخدمها بمسؤولية

هل أنت مستعد لبناء مكتبة كشط غير قابلة للرصد؟ ابدأ بتجربة باقات ProxyHat السكنية — مع استهداف جغرافي على مستوى المدينة وجلسات IP ثابتة، ستحصل على أساس متين لأي نظام puppeteer-extra stealth proxy إنتاجي.

¿Listo para empezar?

Accede a más de 50M de IPs residenciales en más de 148 países con filtrado impulsado por IA.

Ver preciosProxies residenciales
← Volver al Blog