Scraping bất đồng bộ là nơi các thiết lập proxy âm thầm sụp đổ. Với requests bạn xoay một IP cho mỗi lần gọi rồi đi tiếp; với asyncio bạn đột nhiên có 200 request đang chạy cùng lúc, tất cả đều cần exit riêng, session affinity riêng và cơ chế retry riêng khi mục tiêu trả về 403. Làm sai và bạn hoặc là nện một IP đến mức bị ban, hoặc là phá nát mọi session đăng nhập vì xoay IP giữa luồng.
Hướng dẫn này trình bày những mẫu thực sự trụ vững ở mức đồng thời cao: xoay theo từng request trong httpx và aiohttp, một worker pool có giới hạn, sticky session cho các luồng có trạng thái, và retry nhận biết được chặn — kèm code chạy được mà bạn có thể dán vào.
Một scraper đồng bộ chạm vào một proxy tại một thời điểm, nên "xoay ở mỗi request" hiển nhiên là đúng. Async phá vỡ ba giả định cùng một lúc:
httpx.AsyncClient gắn một proxy cho mỗi client, nên để xoay bạn chọn proxy theo từng request và định tuyến qua một pool nhỏ các client được đánh khóa theo exit:
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
Tái sử dụng một client cho mỗi exit giúp HTTP/2 và connection pooling luôn sống thay vì phải trả giá cho một lần bắt tay mới ở mỗi request. Lưu ý httpx>=0.28 đã đổi tên proxies= thành proxy= trên client.
aiohttp đơn giản hơn ở đây — một ClientSession duy nhất nhận tham số proxy= theo từng request, nên bạn xoay ngay trong dòng:
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 hỗ trợ proxy HTTP một cách tự nhiên; với socks5h:// hãy thêm aiohttp-socks và truyền vào một ProxyConnector.
gather không giới hạn là cách nhanh nhất để khiến mọi IP trong pool của bạn bị gắn cờ cùng một lúc. Hãy giới hạn nó để mỗi exit gánh một số lượng request song song hợp lý như con người:
sem = asyncio.Semaphore(10) # at most 10 in flight
async def fetch_capped(session, url):
async with sem:
return await fetch(session, url)
Quy tắc ngón tay cái: giữ mức đồng thời bằng hoặc thấp hơn số lượng exit riêng biệt bạn có, để bạn không chồng nhiều request song song lên một IP residential duy nhất.
Xoay vòng là để vào được; sticky session là để ở lại được. Một lần đăng nhập, một giỏ hàng, bất cứ thứ gì gắn với cookie đều phải giữ một exit cho toàn bộ chuỗi. Hãy ghim proxy theo từng task thay vì theo từng request:
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))))
Hãy dùng một nhà cung cấp hỗ trợ sticky residential session để cùng một exit được giữ trong suốt vòng đời của task, chứ không bị âm thầm xoay vòng dưới chân bạn.
Ở mức đồng thời cao bạn không thể tin một mã 200 — các hệ thống anti-bot trả về 200 kèm một body thử thách. Hãy phát hiện việc bị chặn và retry riêng task thất bại đó trên một exit mới:
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}")
Có một cái bẫy mà async không giải quyết được: cả httpx và aiohttp đều nằm trên TLS stack của Python, nên chúng gửi đi một JA3 mà không trình duyệt thật nào tạo ra. Một pool xoay vòng hoàn hảo trên một IP residential sạch vẫn bị nhận diện là automation ngay ở bước bắt tay. Hãy kết hợp xoay vòng async với việc giả mạo TLS — xem Vượt Qua TLS Fingerprinting với curl_cffi, và kiểm tra JA3 + ASN của exit của bạn tại www.jibaoproxy.com/tools/fingerprint.html.
Người dùng mới nhận 500MB khi đăng ký, cùng tiền thưởng cho lần nạp đầu tiên. Ưu đãi có thời hạn.