چرخش پروکسی در پایتون async: httpx و aiohttp (۲۰۲۶)

منتشر شده در 12 ژوئن 2026 · زمان مطالعه ≈ 10 دقیقه

اسکرپینگ ناهمگام (async) جایی است که تنظیمات پروکسی بی‌سروصدا از هم می‌پاشد. با requests هر فراخوانی یک IP می‌چرخانید و رد می‌شوید؛ با asyncio ناگهان ۲۰۰ درخواست در پرواز دارید که هرکدام به خروجی خود، چسبندگی نشست خود و تلاش مجدد خود هنگام ۴۰۳ نیاز دارند. اشتباه کنید و یا یک IP را تا بن می‌کوبید یا با چرخش وسط جریان، هر نشست ورود را پاره می‌کنید.

این راهنما الگوهایی را نشان می‌دهد که واقعاً زیر هم‌روندی دوام می‌آورند: چرخش هر-درخواست در httpx و aiohttp، یک استخر کارگر محدود، نشست‌های چسبنده برای جریان‌های حالت‌دار و تلاش مجدد آگاه از بلاک — همراه کد کاربردی.

چرا async مسئله‌ی پروکسی را تغییر می‌دهد

یک اسکرپر همگام هر بار یک پروکسی را لمس می‌کند، پس «چرخش در هر درخواست» بدیهی است. async سه فرض را یک‌جا می‌شکند:

httpx: چرخش پروکسی در هر درخواست

httpx.AsyncClient یک پروکسی به هر کلاینت می‌بندد، پس برای چرخش، پروکسی را در هر درخواست انتخاب می‌کنید و از یک استخر کوچک کلاینت کلیدخورده با خروجی عبور می‌دهید:

import asyncio, itertools, httpx

PROXIES = [
    "socks5h://USERNAME:[email protected]:913",
    "socks5h://USERNAME:[email protected]:913",
    "socks5h://USERNAME:[email protected]:913",
]
pool = itertools.cycle(PROXIES)

# One reusable client per exit (connection pooling stays intact)
clients = {p: httpx.AsyncClient(proxy=p, timeout=30) for p in PROXIES}

async def fetch(url):
    proxy = next(pool)
    r = await clients[proxy].get(url)
    return r.status_code, r.text

async def main(urls):
    results = await asyncio.gather(*(fetch(u) for u in urls))
    for client in clients.values():
        await client.aclose()
    return results

استفاده‌ی دوباره از یک کلاینت به‌ازای هر خروجی، HTTP/2 و استخر اتصال را زنده نگه می‌دارد به‌جای پرداخت یک handshake تازه در هر درخواست. توجه: httpx>=0.28 روی کلاینت proxies= را به proxy= تغییر نام داد.

aiohttp: چرخش در سطح درخواست

aiohttp اینجا ساده‌تر است — یک ClientSession آرگومان proxy= را در هر درخواست می‌گیرد، پس درون‌خطی می‌چرخانید:

import asyncio, itertools, aiohttp

pool = itertools.cycle(PROXIES)  # http:// or socks5h:// (needs aiohttp-socks for SOCKS)

async def fetch(session, url):
    proxy = next(pool)
    async with session.get(url, proxy=proxy, timeout=aiohttp.ClientTimeout(total=30)) as r:
        return r.status, await r.text()

async def main(urls):
    async with aiohttp.ClientSession() as session:
        return await asyncio.gather(*(fetch(session, u) for u in urls))

aiohttp پروکسی HTTP را به‌صورت بومی می‌فهمد؛ برای socks5h:// کتابخانه‌ی aiohttp-socks را اضافه کنید و یک ProxyConnector پاس دهید.

هم‌روندی را با سمافور محدود کنید

gather نامحدود سریع‌ترین راه برای بن‌شدن هم‌زمان همه‌ی IPهای استخر است. محدودش کنید تا هر خروجی شمار باورپذیری از درخواست‌های موازی را حمل کند:

sem = asyncio.Semaphore(10)   # at most 10 in flight

async def fetch_capped(session, url):
    async with sem:
        return await fetch(session, url)

قاعده‌ی سرانگشتی: هم‌روندی را برابر یا کمتر از تعداد خروجی‌های مجزای خود نگه دارید تا درخواست‌های موازی زیادی را روی یک IP مسکونی تلنبار نکنید.

نشست‌های چسبنده برای جریان‌های حالت‌دار

چرخش برای ورود است؛ نشست‌های چسبنده برای ماندن. ورود، سبد خرید، هر چیز وابسته به cookie باید یک خروجی را برای کل دنباله نگه دارد. پروکسی را به‌ازای هر کار سنجاق کنید، نه هر درخواست:

async def run_account(account, proxy):
    # Same exit IP for every step of this account's flow
    async with httpx.AsyncClient(proxy=proxy, timeout=30) as client:
        await client.post("https://target.site/login", data=account.creds)
        await client.get("https://target.site/dashboard")
        await client.post("https://target.site/cart", json=account.order)

async def main(accounts):
    # One sticky exit per account, accounts run concurrently
    await asyncio.gather(*(run_account(a, p)
                           for a, p in zip(accounts, itertools.cycle(PROXIES))))

از ارائه‌دهنده‌ای با نشست‌های مسکونی چسبنده استفاده کنید تا همان خروجی برای طول عمر کار نگه داشته شود و بی‌صدا نچرخد.

تلاش مجدد آگاه از بلاک

زیر هم‌روندی نمی‌توان به 200 اعتماد کرد — سامانه‌های ضدبات 200 با بدنه‌ی چالش برمی‌گردانند. بلاک را تشخیص دهید و تنها کارِ ناموفق را روی خروجی تازه دوباره تلاش کنید:

BLOCK_MARKERS = ("just a moment", "/cdn-cgi/challenge-platform",
                 "datadome", "px-captcha", "access denied")

def looks_blocked(status, text):
    return status in (403, 429, 503) or any(m in text[:4000].lower() for m in BLOCK_MARKERS)

async def fetch_retry(url, attempts=4):
    for _ in range(attempts):
        proxy = next(pool)
        async with httpx.AsyncClient(proxy=proxy, timeout=30) as c:
            r = await c.get(url)
            if not looks_blocked(r.status_code, r.text):
                return r
        await asyncio.sleep(0.5)
    raise RuntimeError(f"blocked after {attempts} exits: {url}")

اثرانگشت هنوز باید بخواند

یک چیز را async حل نمی‌کند: httpx و aiohttp هر دو روی پشته‌ی TLS پایتون می‌نشینند، پس JA3‌ای می‌فرستند که هیچ مرورگر واقعی تولید نمی‌کند. یک استخر چرخش بی‌نقص روی IP مسکونی تمیز هم در handshake به‌عنوان اتوماسیون اثرانگشت می‌خورد. چرخش async را با جعل TLS جفت کنید — دور زدن فینگرپرینتینگ TLS با curl_cffi را ببینید و JA3 و ASN خروجی را در check.jibaoproxy.com بررسی کنید.

چک‌لیست

برای همه محصولات IP · هزاران نود همیشه در دسترس

همین حالا ثبت‌نام کنید و تا ۱۰۰٪ کش‌بک شارژ بگیرید

کاربران جدید با ثبت‌نام 500MB هدیه می‌گیرند، به‌علاوه بونوس اولین شارژ. پیشنهاد محدود.