إذا كنت تقوم بعملية تحليل مواقع أو أتمتة الطلبات عبر البروكسي في Python، فسوف تواجه عاجلاً أم آجلاً ثلاث
أخطاء كلاسيكية: ProxyError,
SSLError: WRONG_VERSION_NUMBER و HTTP
407 Proxy Authentication Required. في هذه المقالة، سنحلل
سبب كل منها وسنقدم شيفرة عملية للإصلاح.
كيف تعمل requests مع البروكسي: الصيغة الأساسية
مكتبة requests تقبل البروكسي عبر قاموس
proxies، حيث المفاتيح هي مخطط الطلب
(http و https),
والقيم هي عنوان URL لخادم البروكسي مع تفويض اختياري.
import requests
proxies = {
"http": "http://user:password@proxy_host:port",
"https": "http://user:password@proxy_host:port",
}
response = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=10)
print(response.json())
لاحظ: حتى بالنسبة لطلبات HTTPS، يتم عادةً تحديد مخطط البروكسي كـ
http://، لأن معظم خوادم البروكسي تقبل
نفق CONNECT عبر بروتوكول HTTP، وليس عبر تغليفه في SSL. إذا تم تحديد
https:// لبروكسي لا يدعم ذلك - ستحصل على
SSLError، والذي سيتم تحليله أدناه.
إذا كنت تعمل مع بروكسي SOCKS5، فستحتاج إلى حزمة إضافية:
pip install requests[socks]
# وتغير صيغة البروكسي:
proxies = {
"http": "socks5://user:password@proxy_host:port",
"https": "socks5://user:password@proxy_host:port",
}
ProxyError: الأسباب والحلول
requests.exceptions.ProxyError هو تغليف حول استثناءات الشبكة
المنخفضة المستوى التي تحدث عند محاولة إنشاء اتصال مع خادم البروكسي. غالبًا ما توجد
أربعة سيناريوهات.
1. البروكسي غير متاح أو العنوان/المنفذ محددان بشكل غير صحيح
requests.exceptions.ProxyError: HTTPSConnectionPool(host='example.com', port=443):
Max retries exceeded with url: / (Caused by ProxyError('Cannot connect to proxy.',
ConnectionResetError(104, 'Connection reset by peer')))
تحقق من أن المضيف والمنفذ مدخلان بشكل صحيح، وأن خادم البروكسي فعلاً يعمل. أسرع طريقة للتشخيص هي
محاولة الاتصال عبر curl:
curl -x http://user:password@proxy_host:port https://httpbin.org/ip -v
2. البروكسي حظر طلبك (حظر بواسطة IP أو User-Agent)
غالبًا ما يتم حظر البروكسيات الخاصة بمراكز البيانات من قبل المواقع المستهدفة بسبب أن نطاقات IP الخاصة بها معروفة لأنظمة مكافحة الاحتيال. إذا ظهرت الخطأ بشكل ثابت عند الوصول إلى موقع معين (مثل Wildberries أو Ozon أو الشبكات الاجتماعية)، وليس لجميع المواقع في نفس الوقت - فإن المشكلة ليست في البروكسي نفسه، بل في سمعته. في هذه الحالة، يساعد الانتقال إلى بروكسيات سكنية - فهي تستخدم IP حقيقي للمستخدمين وتتعرض للحظر بشكل أقل.
3. تجاوز الحد الأقصى للاتصالات المتزامنة
إذا كنت تقوم بتشغيل تحليل متعدد الخيوط وتفتح مئات الاتصالات عبر نفس البروكسي، قد يبدأ الخادم
في إعادة ضبط الاتصالات الزائدة. الحل هو استخدام مجموعة من الاتصالات عبر
requests.adapters.HTTPAdapter مع تحديد
pool_maxsize و
max_retries:
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
retries = Retry(total=3, backoff_factor=1, status_forcelist=[502, 503, 504])
adapter = HTTPAdapter(max_retries=retries, pool_connections=20, pool_maxsize=20)
session.mount("http://", adapter)
session.mount("https://", adapter)
response = session.get("https://httpbin.org/ip", proxies=proxies, timeout=10)
4. مهلة على جانب خادم البروكسي
إذا كان البروكسي "معلقًا" لفترة أطول من المهلة المحددة، سترى
ProxyError، مغلفًا في
ReadTimeoutError. قم بزيادة المهلة إلى قيمة معقولة
(عادةً 15-30 ثانية كافية) وأضف معالجة الاستثناء بشكل منفصل:
try:
response = requests.get(url, proxies=proxies, timeout=20)
except requests.exceptions.ProxyError as e:
print(f"مشكلة في البروكسي: {e}")
except requests.exceptions.Timeout:
print("لم يرد البروكسي في الوقت المحدد")
SSLError WRONG_VERSION_NUMBER: تحليل المشكلة
هذه الخطأ هي واحدة من أكثر الأخطاء غموضًا في البداية، ولكن في الواقع، لها سبب واحد تقريبًا دائمًا.
requests.exceptions.SSLError: HTTPSConnectionPool(host='example.com', port=443):
Max retries exceeded with url: / (Caused by SSLError(SSLError(1, '[SSL:
WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1123)')))
WRONG_VERSION_NUMBER تعني أن العميل حاول إجراء
SSL/TLS handshake حيث ينتظر الخادم حركة مرور HTTP العادية (بدون تشفير). غالبًا ما تكمن المشكلة في عدم
تحديد مخطط البروكسي بشكل صحيح في القاموس proxies.
خطأ شائع: تحديد https:// بدلاً من
http:// في عنوان خادم البروكسي. تقبل معظم بروكسيات HTTP(S)
الاتصال عبر HTTP العادي، حتى عند بروكسي حركة مرور HTTPS عبر آلية CONNECT.
# غير صحيح — سيسبب WRONG_VERSION_NUMBER
proxies = {
"http": "https://user:password@proxy_host:port",
"https": "https://user:password@proxy_host:port",
}
# صحيح
proxies = {
"http": "http://user:password@proxy_host:port",
"https": "http://user:password@proxy_host:port",
}
السبب الشائع الثاني هو المنفذ الذي لا يدعم فعليًا TLS. على سبيل المثال، إذا كان مزود البروكسي قد خصص منفذ 8080 فقط لحركة مرور HTTP، وأنت تحاول إنشاء اتصال TLS عليه مباشرة (متجاوزًا CONNECT)، سيرد الخادم بنص عادي بدلاً من مصافحة TLS، وستظهر مكتبة OpenSSL الخاصة بالعميل هذه الخطأ. تحقق في وثائق مزود البروكسي الخاص بك عن المنافذ والبروتوكولات (HTTP/HTTPS/SOCKS5) المتاحة على كل منفذ منفصل.
الخيار الثالث هو أنك تستخدم socket أو مكتبات خارجية
لإنشاء اتصال TCP مباشر مع البروكسي، متجاوزًا آلية requests القياسية. في هذه الحالة، تأكد من أن
تغليف TLS يتم تطبيقه فقط على الموقع المستهدف الحقيقي، وليس على الاتصال نفسه مع البروكسي.
407 Proxy Authentication Required: التفويض
رمز الاستجابة 407 هو نظير للرمز القياسي 401، ولكنه خاص بخوادم البروكسي. يعني أنه يتطلب الخادم التفويض، وأنك إما لم تقدمه، أو قدمته بشكل غير صحيح.
الطريقة 1: تسجيل الدخول وكلمة المرور مباشرة في URL
proxies = {
"http": "http://login:password@proxy_host:port",
"https": "http://login:password@proxy_host:port",
}
response = requests.get("https://httpbin.org/ip", proxies=proxies)
هذه الطريقة هي الأسهل، ولكن إذا كانت هناك رموز خاصة في اسم المستخدم أو كلمة المرور (مثل @,
:, /),
يجب عليك هروبها عبر urllib.parse.quote:
from urllib.parse import quote
login = quote("user@domain")
password = quote("p@ssw:ord!")
proxy_url = f"http://{login}:{password}@proxy_host:port"
proxies = {"http": proxy_url, "https": proxy_url}
الطريقة 2: رأس Proxy-Authorization يدويًا
إذا كان مزود البروكسي يستخدم مخطط تفويض غير قياسي أو كنت بحاجة إلى مزيد من التحكم، يمكنك تجميع الرأس يدويًا عبر ترميز base64:
import base64
import requests
login = "user"
password = "password"
credentials = base64.b64encode(f"{login}:{password}".encode()).decode()
headers = {
"Proxy-Authorization": f"Basic {credentials}"
}
proxies = {
"http": "http://proxy_host:port",
"https": "http://proxy_host:port",
}
response = requests.get(
"https://httpbin.org/ip",
proxies=proxies,
headers=headers,
timeout=10
)
من المهم: بعض مزودي البروكسي يربطون التفويض ليس باسم المستخدم/كلمة المرور، ولكن بعنوان IP ثابت محدد للعميل (قائمة بيضاء). إذا كنت تغير IP (على سبيل المثال، تعمل عبر VPN أو IP ديناميكي من المزود)، قد تظهر خطأ 407 حتى مع بيانات الاعتماد الصحيحة - لأن IP الخارجي الحالي الخاص بك غير مدرج في القائمة البيضاء على جانب البروكسي. الحل هو الدخول إلى لوحة التحكم الخاصة بالمزود وتحديث قائمة IP المسموح بها، أو التبديل إلى التفويض بواسطة اسم المستخدم/كلمة المرور بدلاً من القائمة البيضاء لـ IP.
منطق إعادة المحاولة ومعالجة الاستثناءات
في الممارسة العملية، نادرًا ما تكون أخطاء البروكسي لمرة واحدة - خاصة عند التحليل الجماعي. يجب أن تتضمن بنية الشيفرة الصحيحة محاولات متكررة مع تأخير وتسجيل كل خطأ بشكل منفصل.
import time
import requests
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def fetch_with_retry(url, proxies, max_retries=3, backoff=2):
for attempt in range(1, max_retries + 1):
try:
response = requests.get(url, proxies=proxies, timeout=15)
response.raise_for_status()
return response
except requests.exceptions.ProxyError as e:
logger.warning(f"[محاولة {attempt}] ProxyError: {e}")
except requests.exceptions.SSLError as e:
logger.warning(f"[محاولة {attempt}] SSLError: {e}")
except requests.exceptions.HTTPError as e:
if response.status_code == 407:
logger.error("407: تحقق من اسم المستخدم/كلمة المرور أو قائمة IP البيضاء للبروكسي")
break
logger.warning(f"[محاولة {attempt}] HTTPError: {e}")
except requests.exceptions.Timeout:
logger.warning(f"[محاولة {attempt}] مهلة الاتصال")
time.sleep(backoff * attempt)
raise RuntimeError(f"فشل الحصول على {url} بعد {max_retries} محاولات")
هذا النهج يوفر ساعات من التصحيح: بدلاً من انهيار السكربت عند أول عثرة في الشبكة، تحصل على سجل واضح يوضح نوع الخطأ المحدد في كل محاولة.
تدوير البروكسي ومكافحة الحظر
إذا كانت ProxyError و 407 مرتبطة غالبًا بإعدادات الشيفرة، فإن الحظر الجماعي والرفض الثابت للاتصالات على نطاقات معينة تشير تقريبًا دائمًا إلى مشكلة في جودة مجموعة IP نفسها. لمثل هذه المهام، مثل تحليل الأسواق (Wildberries و Ozon و Avito) أو العمل مع الشبكات الاجتماعية، يُوصى بتدوير IP مع كل طلب أو عبر فترة محددة.
import random
proxy_pool = [
"http://user:[email protected]:8000",
"http://user:[email protected]:8000",
"http://user:[email protected]:8000",
]
def get_random_proxy():
proxy = random.choice(proxy_pool)
return {"http": proxy, "https": proxy}
response = requests.get(url, proxies=get_random_proxy(), timeout=15)
بالنسبة لمحللات الإنتاج، عادةً ما يكون من الأسهل استخدام بروكسي مع تدوير تلقائي على جانب المزود - لذلك لا تحتاج إلى الاحتفاظ بقائمة IP بنفسك، ويتم التدوير بشكل شفاف عبر نقطة نهاية واحدة. في هذه الحالة، البروكسيات السكنية مع التدوير مناسبة بشكل خاص لتجاوز حماية مكافحة الروبوتات على المواقع ذات أنظمة الكشف الصارمة، بينما بروكسيات مراكز البيانات هي خيار جيد، إذا كانت سرعة الطلبات مهمة، ولم تقم المواقع المستهدفة بحظر نطاقات مراكز البيانات بشكل عدواني.
| نوع البروكسي | السرعة | خطر الحظر | المهام |
|---|---|---|---|
| مراكز البيانات | عالية | متوسطة إلى عالية | تحليل سريع لواجهات برمجة التطبيقات المفتوحة |
| سكنية | متوسطة | منخفضة | تحليل الأسواق والشبكات الاجتماعية |
| محمول | متوسطة إلى منخفضة | حد أدنى | العمل مع أنظمة مكافحة الروبوتات الصارمة |
الجلسة، مهلات و التحقق من SSL
بالنسبة للسكربتات طويلة الأمد (مثل المحللات الدورية)، من الأفضل استخدام
requests.Session() بدلاً من استدعاءات منفصلة
requests.get(). تعيد الجلسة استخدام اتصالات TCP،
مما يقلل من الحمل على خادم البروكسي ويقلل من عدد ProxyError عند كميات كبيرة من الطلبات.
session = requests.Session()
session.proxies.update({
"http": "http://user:password@proxy_host:port",
"https": "http://user:password@proxy_host:port",
})
for url in urls_list:
try:
resp = session.get(url, timeout=15)
print(resp.status_code, url)
except requests.exceptions.RequestException as e:
print(f"خطأ في {url}: {e}")
من المهم أيضًا ذكر المعامل verify. إذا كنت تعمل
مع بروكسي يقوم بتغيير شهادة SSL بنفسه (بروكسي MITM للتصحيح أو بعض البروكسيات المؤسسية)،
قد تواجه requests مشاكل في عدم تطابق الشهادة. يجب عليك تعطيل التحقق تمامًا
(verify=False) فقط في بيئة اختبارية - في الإنتاج
هذا يخلق خطر هجمات "الرجل في المنتصف":
# فقط للتصحيح المحلي!
response = requests.get(url, proxies=proxies, verify=False, timeout=15)
# الطريقة الصحيحة - تحديد المسار إلى الشهادة الموثوقة للبروكسي
response = requests.get(url, proxies=proxies, verify="/path/to/proxy_cert.pem")
قائمة التحقق لتشخيص الأخطاء
إذا كان السكربت يتعطل بخطأ بروكسي، مر عبر هذه القائمة من الأعلى إلى الأسفل - في 90% من الحالات، ستجد السبب في النقاط الثلاث الأولى.
- تحقق من المخطط في القاموس
proxies- يجب أن يكون دائمًاhttp://، حتى لطلبات HTTPS. - اختبر البروكسي مباشرة عبر
curl -xلاستبعاد المشكلة على جانب الشيفرة. - تأكد من أن اسم المستخدم وكلمة المرور لا تحتويان على رموز خاصة غير مهروبة في URL.
- تحقق مما إذا كان البروكسي مرتبطًا بعنوان IP معين (قائمة بيضاء) بدلاً من التفويض بواسطة اسم المستخدم/كلمة المرور.
- قم بزيادة
timeoutإلى 15-30 ثانية قبل اعتبار البروكسي غير عامل. - إذا ظهرت الخطأ فقط على موقع معين - فالمشكلة في سمعة IP، وليست في الشيفرة.
- أضف منطق إعادة المحاولة مع تأخير أسي بدلاً من المحاولات الفردية.
- عند التحليل الجماعي، استخدم تدوير IP، وليس بروكسي ثابت واحد لجميع الطلبات.
الخاتمة
معظم أخطاء البروكسي في Python requests تتلخص في ثلاث نقاط: المخطط غير الصحيح في عنوان URL للبروكسي
(http بدلاً من https),
عدم وجود أو تنسيق غير صحيح للتفويض، وجودة مجموعة IP نفسها. عادةً ما تشير ProxyError إلى مشكلة
في الاتصال أو حظر IP، بينما تشير SSLError WRONG_VERSION_NUMBER تقريبًا دائمًا إلى مخطط بروكسي غير صحيح، و 407 -
إلى مشكلة في اسم المستخدم/كلمة المرور أو القائمة البيضاء.
إذا كنت تواجه حظرًا متكررًا وأخطاء 407 عند تحليل الأسواق أو الشبكات الاجتماعية، يجب عليك إعادة النظر في مصدر البروكسي نفسه، وليس فقط الشيفرة. للحصول على تحليل مستقر مع نسبة منخفضة من الحظر، نوصي بتجربة البروكسيات السكنية - فهي تستخدم عناوين IP حقيقية وتسبب بشكل أقل بكثير ProxyError على المواقع المحمية. إذا كانت السرعة وحجم الطلبات هي الأولوية، وليس تجاوز أنظمة مكافحة الروبوتات المعقدة، انظر إلى بروكسيات مراكز البيانات - فهي أرخص وأسرع للمهام البسيطة للتحليل.