ProxynetProxynet

مقایسه HTTPX، Requests و AIOHTTP

تاریخ انتشار:

11 دقیقه مطالعه

Acar Diveroli
نویسنده: Acar Diveroli
بلوک‌های کد در سه ستون

برای ارسال درخواست HTTP در Python سه کتابخانه برجسته‌اند: Requests، HTTPX و AIOHTTP. هر سه کار اصلی یکسانی انجام می‌دهند، اما تفاوت آن‌ها هنگام نوشتن یک اسکریپت کوچک دیده نمی‌شود؛ این تفاوت وقتی آشکار می‌شود که باید هزاران درخواست را هم‌زمان مدیریت کنید. در این نوشته سه کتابخانه را از نظر سادگی استفاده، هم‌زمانی، پشتیبانی از پروکسی، مدیریت خطا و اکوسیستم مقایسه می‌کنیم، برای هرکدام نمونه‌ای کارا می‌آوریم و مسیر گذر از Requests به کد ناهمگام را نشان می‌دهیم.

کدهای نمونه با Python 3.13 و نسخه‌های Requests 2.32، HTTPX 0.28 و AIOHTTP 3.13 آزموده شده‌اند.

سه کتابخانه به اختصار

  • Requests: رایج‌ترین کتابخانه HTTP در Python. به‌صورت همگام کار می‌کند، یادگیری آن بسیار آسان است و مستندات و نمونه‌هایش همه‌جا پیدا می‌شود. به یاد داشته باشید که برنامه تا پایان هر درخواست روی همان خط منتظر می‌ماند.
  • HTTPX: رابطی بسیار شبیه به Requests را هم به‌صورت همگام و هم ناهمگام ارائه می‌دهد. از HTTP/2 نیز پشتیبانی می‌کند. کم‌زحمت‌ترین راه برای انتقال کد موجود Requests به حالت ناهمگام معمولاً HTTPX است.
  • AIOHTTP: کتابخانه‌ای که از ابتدا ناهمگام طراحی شده است. علاوه بر کلاینت، یک وب‌سرور هم دارد. برای کارهایی که هم‌زمانی بسیار بالا می‌خواهند انتخابی بالغ و پرکاربرد است.

همگام و ناهمگام یعنی چه؟

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

بیشتر زمان یک درخواست وب در شبکه سپری می‌شود، یعنی در انتظار پاسخ سرور مقصد. در کد همگام این انتظار هدر می‌رود؛ در کد ناهمگام در همان مدت می‌توان منتظر ده‌ها درخواست دیگر ماند. این تفاوت هنگام دریافت صد صفحه احساس نمی‌شود؛ هنگام دریافت ده هزار صفحه به تفاوت میان ساعت‌ها و دقیقه‌ها تبدیل می‌شود.

بهای این برتری پیچیدگی است: نحو async/await، حلقه رویداد و ناسازگاری با کتابخانه‌هایی که ناهمگام نیستند. به همین دلیل گفتن اینکه «ناهمگام همیشه بهتر است» نادرست است؛ اگر نیازی نباشد، کد همگام خواناتر است و نگهداری آن آسان‌تر.

تفاوت‌های اصلی در یک جدول

ویژگیRequestsHTTPXAIOHTTP
استفاده همگامداردداردندارد
استفاده ناهمگامندارددارددارد
HTTP/2ندارددارد (با بسته اضافه)ندارد
پروکسی HTTP و HTTPSدرون‌ساختدرون‌ساختدرون‌ساخت
پروکسی SOCKSبا requests[socks]با httpx[socks]با بسته بیرونی
مهلت پیش‌فرضندارد (بی‌پایان منتظر می‌ماند)5 ثانیه5 دقیقه (کل)
دنبال کردن تغییرمسیربه‌طور پیش‌فرض فعالبه‌طور پیش‌فرض غیرفعالبه‌طور پیش‌فرض فعال
استفاده دوباره از اتصالبا Sessionبا Clientبا ClientSession
منحنی یادگیریبسیار آسانآسانمتوسط
مناسب‌ترین کاراسکریپت ساده، حجم کمپروژه‌های ترکیبی همگام و ناهمگامهم‌زمانی بالا

ردیف «مهلت پیش‌فرض» در جدول، تفاوتی است که میان این سه کتابخانه بیش از همه نادیده گرفته می‌شود. اگر در Requests مهلت تعیین نکنید، درخواست ممکن است تا ابد منتظر بماند؛ به همین دلیل افزودن timeout= به هر فراخوانی Requests باید به عادت تبدیل شود.

Requests: ساده و همگام

نصب:

bash
pip install requests

یک درخواست از طریق پروکسی:

python
import requests

PROXY = "http://user:pass@pr.proxynet.io:8000"
proxies = {"http": PROXY, "https": PROXY}

response = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=20)
print(response.status_code, response.json())

کلید https در دیکشنری proxies پروکسی‌ای است که هنگام رفتن به نشانی‌های HTTPS به کار می‌رود. اینکه مقدار آن با http:// شروع می‌شود درست است: ترافیک HTTPS درون اتصالی که با پروکسی برقرار شده تونل می‌شود.

اگر چند درخواست می‌فرستید از Session استفاده کنید؛ اتصال‌ها دوباره به کار می‌روند، کوکی‌ها حفظ می‌شوند و پروکسی فقط یک بار تنظیم می‌شود:

python
import requests

PROXY = "http://user:pass@pr.proxynet.io:8000"

with requests.Session() as session:
    session.proxies.update({"http": PROXY, "https": PROXY})
    session.headers.update({"User-Agent": "data-collector-bot/1.0"})
    for path in ["ip", "headers", "user-agent"]:
        r = session.get(f"https://httpbin.org/{path}", timeout=20)
        print(path, r.status_code)

محدودیت Requests هم‌زمانی است. اگر 1,000 صفحه را پشت سر هم دریافت کنید، زمان کل به مجموع زمان تک‌تک درخواست‌ها نزدیک می‌شود. می‌توان این مشکل را با concurrent.futures.ThreadPoolExecutor کاهش داد، اما در حجم‌های بسیار بالا یک کتابخانه ناهمگام کارآمدتر است.

HTTPX: میان دو دنیا

نصب:

bash
pip install httpx

استفاده همگام تقریباً همانند Requests است:

python
import httpx

PROXY = "http://user:pass@pr.proxynet.io:8000"

with httpx.Client(proxy=PROXY, timeout=20) as client:
    response = client.get("https://httpbin.org/ip")
    print(response.json())

نسخه ناهمگام همین کد، چند درخواست را هم‌زمان می‌فرستد:

python
import asyncio
import httpx

PROXY = "http://user:pass@pr.proxynet.io:8000"
URLS = ["https://httpbin.org/ip"] * 10

async def main():
    async with httpx.AsyncClient(proxy=PROXY, timeout=20) as client:
        responses = await asyncio.gather(*(client.get(url) for url in URLS))
        print([r.status_code for r in responses])

asyncio.run(main())

در اینجا 10 درخواست پشت سر هم نمی‌روند، بلکه هم‌زمان راه می‌افتند. زمان کل به زمان کندترین درخواست نزدیک می‌شود.

برای استفاده از HTTP/2 کافی است pip install "httpx[http2]" را نصب کنید و http2=True را به کلاینت بدهید. هنگام ارسال درخواست‌های زیاد به یک سرور، چون چندگانه‌سازی روی یک اتصال انجام می‌شود، هزینه برقراری اتصال کاهش می‌یابد. هنگام استفاده از پروکسی، HTTP/2 درون تونل CONNECT میان شما و سایت مقصد کار می‌کند؛ لازم نیست پروکسی از HTTP/2 پشتیبانی کند.

AIOHTTP: برای هم‌زمانی بالا

نصب:

bash
pip install aiohttp
python
import asyncio
import aiohttp

PROXY = "http://pr.proxynet.io:8000"
AUTH = aiohttp.BasicAuth("user", "pass")
URLS = ["https://httpbin.org/ip"] * 10

async def fetch(session, url):
    async with session.get(url, proxy=PROXY, proxy_auth=AUTH) as response:
        return response.status

async def main():
    async with aiohttp.ClientSession() as session:
        statuses = await asyncio.gather(*(fetch(session, url) for url in URLS))
        print(statuses)

asyncio.run(main())

در AIOHTTP پروکسی به‌جای نشست، در هر درخواست با پارامتر proxy= داده می‌شود. اطلاعات ورود را می‌توانید جداگانه با proxy_auth بدهید یا درون نشانی (http://user:pass@...) بنویسید. با این طراحی، دادن پروکسی متفاوت در هر درخواست به‌طور طبیعی ممکن است؛ اگر می‌خواهید فهرست IP خودتان را بچرخانید، AIOHTTP این کار را آسان می‌کند.

تفاوت دیگر AIOHTTP این است که بدنه پاسخ باید درون مدیر زمینه خوانده شود. فراخوانی await response.text() پس از خروج از بلوک async with خطا می‌دهد؛ بدنه را درون بلوک بخوانید و در یک متغیر نگه دارید.

محدود کردن هم‌زمانی

هنگام کار ناهمگام، هم‌زمانی را نامحدود رها نکنید. شروع یکباره ده هزار URL با asyncio.gather هم سقف توصیفگر فایل سیستم خودتان را تحت فشار می‌گذارد و هم سایت مقصد را فوراً به اعمال محدودیت نرخ وامی‌دارد. محدود کردن تعداد درخواست‌های باز هم‌زمان با asyncio.Semaphore از هر دو طرف محافظت می‌کند:

python
import asyncio
import httpx

PROXY = "http://user:pass@pr.proxynet.io:8000"
URLS = [f"https://httpbin.org/get?i={i}" for i in range(100)]
LIMIT = asyncio.Semaphore(10)

async def fetch(client, url):
    async with LIMIT:
        r = await client.get(url)
        return r.status_code

async def main():
    async with httpx.AsyncClient(proxy=PROXY, timeout=20) as client:
        results = await asyncio.gather(*(fetch(client, u) for u in URLS))
        print(sum(1 for s in results if s == 200), "successful")

asyncio.run(main())

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

مدیریت خطا و تلاش دوباره

در هر سه کتابخانه خطاهای شبکه به شکل استثنا می‌رسند؛ اما کدهای خطای HTTP (404، 429، 500) به‌طور پیش‌فرض استثنا نیستند. بررسی پاسخ بر عهده شماست:

  • در Requests و HTTPX فراخوانی response.raise_for_status() برای پاسخ‌های 4xx و 5xx استثنا ایجاد می‌کند.
  • در AIOHTTP همین کار با response.raise_for_status() یا با raise_for_status=True هنگام باز کردن نشست انجام می‌شود.

کدهایی که هنگام کار با پروکسی بیش از همه می‌بینید این‌ها هستند:

کدمعناچه باید کرد؟
407احراز هویت پروکسی ناموفق بودنام کاربری، رمز عبور و کدگذاری نویسه‌های ویژه را بررسی کنید
429سایت مقصد محدودیت نرخ اعمال می‌کندهم‌زمانی را کم کنید، صبر کنید و دوباره تلاش کنید، توزیع IP را بیشتر کنید
403سایت مقصد درخواست را رد می‌کندUser-Agent و نوع IP را بازبینی کنید
502 / 504پروکسی به مقصد نرسیدبا IP خروجی دیگری دوباره تلاش کنید

منطق تلاش دوباره را با انتظار نمایی بنویسید: در تلاش اول یک ثانیه، سپس دو، سپس چهار. پافشاری در فاصله‌های ثابت ممکن است به مسدود شدن کامل IP‌ای بینجامد که 429 گرفته است.

کارایی: کدام سریع‌تر است؟

در انتخاب کتابخانه، پرسش «کدام سریع‌تر است» معمولاً به جای اشتباهی نگاه می‌کند. بیشتر زمان یک درخواست وب در شبکه سپری می‌شود؛ زمان پردازش خود کتابخانه در کنار آن ناچیز است.

عامل تعیین‌کننده تعداد درخواست‌هایی است که می‌توانید هم‌زمان منتظرشان بمانید:

  • کلاینت همگام منتظر هر درخواست به نوبت می‌ماند.
  • کلاینت ناهمگام می‌تواند هم‌زمان منتظر صدها درخواست بماند.

یعنی برای کاری با 10 صفحه، Requests کافی است. برای کاری با 10,000 صفحه، کلاینت ناهمگام HTTPX یا AIOHTTP زمان را به‌طور چشمگیری کوتاه می‌کند. در یک دسته (میان HTTPX ناهمگام و AIOHTTP) تفاوت‌های قابل اندازه‌گیری وجود دارد و AIOHTTP در توان خام معمولاً جلوتر است؛ اما در بیشتر پروژه‌ها این تفاوت زیر سایه زمان پاسخ سایت مقصد و تأخیر پروکسی قرار می‌گیرد.

پروکسی و هم‌زمانی را چگونه با هم در نظر بگیریم؟

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

  • IP متفاوت در هر درخواست: با پروکسی چرخشی از یک نشانی ورودی استفاده می‌کنید و در هر درخواست IP خروجی متفاوتی می‌گیرید. نیازی به تغییر کد نیست؛ همه نمونه‌های بالا همان‌طور که هستند کار می‌کنند.
  • IP ثابت در طول نشست: در جریان‌هایی که وضعیت نگه می‌دارند، مانند ورود به حساب یا سبد خرید، پروکسی با نشست ثابت همان IP را برای مدت معینی حفظ می‌کند.
  • چرخاندن فهرست خودتان: اگر فهرست ثابتی از IP دارید، می‌توانید چرخش را در کد انجام دهید؛ برای نمونه گام‌به‌گام نوشته چرخاندن پروکسی‌ها در Python را ببینید.
  • نوع IP: در مقصدهای محافظت‌شده، نوع IP پیش از هم‌زمانی تعیین‌کننده است؛ دلیلش را در نوشته تفاوت پروکسی مسکونی و دیتاسنتر توضیح داده‌ایم.

گذر از Requests به HTTPX

اگر می‌خواهید یک پروژه موجود Requests را به حالت ناهمگام منتقل کنید، HTTPX کوتاه‌ترین راه است. تفاوت‌هایی که در این گذر باید در نظر بگیرید:

RequestsHTTPXیادداشت
requests.get(url)httpx.get(url)یکسان
proxies={"http": p, "https": p}proxy=pیک پارامتر
پیش‌فرض timeout=Noneپیش‌فرض timeout=5در HTTPX مهلت فعال است
تغییرمسیر به‌طور پیش‌فرض دنبال می‌شودfollow_redirects=True لازم استHTTPX به‌طور پیش‌فرض دنبال نمی‌کند
Session()Client() / AsyncClient()منطق یکسان
response.json()response.json()یکسان

بخش بزرگی از کد بدون تغییر کار می‌کند؛ تفاوت‌ها در پنج ردیف بالا جمع شده‌اند. اینکه ابتدا با Client همگام منتقل شوید و آزمون‌ها را اجرا کنید و سپس به AsyncClient بروید، خطر را کمتر می‌کند.

کدام را انتخاب کنید؟

  • Requests را انتخاب کنید: برای اسکریپت‌های کوچک، کارهای خودکارسازی، یکپارچه‌سازی با API و کارهایی که هم‌زمانی در آن‌ها مهم نیست.
  • HTTPX را انتخاب کنید: اگر امروز همگام شروع می‌کنید و احتمال دارد فردا به ناهمگام بروید، اگر HTTP/2 لازم دارید یا می‌خواهید هر دو شیوه را در یک پروژه داشته باشید.
  • AIOHTTP را انتخاب کنید: برای کارهای جمع‌آوری داده بسیار پرحجم که از ابتدا ناهمگام طراحی شده‌اند و برای برنامه‌ای که هم کلاینت و هم سرور لازم دارد.

اگر دریافت صفحه‌ها فقط با HTTP کافی نیست، یعنی محتوا با JavaScript بارگذاری می‌شود، این کتابخانه‌ها به‌تنها کافی نیستند و به خودکارسازی مرورگر نیاز دارید. این تمایز را در نوشته اسکرپینگ وب: JavaScript یا Python؟ بررسی کرده‌ایم. برای خودکارسازی مرورگر در Python راهنماهای Selenium و پروکسی در SeleniumBase را ببینید.

پرسش‌های متداول

گذر از Requests به HTTPX دشوار است؟

معمولاً نه. نوشتن httpx.get به‌جای requests.get در بیشتر کدهای ساده مستقیم کار می‌کند. تفاوت‌ها در جزئیاتی مانند فعال بودن پیش‌فرض مهلت، دنبال نشدن پیش‌فرض تغییرمسیرها و نام پارامتر پروکسی است. جدول گذر در بالا این تفاوت‌ها را فهرست می‌کند.

از پروکسی SOCKS5 چگونه استفاده می‌شود؟

برای Requests بسته pip install "requests[socks]" و برای HTTPX بسته pip install "httpx[socks]" را نصب کنید و در نشانی پروکسی از طرح socks5:// استفاده کنید. اگر می‌خواهید تفکیک DNS هم در سمت پروکسی انجام شود، در Requests بنویسید socks5h://. برای تفاوت‌های پروتکل‌ها نوشته تفاوت پروکسی SOCKS و HTTP را ببینید.

کد ناهمگام همیشه بهتر است؟

نه. کد ناهمگام پیچیده‌تر است و اشکال‌زدایی آن دشوارتر. برای کاری که واقعاً از هم‌زمانی سود نمی‌برد، کد همگام خواناتر است و نگهداری آن آسان‌تر.

استفاده از thread با Requests جایگزینی برای ناهمگام است؟

برای حجم‌های متوسط بله. با ThreadPoolExecutor می‌توان ده‌ها درخواست را موازی اجرا کرد و کد همگام می‌ماند. در صدها درخواست هم‌زمان، هزینه حافظه threadها بالا می‌رود؛ در آن نقطه کلاینت ناهمگام کارآمدتر است.

باید اطلاعات ورود پروکسی را در کد بنویسم؟

نه. هر سه کتابخانه متغیرهای محیطی HTTP_PROXY و HTTPS_PROXY را می‌خوانند (در HTTPX گزینه trust_env به‌طور پیش‌فرض فعال است). نگهداری اطلاعات ورود در متغیر محیطی خطر نشت آن‌ها را هنگام ارسال کد به مخزن کاهش می‌دهد.

هم‌زمان چند درخواست باز کنم؟

عدد درست یکتایی وجود ندارد. تحمل سایت مقصد، سقف اتصال هم‌زمان بسته پروکسی شما و محدودیت‌های دستگاه خودتان آن را تعیین می‌کنند. شروع با ده و افزایش آن تا زمانی که پاسخ 429 نبینید روشی مطمئن است.

جمع‌بندی

هر سه کتابخانه کار خود را به‌خوبی انجام می‌دهند؛ انتخاب به مقیاس پروژه بستگی دارد. Requests برای کارهای ساده، HTTPX برای انعطاف و آمادگی برای آینده و AIOHTTP برای هم‌زمانی بالا برجسته است. هر کتابخانه‌ای که انتخاب کنید، مهلت تعیین کنید، هم‌زمانی را محدود کنید و تلاش دوباره را با انتظار نمایی بنویسید. با افزایش حجم، راهبرد IP هم‌پای کتابخانه اهمیت پیدا می‌کند. برای زیرساخت جمع‌آوری داده در مقیاس بزرگ، راهکارهای استخراج داده ما را ببینید.

پرسش از ChatGPTپرسش از Claude