أصبحت مكتبة httpx معيارًا لطلبات HTTP غير المتزامنة في Python، ولكن عند العمل مع البروكسي، هناك بعض الفروق الدقيقة التي تكسر المحللات في الإنتاج. دعونا نناقش كيفية إعداد تدوير البروكسي غير المتزامن، وتفعيل HTTP/2 ولماذا يتعطل سكربتك كل 50-100 طلب.
لماذا httpx وليس requests أو aiohttp
requests متزامن ولا يدعم HTTP/2 بشكل افتراضي. aiohttp غير متزامن، لكن إعداد البروكسي هناك أقل ودية، ودعم HTTP/2 غير موجود على الإطلاق. httpx يغطي كلا السؤالين: عميل متزامن وغير متزامن مع واجهة برمجة تطبيقات موحدة، ودعم أصلي لـ HTTP/2 من خلال الإضافة h2، وعمل مرن مع البروكسي على مستوى النقل.
بالنسبة لمهام تحليل البيانات من الأسواق (Wildberries، Ozon، Avito)، جمع البيانات من واجهات برمجة التطبيقات لوسائل التواصل الاجتماعي أو مراقبة أسعار المنافسين، فإن عدم التزامن أمر حاسم - حيث يوفر زيادة كبيرة في السرعة مقارنة بالطلبات المتسلسلة. ولكن بدون تدوير بروكسي جيد وإدارة الاتصالات، يتحول عدم التزامن إلى مصدر لمشاكل جديدة: نفاد تجمع الاتصالات، الكوروتينات المعلقة، تسرب أوصاف الملفات.
النقطة المهمة الثانية - HTTP/2 يمكنه تعدد الطلبات في اتصال TCP واحد، مما يقلل من الحمل على الخادم المستهدف ويساعد في تجاوز بعض أنظمة مكافحة الروبوتات الساذجة التي تنظر إلى عدد عمليات TCP handshake من عنوان 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، ويحد من التوازي عبر semaphore، حتى لا يتعطل الخادم المستهدف ولا يتم حظره بسبب معدل الطلبات.
بالنسبة للمحللات الإنتاجية التي تعمل مع Wildberries أو Ozon أو واجهة برمجة تطبيقات Instagram، من المهم أن يكون تجمع البروكسي كبيرًا ومتعدد الجوانب جغرافيًا - وإلا فإن التدوير داخل نفس الشبكة الفرعية لن ينقذك من الحظر. هنا، تعتبر البروكسي السكنية خيارًا جيدًا، حيث أن لديها عناوين 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 handshakes من عنوان 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+ كوروتين بدون semaphore)، يبدأ httpx في طرح PoolTimeout، لأن حد الاتصالات في التجمع (بشكل افتراضي 100) قد تم استنفاده. قم بضبط الحدود بوضوح عبر httpx.Limits(max_connections=200, max_keepalive_connections=50) ودائمًا استخدم semaphore للتحكم في التوازي.
4. تعطيل التحقق بدون فهم العواقب
الكثيرون يقومون بنسخ verify=False من Stack Overflow للتخلص من أخطاء SSL عند العمل عبر بروكسيات مع شهادات MITM. هذا يفتح الطريق لهجمات "الرجل في المنتصف" ويمكن أن يؤدي إلى تسرب البيانات عبر بروكسيات مخترقة. إذا كان البروكسي يتطلب شهادة خاصة - أضفها بوضوح عبر verify="/path/to/cert.pem"، وليس تعطيل التحقق بالكامل.
5. عدم إغلاق العملاء وتسرب أوصاف الملفات
إذا كنت تقوم بإنشاء تجمع من العملاء يدويًا (كما في المثال أعلاه)، فلا تنسَ استدعاء await client.aclose() لكل منهم عند الانتهاء. السكربتات التي تعمل لفترة طويلة بدون إغلاق واضح للاتصالات تصل إلى حد أوصاف الملفات المفتوحة في نظام التشغيل (عادة 1024 على Linux بشكل افتراضي)، ويتعطل المحلل مع خطأ غامض Too many open files بعد بضع ساعات من العمل.
ما هو نوع البروكسي الذي يجب اختياره للمحللات
يؤثر اختيار البروكسي بشكل مباشر على عدد الأخطاء من القائمة أعلاه التي ستراها - البروكسيات غير المستقرة تسبب مهلات وانقطاع الاتصالات، حتى لو كان الكود مكتوبًا بشكل مثالي.
| نوع البروكسي | السرعة | خطر الحظر | متى تستخدمه |
|---|---|---|---|
| مركز البيانات | عالية | مرتفعة على المواقع المحمية | Open API، حماية ضعيفة ضد الروبوتات |
| سكنية | متوسطة | منخفضة | Wildberries، Ozon، وسائل التواصل الاجتماعي |
| محمول | متوسطة-منخفضة | أدنى | Instagram، واجهة برمجة تطبيقات TikTok، حماية صارمة |
بالنسبة للتحليل غير المتزامن مع سرعة عالية في الطلبات إلى واجهات برمجة التطبيقات مع حماية معتدلة، تظل بروكسيات مراكز البيانات خيارًا اقتصاديًا - الأهم هو توزيع الحمل بالتساوي عبر التجمع، كما هو موضح في الكود أعلاه.
قائمة التحقق قبل الإنتاج
- يتم إنشاء عملاء httpx مرة واحدة وإعادة استخدامها، وليس لكل طلب
- تم تقسيم المهلات إلى connect/read/write/pool، وليس استخدام الافتراضي
- تم تحديد التوازي عبر
asyncio.Semaphore - تم ضبط حدود تجمع الاتصالات وفقًا للحمل الفعلي
- يوجد منطق إعادة المحاولة مع معالجة ProxyError وConnectTimeout وReadTimeout
- يتم إغلاق جميع العملاء بوضوح عبر
aclose()عند الانتهاء - لا يتم استخدام verify=False بدون ضرورة قصوى
- تم تفعيل HTTP/2 بوعي وتم التحقق منه عبر response.http_version
الخاتمة
يوفر httpx أدوات قوية للعمل غير المتزامن مع البروكسي وHTTP/2، ولكنه يتطلب إعدادًا دقيقًا لتجمع العملاء، المهلات وحدود الاتصالات. معظم انهيارات المحللات في الإنتاج مرتبطة ليس بالمكتبة، ولكن بأخطاء معمارية: إعادة إنشاء العملاء، عدم وجود قيود على التوازي ومعالجة غير صحيحة لاستثناءات البروكسي.
إذا كنت تبني محللًا للأسواق أو واجهات برمجة تطبيقات وسائل التواصل الاجتماعي التي تراقب النشاط المشبوه بنشاط، فإن استقرار بنية البروكسي مهم بنفس القدر مثل الكود. لمثل هذه المهام، من الجيد النظر إلى البروكسي السكنية - حيث تقلل من نسبة أخطاء ProxyError وConnectTimeout بسبب سمعة أعلى لعناوين IP مقارنة بتجمعات مراكز البيانات الرخيصة.