کتابخانه httpx به استانداردی برای درخواستهای HTTP غیرهمزمان در Python تبدیل شده است، اما هنگام کار با پروکسی، نکات خاصی وجود دارد که پارسرها را در پروسه خراب میکند. بررسی میکنیم که چگونه چرخش غیرهمزمان پروکسی را تنظیم کنیم، HTTP/2 را فعال کنیم و چرا اسکریپت شما هر 50-100 درخواست سقوط میکند.
چرا httpx و نه requests یا aiohttp
requests همزمان است و به طور پیشفرض از HTTP/2 پشتیبانی نمیکند. aiohttp غیرهمزمان است، اما تنظیم پروکسی در آن کمتر دوستانه است و پشتیبانی از HTTP/2 به طور کامل وجود ندارد. httpx هر دو سوال را حل میکند: یک کلاینت همزمان و غیرهمزمان با یک API واحد، پشتیبانی بومی HTTP/2 از طریق افزونه h2 و کار با پروکسی به صورت انعطافپذیر در سطح انتقال.
برای وظایف پارس کردن بازارها (Wildberries، Ozon، Avito)، جمعآوری دادهها از API شبکههای اجتماعی یا نظارت بر قیمتهای رقبای خود، غیرهمزمان بودن بسیار حیاتی است — زیرا سرعت را به طور قابل توجهی نسبت به درخواستهای متوالی افزایش میدهد. اما بدون چرخش صحیح پروکسی و مدیریت اتصالات، غیرهمزمان بودن به منبع جدیدی از مشکلات تبدیل میشود: تمام شدن استخر اتصالات، کوروتینهای معلق، نشت توصیفگرهای فایل.
نکته دوم مهم — HTTP/2 میتواند چندین درخواست را در یک اتصال TCP چندگانه کند، که بار روی سرور هدف را کاهش میدهد و به دور زدن برخی از سیستمهای ضد ربات ساده کمک میکند که به تعداد هاندشیکهای TCP از یک IP نگاه میکنند.
تنظیمات پایه پروکسی در httpx
از نسخه httpx 0.26+، نحو تنظیم پروکسی تغییر کرده است — به جای دیکشنری proxies از پارامتر proxy استفاده میشود. این یک منبع رایج سردرگمی است، اگر شما به آموزشهای قدیمی نگاه میکنید.
import httpx
import asyncio
PROXY_URL = "http://user:[email protected]:8000"
async def fetch(url: str):
async with httpx.AsyncClient(proxy=PROXY_URL, timeout=15.0) as client:
response = await client.get(url)
return response.status_code, response.text[:200]
asyncio.run(fetch("https://httpbin.org/ip"))
اگر نیاز به تقسیم پروکسی برای ترافیک HTTP و HTTPS یا تنظیم پروکسیهای مختلف برای دامنههای مختلف دارید، از httpx.Client(mounts=...) استفاده کنید — حمل و نقلها بر اساس الگوی URL نصب میشوند:
mounts = {
"http://": httpx.AsyncHTTPTransport(proxy="http://user:[email protected]:8000"),
"https://": httpx.AsyncHTTPTransport(proxy="http://user:[email protected]:8000"),
}
async with httpx.AsyncClient(mounts=mounts) as client:
response = await client.get("https://example.com")
برای احراز هویت با نام کاربری و رمز عبور، فرمت http://user:pass@host:port به طور پایدار برای پروکسی HTTP کار میکند. برای SOCKS5 نیاز به نصب اضافی httpx[socks] دارید، در غیر این صورت در زمان اجرا با ImportError مواجه خواهید شد، نه در مرحله نوشتن کد — این یک تله است که بسیاری در آن گرفتار میشوند.
چرخش غیرهمزمان پروکسی: کد کارا
بزرگترین اشتباه در چرخش پروکسی — ایجاد یک AsyncClient جدید برای هر درخواست است. این باعث کاهش عملکرد میشود، زیرا کلاینت یک استخر اتصالات را نگه میدارد و بازسازی آن تمام مزایای اتصالات keep-alive را از بین میبرد. رویکرد صحیح — از قبل یک استخر کلاینت ایجاد کنید، یکی برای هر پروکسی، و وظایف را بین آنها توزیع کنید.
import httpx
import asyncio
import itertools
import random
PROXIES = [
"http://user:[email protected]:8000",
"http://user:[email protected]:8000",
"http://user:[email protected]:8000",
]
class ProxyPool:
def __init__(self, proxies: list[str]):
self.clients = [
httpx.AsyncClient(proxy=p, timeout=httpx.Timeout(15.0, connect=5.0))
for p in proxies
]
self._cycle = itertools.cycle(self.clients)
def get_client(self) -> httpx.AsyncClient:
return next(self._cycle) # round-robin
# برای انتخاب تصادفی: random.choice(self.clients)
async def close_all(self):
for client in self.clients:
await client.aclose()
async def fetch_with_retry(pool: ProxyPool, url: str, retries: int = 3):
for attempt in range(retries):
client = pool.get_client()
try:
response = await client.get(url)
if response.status_code == 200:
return response.text
except (httpx.ConnectTimeout, httpx.ProxyError, httpx.ReadTimeout):
await asyncio.sleep(0.5 * (attempt + 1))
continue
return None
async def main():
pool = ProxyPool(PROXIES)
urls = [f"https://example.com/page/{i}" for i in range(100)]
sem = asyncio.Semaphore(20) # محدود کردن همزمانی
async def bound_fetch(url):
async with sem:
return await fetch_with_retry(pool, url)
results = await asyncio.gather(*(bound_fetch(u) for u in urls))
await pool.close_all()
return results
asyncio.run(main())
این الگو سه مشکل را به طور همزمان حل میکند: اتصالات TCP را از طریق keep-alive دوباره استفاده میکند، بار را به صورت round-robin بین پروکسیها توزیع میکند و همزمانی را از طریق سمفور محدود میکند تا سرور هدف را خراب نکند و از مسدود شدن به دلیل محدودیت نرخ جلوگیری کند.
برای پارسرهای تولیدی که با Wildberries، Ozon یا API اینستاگرام کار میکنند، مهم است که استخر پروکسی به اندازه کافی بزرگ و متنوع از نظر جغرافیایی باشد — در غیر این صورت چرخش در یک زیرشبکه نمیتواند از مسدود شدن جلوگیری کند. پروکسیهای مسکونی به خوبی مناسب هستند، زیرا آنها IP کاربران واقعی را دارند و به ندرت تحت فیلترهای سیستمهای ضد ربات قرار میگیرند که ASN و شهرت دامنه را تجزیه و تحلیل میکنند.
HTTP/2 در httpx: چرا و چگونه فعال کنیم
به طور پیشفرض httpx از HTTP/1.1 استفاده میکند. برای فعال کردن HTTP/2، باید افزونه را نصب کنید و به وضوح پرچم را منتقل کنید:
# pip install httpx[http2]
async with httpx.AsyncClient(http2=True, proxy=PROXY_URL) as client:
response = await client.get("https://example.com")
print(response.http_version) # انتظار داریم "HTTP/2"
نکته مهم: همه پروکسیها به درستی از تونلسازی HTTP/2 پشتیبانی نمیکنند. برخی از پروکسیهای ارزان مراکز داده فقط به عنوان پروکسی HTTP/1.1 کار میکنند حتی اگر سرور نهایی از HTTP/2 پشتیبانی کند — در این صورت httpx به آرامی به HTTP/1.1 از طریق تونل CONNECT برمیگردد و شما همیشه این را بدون بررسی صریح response.http_version متوجه نخواهید شد.
HTTP/2 در سه سناریو واقعی سود میدهد: زمانی که شما درخواستهای زیادی به یک دامنه میزنید (چندگانهسازی در یک اتصال تأخیرها را کاهش میدهد)، زمانی که مهم است که قابل مشاهده بودن درخواستها برای حفاظت ضد ربات کاهش یابد (تعداد کمتر هاندشیکهای TCP از یک IP طبیعیتر به نظر میرسد) و زمانی که سایت هدف خود HTTP/2 را درخواست میکند و به مشتریان در HTTP/1.1 خطا یا محتوای محدود برمیگرداند — این گاهی در سایتهای Cloudflare مشاهده میشود.
برای وظایفی که سرعت و ثبات اتصالات HTTP/2 از طریق پروکسی مهم است، پروکسیهای اختصاصی مراکز داده بهتر هستند — آنها سریعتر از پروکسیهای مسکونی هستند و معمولاً به درستی از تونلسازی پروتکلهای مدرن پشتیبانی میکنند.
5 خطای رایج با پروکسی در httpx
1. بازسازی کلاینت برای هر درخواست
کد به شکل async with httpx.AsyncClient(...) as client: await client.get(url) درون حلقه — این یک الگوی ضد است. هر بار که کلاینت ایجاد میشود — یک اتصال TCP جدید، یک هاندشیک TLS جدید، و کامل نادیده گرفتن استخر اتصالات. در 1000 درخواست، تفاوت سرعت بین رویکرد صحیح و نادرست میتواند به 5-10 برابر برسد.
2. عدم وجود تایماوتها یا تایماوتهای بسیار بزرگ به طور پیشفرض
به طور پیشفرض httpx یک تایماوت 5 ثانیهای برای همه چیز تعیین میکند — اتصال، خواندن، نوشتن، استخر. هنگام کار با پروکسیهای ناپایدار، این برای اتصال کم است، اما برای خواندن زیاد است، اگر پروکسی فقط "آویزان" باشد. تایماوتها را به وضوح تقسیم کنید:
timeout = httpx.Timeout(
connect=5.0,
read=10.0,
write=10.0,
pool=5.0
)
client = httpx.AsyncClient(proxy=PROXY_URL, timeout=timeout)
3. نادیده گرفتن محدودیتهای استخر اتصالات
با تعداد زیادی از وظایف همزمان (gather بر روی 500+ کوروتین بدون سمفور) httpx شروع به پرتاب PoolTimeout میکند، زیرا محدودیت اتصالات در استخر (به طور پیشفرض 100) تمام شده است. محدودیتها را به وضوح از طریق httpx.Limits(max_connections=200, max_keepalive_connections=50) تنظیم کنید و همیشه از سمفور برای کنترل همزمانی استفاده کنید.
4. غیرفعال کردن verify بدون درک عواقب
بسیاری از افراد verify=False را از Stack Overflow کپی میکنند تا از خطاهای SSL هنگام کار با پروکسیهای دارای گواهی MITM خلاص شوند. این راه را برای حملات "انسان در میانه" باز میکند و میتواند منجر به نشت دادهها از طریق پروکسیهای آسیبدیده شود. اگر پروکسی به گواهی خاصی نیاز دارد — آن را به وضوح از طریق verify="/path/to/cert.pem" اضافه کنید، نه اینکه بررسی را به طور کامل غیرفعال کنید.
5. کلاینتهای غیر بسته و نشت توصیفگرهای فایل
اگر شما یک استخر کلاینت به صورت دستی ایجاد میکنید (مانند مثال بالا)، فراموش نکنید که await client.aclose() را برای هر یک از آنها هنگام پایان کار فراخوانی کنید. اسکریپتهای طولانی بدون بستن صریح اتصالات به محدودیت تعداد توصیفگرهای فایل باز در سیستم عامل (معمولاً 1024 در Linux به طور پیشفرض) برخورد میکنند و پارسر با خطای مرموز Too many open files پس از چند ساعت کار سقوط میکند.
کدام نوع پروکسی برای پارس کردن انتخاب کنیم
انتخاب پروکسی به طور مستقیم بر این تأثیر میگذارد که چند خطای موجود در لیست بالا را شما به طور کلی مشاهده خواهید کرد — پروکسیهای ناپایدار باعث ایجاد تایماوتها و قطع اتصالات میشوند، حتی اگر کد به طور کامل نوشته شده باشد.
| نوع پروکسی | سرعت | ریسک مسدود شدن | کی استفاده کنیم |
|---|---|---|---|
| مرکز داده | بالا | بالا در سایتهای محافظت شده | API باز، حفاظت ضد ربات ضعیف |
| مسکونی | متوسط | پایین | Wildberries، Ozon، شبکههای اجتماعی |
| موبایل | متوسط-پایین | حداقل | API اینستاگرام، TikTok، حفاظت سخت |
برای پارس کردن غیرهمزمان با سرعت بالای درخواستها به API با حفاظت متوسط، پروکسیهای مراکز داده همچنان گزینهای اقتصادی هستند — نکته اصلی این است که بار را به طور یکنواخت بین استخر توزیع کنید، همانطور که در کد بالا نشان داده شده است.
چکلیست قبل از تولید
- کلاینتهای httpx یک بار ایجاد میشوند و دوباره استفاده میشوند، نه برای هر درخواست
- تایماوتها به connect/read/write/pool تقسیم شدهاند، نه اینکه از پیشفرض استفاده شود
- همزمانی از طریق
asyncio.Semaphoreمحدود شده است - محدودیتهای استخر اتصالات بر اساس بار واقعی تنظیم شدهاند
- منطق retry با پردازش ProxyError، ConnectTimeout، ReadTimeout وجود دارد
- همه کلاینتها به وضوح از طریق
aclose()هنگام پایان کار بسته میشوند - verify=False بدون ضرورت شدید استفاده نمیشود
- HTTP/2 به طور آگاهانه فعال شده و از طریق response.http_version بررسی شده است
نتیجهگیری
httpx ابزار قدرتمندی برای کار غیرهمزمان با پروکسی و HTTP/2 ارائه میدهد، اما نیاز به تنظیم دقیق استخر کلاینتها، تایماوتها و محدودیتهای اتصالات دارد. بیشتر سقوطهای پارسرها در تولید به کتابخانه مربوط نمیشود، بلکه به اشتباهات معماری مربوط میشود: بازسازی کلاینتها، عدم محدودیت همزمانی و پردازش نادرست استثنائات پروکسی.
اگر شما در حال ساخت پارسری برای بازارها یا API شبکههای اجتماعی هستید که به طور فعال فعالیتهای مشکوک را زیر نظر دارند، ثبات زیرساخت پروکسی به اندازه کد مهم است. برای چنین وظایفی، بهتر است به پروکسیهای مسکونی توجه کنید — زیرا آنها سهم خطاهای ProxyError و ConnectTimeout را به دلیل شهرت بالاتر IP آدرسها نسبت به استخرهای ارزان مراکز داده کاهش میدهند.