اسکرپینگ ناهمگام (async) جایی است که تنظیمات پروکسی بیسروصدا از هم میپاشد. با requests هر فراخوانی یک IP میچرخانید و رد میشوید؛ با asyncio ناگهان ۲۰۰ درخواست در پرواز دارید که هرکدام به خروجی خود، چسبندگی نشست خود و تلاش مجدد خود هنگام ۴۰۳ نیاز دارند. اشتباه کنید و یا یک IP را تا بن میکوبید یا با چرخش وسط جریان، هر نشست ورود را پاره میکنید.
این راهنما الگوهایی را نشان میدهد که واقعاً زیر همروندی دوام میآورند: چرخش هر-درخواست در httpx و aiohttp، یک استخر کارگر محدود، نشستهای چسبنده برای جریانهای حالتدار و تلاش مجدد آگاه از بلاک — همراه کد کاربردی.
یک اسکرپر همگام هر بار یک پروکسی را لمس میکند، پس «چرخش در هر درخواست» بدیهی است. async سه فرض را یکجا میشکند:
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 اینجا سادهتر است — یک 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 بررسی کنید.
کاربران جدید با ثبتنام 500MB هدیه میگیرند، بهعلاوه بونوس اولین شارژ. پیشنهاد محدود.