← بازگشت به وبلاگ

پروکسی در Rust reqwest: تنظیم SOCKS5، استخر مشتریان و چرخش IP برای پارسر

نحوه اتصال پروکسی SOCKS5 در Rust reqwest را بررسی می‌کنیم، استخر مشتریان را ایجاد کرده و چرخش IP را برای پارسینگ پایدار بدون مسدودسازی پیاده‌سازی می‌کنیم.

📅۲۶ مهر ۱۴۰۵

هر پارسری که بیشتر از پنج دقیقه بر روی سایت‌های واقعی کار کند، دیر یا زود با مسدودسازی IP مواجه می‌شود. Rust با کتابخانه reqwest عملکرد عالی‌ای دارد، اما به طور پیش‌فرض مشکل چرخش پروکسی را حل نمی‌کند — این باید به صورت دستی پیاده‌سازی شود. در این مقاله بررسی می‌کنیم که چگونه پروکسی SOCKS5 و HTTP(S) را متصل کنیم، استخر مشتریان را جمع‌آوری کنیم و چرخش IP را به گونه‌ای سازماندهی کنیم که پارسر پس از اولین پاسخ‌های 429 و 403 سقوط نکند.

چرا پارسر به پروکسی نیاز دارد و خطرات عدم وجود آن چیست

سایت‌هایی مانند بازارهای آنلاین، تجمیع‌کننده‌های قیمت و شبکه‌های اجتماعی فعالیت غیرعادی را از یک IP ردیابی می‌کنند: ده‌ها درخواست در ثانیه، User-Agent های مشابه، عدم وجود کوکی‌ها بین درخواست‌ها. به محض اینکه شمارش فعالیت‌های مشکوک از حد مجاز فراتر رود، IP در لیست مسدود قرار می‌گیرد — موقت یا دائمی. برای پارسر این به معنای توقف کامل جمع‌آوری داده‌ها است تا زمانی که IP به صورت دستی تغییر کند.

پروکسی این مشکل را حل می‌کند و درخواست‌ها را بین ده‌ها یا صدها آدرس IP توزیع می‌کند. در اینجا مهم است که نه تنها "پروکسی را جایگزین کنید"، بلکه استخر اتصالات و چرخش را به درستی سازماندهی کنید، در غیر این صورت یا به محدودیت‌های خود reqwest (یک مشتری = یک استخر اتصالات) برخورد می‌کنید، یا شروع به ارسال درخواست‌ها از یک پروکسی مشابه بیش از حد می‌کنید. در ادامه هر دو مشکل را به ترتیب بررسی خواهیم کرد: ابتدا اتصال پایه، سپس معماری استخر.

برای پارسرهای تولیدی، نه تنها چرخش IP مهم است، بلکه کیفیت خود آدرس‌ها نیز اهمیت دارد. اگر شما Wildberries، Ozon یا Avito را پارس می‌کنید، IP های مرکز داده سریع‌تر از IP های کاربران واقعی مسدود می‌شوند — درباره این موضوع در بخش انتخاب نوع پروکسی بیشتر توضیح خواهیم داد.

تنظیمات پایه پروکسی HTTP/HTTPS در reqwest

reqwest پروکسی را از طریق ساختار Proxy پشتیبانی می‌کند که باید به ClientBuilder منتقل شود. وابستگی‌ها را به Cargo.toml اضافه کنید:

[dependencies]
reqwest = { version = "0.12", features = ["socks", "json"] }
tokio = { version = "1", features = ["full"] }

یک مثال حداقلی با پروکسی HTTP که نیاز به احراز هویت با نام کاربری و رمز عبور دارد:

use reqwest::{Client, Proxy};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let proxy = Proxy::all("http://gate.proxycove.com:8000")?
        .basic_auth("user_login", "user_password");

    let client = Client::builder()
        .proxy(proxy)
        .timeout(std::time::Duration::from_secs(15))
        .build()?;

    let resp = client
        .get("https://httpbin.org/ip")
        .send()
        .await?
        .text()
        .await?;

    println!("پاسخ از طریق پروکسی: {resp}");
    Ok(())
}

متد Proxy::all ترافیک HTTP و HTTPS را از طریق پروکسی هدایت می‌کند. اگر نیاز به تفکیک پروتکل‌ها دارید، از Proxy::http() و Proxy::https() به صورت جداگانه استفاده کنید — این مفید است اگر ارائه‌دهنده پروکسی پورت‌های مختلفی را برای پروتکل‌های مختلف گوش می‌دهد.

اتصال پروکسی SOCKS5

SOCKS5 در سطح پایین‌تری نسبت به پروکسی HTTP کار می‌کند و از اتصالات TCP بدون تجزیه و تحلیل محتوای درخواست پشتیبانی می‌کند — این برای پارسینگ سریع‌تر و پایدارتر است، به ویژه اگر شما فقط به HTTP(S) نروید، بلکه WebSocket endpoint ها را نیز فراخوانی کنید. برای SOCKS5 حتماً ویژگی socks را در Cargo.toml فعال کنید (که در بالا ذکر شد). مثال اتصال:

use reqwest::{Client, Proxy};

async fn build_socks5_client(
    host: &str,
    login: &str,
    password: &str,
) -> reqwest::Result<Client> {
    let proxy_url = format!("socks5h://{host}");
    let proxy = Proxy::all(&proxy_url)?.basic_auth(login, password);

    Client::builder()
        .proxy(proxy)
        .timeout(std::time::Duration::from_secs(20))
        .build()
}

به الگوی socks5h به جای socks5 توجه کنید. حرف "h" در انتها به این معنی است که حل DNS دامنه در سمت سرور پروکسی انجام می‌شود و نه به صورت محلی. این برای ناشناسی حیاتی است: بدون آن، حل‌کننده DNS واقعی شما می‌تواند دامنه‌های درخواست شده را بدون پروکسی "فاش" کند، حتی اگر خود ترافیک از طریق تونل SOCKS5 منتقل شود.

اگر ارائه‌دهنده پروکسی نیاز به احراز هویت با نام کاربری/رمز عبور ندارد (IP-whitelisting)، فقط متد basic_auth را فراخوانی نکنید — IP آدرس سرور خود را که درخواست‌ها از آن ارسال می‌شوند، در پنل ارائه‌دهنده مشخص کنید.

استخر مشتریان reqwest برای پروکسی‌های مختلف

یک اشتباه رایج — ایجاد یک Client جدید برای هر درخواست است. این هزینه‌بر است: reqwest درون خود یک استخر اتصالات TCP و جلسات TLS نگه می‌دارد و ایجاد مجدد مشتری تمام مزایای keep-alive را از بین می‌برد. رویکرد صحیح — ایجاد یک مشتری برای هر پروکسی از قبل و استفاده مجدد از آنها از استخر مشترک است.

use reqwest::{Client, Proxy};
use std::sync::Arc;

#[derive(Clone)]
struct ProxyEndpoint {
    url: String,
    login: String,
    password: String,
}

struct ClientPool {
    clients: Vec<Client>,
}

impl ClientPool {
    fn build(endpoints: &[ProxyEndpoint]) -> reqwest::Result<Self> {
        let mut clients = Vec::with_capacity(endpoints.len());

        for ep in endpoints {
            let proxy = Proxy::all(&ep.url)?
                .basic_auth(&ep.login, &ep.password);

            let client = Client::builder()
                .proxy(proxy)
                .pool_max_idle_per_host(4)
                .timeout(std::time::Duration::from_secs(15))
                .build()?;

            clients.push(client);
        }

        Ok(Self { clients })
    }
}

چنین استخرهایی یک بار در زمان شروع پارسر ایجاد می‌شوند. تعداد مشتریان معمولاً برابر با تعداد منحصر به فرد پروکسی‌ها در استخر شما است — اگر ارائه‌دهنده دسترسی به چرخش در سطح دروازه (gateway) را فراهم کند، کافی است یک یا دو مشتری داشته باشید، زیرا چرخش IP به طور خودکار در سمت ارائه‌دهنده با هر اتصال TCP جدید انجام می‌شود.

برای کارهای با هم‌زمانی بالا، استخر را در Arc بپیچید و بین تسک‌های tokio به اشتراک بگذارید — مشتریان reqwest ایمن در برابر رشته‌ها هستند و برای استفاده همزمان طراحی شده‌اند.

استراتژی‌های چرخش IP

دو رویکرد کاملاً متفاوت برای چرخش وجود دارد. اولی — چرخش در سمت ارائه‌دهنده: شما به یک آدرس gateway متصل می‌شوید و ارائه‌دهنده خود IP جدید را برای هر درخواست یا هر N دقیقه جایگزین می‌کند. دومی — چرخش در سمت شما: شما یک لیست از پروکسی‌های استاتیک نگه می‌دارید و خودتان انتخاب می‌کنید که کدام را برای درخواست بعدی استفاده کنید.

اگر از پروکسی‌های مسکونی با چرخش خودکار از طریق gateway استفاده می‌کنید، کد به حداقل می‌رسد — فقط درخواست‌ها را از طریق یک و همان مشتری ارسال می‌کنید، ارائه‌دهنده خود IP خروجی را تغییر می‌دهد. اما اگر کنترل لازم است (به عنوان مثال، IP را برای یک جلسه به مدت 10 دقیقه قفل کنید تا رفتار کاربر واقعی را شبیه‌سازی کنید)، چرخش را به صورت دستی پیاده‌سازی کنید:

use rand::seq::SliceRandom;
use std::sync::atomic::{AtomicUsize, Ordering};

struct RotatingPool {
    clients: Vec<Client>,
    counter: AtomicUsize,
}

impl RotatingPool {
    // Round-robin: به نوبت مشتریان را بررسی می‌کنیم
    fn next_round_robin(&self) -> &Client {
        let idx = self.counter.fetch_add(1, Ordering::SeqCst) % self.clients.len();
        &self.clients[idx]
    }

    // انتخاب تصادفی — مفید برای جلوگیری از تحلیل الگو
    fn next_random(&self) -> &Client {
        self.clients.choose(&mut rand::thread_rng()).unwrap()
    }
}

Round-robin زمانی مناسب است که بار یکنواختی بر روی تمام IP ها مهم باشد. انتخاب تصادفی بهتر است الگوی درخواست‌ها را پنهان کند، اگر سایت هدف فاصله‌های بین درخواست‌ها را از یک آدرس تحلیل کند. برای کارهایی مانند نظارت بر قیمت‌ها در Ozon یا جمع‌آوری آگهی‌ها از Avito، در عمل ترکیبی مؤثرتر است: انتخاب تصادفی پروکسی به علاوه تأخیر تصادفی 1–3 ثانیه بین درخواست‌های یک مشتری.

به طور جداگانه باید درباره جلسات چسبنده (sticky sessions) صحبت کرد. اگر پارسر یک سناریوی چند مرحله‌ای را طی کند (ورود → انتقال به صفحات → اقدام)، IP نمی‌تواند در میانه جلسه تغییر کند — سایت تغییر IP را در حین جلسه کوکی فعال شناسایی می‌کند و یا حساب را از سیستم خارج می‌کند یا مسدود می‌کند. برای چنین سناریوهایی یک مشتری (و به تبع آن یک IP) را به "جلسه" خاص پارسر در تمام مدت زندگی آن متصل کنید.

پردازش خطاها و منطق تلاش مجدد

پروکسی‌ها گهگاه از کار می‌افتند: زمان‌های انتظار، 407 (احراز هویت پروکسی لازم است)، قطع اتصال TCP. پارسر باید بتواند "پروکسی خراب است — از استخر حذف کنید" را از "سایت 429 را برگرداند — فقط صبر کنید و IP را تغییر دهید" تشخیص دهد. در اینجا یک الگوی کاری با تلاش مجدد و حذف پروکسی‌های مرده وجود دارد:

use reqwest::StatusCode;
use std::time::Duration;

async fn fetch_with_retry(
    pool: &RotatingPool,
    url: &str,
    max_attempts: u32,
) -> Result<String, String> {
    let mut last_error = String::new();

    for attempt in 0..max_attempts {
        let client = pool.next_random();

        match client.get(url).send().await {
            Ok(resp) if resp.status() == StatusCode::TOO_MANY_REQUESTS => {
                last_error = "429: محدودیت نرخ، IP را تغییر می‌دهیم".into();
                tokio::time::sleep(Duration::from_millis(500 * (attempt as u64 + 1))).await;
                continue;
            }
            Ok(resp) if resp.status().is_success() => {
                return resp.text().await.map_err(|e| e.to_string());
            }
            Ok(resp) => {
                last_error = format!("وضعیت غیرمنتظره: {}", resp.status());
            }
            Err(e) if e.is_timeout() || e.is_connect() => {
                last_error = format!("مشکل با پروکسی: {e}");
                continue;
            }
            Err(e) => {
                last_error = e.to_string();
            }
        }
    }

    Err(format!("تمام تلاش‌ها تمام شده است: {last_error}"))
}

نکته مهم: تأخیر نمایی (500 * attempt) بار را بر روی سایت هدف در زمان‌های 429-پاسخ انبوه کاهش می‌دهد و همزمان خطر مسدودسازی کامل کل استخر پروکسی را به یکباره کاهش می‌دهد. برای پارسرهای تولیدی، یک شمارنده خطا برای هر پروکسی در استخر اضافه کنید و به طور خودکار آدرس را حذف کنید اگر 5+ خطا به طور متوالی برگرداند — این از تلاش‌های بی‌فایده از طریق IP مسدود شده جلوگیری می‌کند.

کدام نوع پروکسی را برای پارسینگ انتخاب کنیم

انتخاب نوع پروکسی به طور مستقیم بر تعداد درخواست‌های موفقی که قبل از مسدودسازی دریافت می‌کنید تأثیر می‌گذارد. IP های مرکز داده ارزان‌ترین و سریع‌ترین هستند، اما متعلق به دامنه‌های شناخته شده ارائه‌دهندگان هاستینگ هستند — سیستم‌های ضد ربات بازارها و شبکه‌های اجتماعی آنها را تقریباً به سرعت بر اساس پایگاه‌های ASN شناسایی می‌کنند. IP های مسکونی و موبایلی به نظر می‌رسند که کاربران عادی هستند، بنابراین مدت زمان بیشتری زنده می‌مانند.

نوع پروکسی سرعت خطر مسدودسازی کی باید استفاده شود
مرکز داده بالا بالا API های باز، سایت‌های بدون ضد ربات
مسکونی متوسط پایین Wildberries، Ozon، Avito، شبکه‌های اجتماعی
موبایلی متوسط حداقل Instagram، TikTok، ضد ربات‌های سختگیر

برای پارسینگ بازارها در حجم‌های بالا، تعادل معقول — پروکسی‌های مسکونی است: آنها به اندازه پروکسی‌های موبایلی در مقایسه با ترافیک گران نیستند، اما از بررسی‌های سطح "کاربر واقعی" عبور می‌کنند که دامنه‌های مرکز داده را قطع می‌کند. اگر پارسر از طریق API شبکه‌های اجتماعی کار می‌کند یا رفتار یک برنامه موبایل را شبیه‌سازی می‌کند، پروکسی‌های موبایلی کمترین درصد CAPTCHA و مسدودسازی‌های موقتی را به دلیل IP های متصل به اپراتورهای ارتباطی ارائه می‌دهند.

عملکرد و نکات مربوط به بارگذاری

Rust اجازه می‌دهد تا هزاران درخواست همزمان در یک نمونه نگه داشته شود، اما ارائه‌دهنده پروکسی و سایت هدف همچنان ظرفیت واقعی را محدود می‌کنند. چند قانون عملی برای پارسر تولید:

  • همزمانی را از طریق tokio::sync::Semaphore محدود کنید — حداکثر 20-50 درخواست همزمان برای یک استخر پروکسی، در غیر این صورت برخی از اتصالات به دلیل زمان انتظار قطع می‌شوند.
  • از pool_idle_timeout در ClientBuilder استفاده کنید تا reqwest اتصالات مرده را از طریق پروکسی‌های ناپایدار نگه ندارد.
  • وضعیت‌های HTTP و زمان پاسخ را برای هر پروکسی به طور جداگانه ثبت کنید — اینگونه به سرعت خواهید دید که کدام آدرس‌ها شروع به "فساد" کرده و نیاز به تعویض دارند.
  • چرخش User-Agent به همراه چرخش IP به طور قابل توجهی احتمال شناسایی را کاهش می‌دهد: آنها را به صورت همزمان تغییر دهید، نه در چرخه‌های جداگانه.
  • برای پارسرهای طولانی‌مدت، یک بررسی سلامت اضافه کنید: هر چند دقیقه یک بار یک درخواست آزمایشی به https://httpbin.org/ip از طریق هر پروکسی ارسال کنید و پاسخ‌گوهای غیرپاسخگو را حذف کنید.

توجه ویژه‌ای به اثرات TLS (JA3/JA3S) داشته باشید. reqwest به طور پیش‌فرض از rustls یا nativetls بسته به ویژگی‌های ساخت استفاده می‌کند و هر دو اثرات متفاوتی از دست دادن دست دادن TLS دارند. برخی از سیستم‌های ضد ربات (به عنوان مثال، در بازارهای بزرگ) به طور خاص اثر انگشت TLS را تجزیه و تحلیل می‌کنند، نه فقط IP — بنابراین حتی یک پروکسی ایده‌آل نجات نخواهد داد اگر خود مشتری خود را با یک handshake غیرمعمول TLS معرفی کند. اگر با مسدودسازی‌ها در پروکسی‌های "تمیز" مواجه شدید، ویژگی‌های reqwest را بررسی کنید و سعی کنید ویژگی native-tls را به جای rustls یا برعکس امتحان کنید.

نتیجه‌گیری

پروکسی در Rust reqwest به سرعت تنظیم می‌شوند — چند خط با Proxy::all یا الگوی SOCKS5 socks5h://. کار اصلی از آنجا آغاز می‌شود: ساخت استخر مشتریان، انتخاب استراتژی چرخش (round-robin، تصادفی یا جلسات چسبنده) و پردازش صحیح خطاها با حذف آدرس‌های غیرکاربردی از استخر. بدون این موارد حتی سریع‌ترین پارسر در Rust پس از چند دقیقه کار با مسدودسازی مواجه خواهد شد.

اگر پارسر شما با بازارها یا شبکه‌های اجتماعی کار می‌کند که محافظت ضد ربات نه تنها فرکانس درخواست‌ها، بلکه منبع IP را نیز تحلیل می‌کند، با پروکسی‌های مسکونی شروع کنید — آنها بهترین تعادل قیمت و ماندگاری جلسات را ارائه می‌دهند. برای اهداف سختگیرانه‌تر مانند Instagram و TikTok باید IP های موبایلی را آزمایش کنید، و برای کارهای ساده بدون ضد ربات، پروکسی‌های سریع و ارزان مرکز داده به خوبی کافی خواهند بود.