← بازگشت به وبلاگ

پایتون درخواست‌ها: خطای پروکسی، خطای SSL شماره نسخه نادرست و ۴۰۷ — چگونه در عمل اصلاح کنیم

بررسی دقیق اشتباهات رایج در کار با پروکسی در Python requests: ProxyError، SSLError WRONG_VERSION_NUMBER، 407 نیاز به احراز هویت پروکسی. مثال‌های کاربردی کد برای هر وضعیت.

📅۲۰ آبان ۱۴۰۵

اگر شما سایت‌ها را پارس می‌کنید یا درخواست‌ها را از طریق پروکسی در 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 اختصاص داده باشد و شما سعی کنید به طور مستقیم (بدون CONNECT) یک اتصال TLS به آن برقرار کنید، سرور به جای handshake 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 خاصی (whitelist) مرتبط می‌کنند، نه به نام کاربری/رمز عبور. اگر شما IP خود را تغییر دهید (مثلاً از طریق VPN یا IP دینامیک ارائه‌دهنده)، خطای 407 ممکن است حتی با اعتبارنامه‌های صحیح ظاهر شود — زیرا IP خارجی فعلی شما در لیست سفید در سمت پروکسی قرار ندارد. راه‌حل — وارد حساب کاربری ارائه‌دهنده شوید و لیست IP‌های مجاز را به‌روزرسانی کنید، یا به احراز هویت بر اساس نام کاربری/رمز عبور به جای IP-whitelist تغییر دهید.

منطق تکرار و مدیریت استثناها

در عمل، خطاهای پروکسی به ندرت یک‌باره هستند — به ویژه هنگام پارس انبوه. معماری صحیح کد باید تلاش‌های تکراری با تأخیر و ثبت هر خطا به صورت جداگانه را پیش‌بینی کند.

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: نام کاربری/رمز عبور یا whitelist 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-pool هستند. برای چنین وظایفی مانند پارس بازارها (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 به صورت دستی نیست و چرخش به صورت شفاف از طریق یک endpoint انجام می‌شود. در این صورت، پروکسی‌های مسکونی با چرخش به ویژه برای دور زدن محافظت‌های ضد ربات در سایت‌هایی با سیستم‌های تشخیص سخت، مناسب هستند و پروکسی‌های مراکز داده انتخاب خوبی هستند اگر سرعت درخواست‌ها مهم باشد و سایت هدف به طور تهاجمی دامنه‌های مراکز داده را مسدود نکند.

نوع پروکسی سرعت ریسک مسدودیت وظایف
مراکز داده بالا متوسط-بالا پارس سریع API‌های باز
مسکونی متوسط پایین پارس بازارها، شبکه‌های اجتماعی
موبایل متوسط-پایین حداقل کار با سیستم‌های ضد ربات سخت

جلسه، زمان‌های تایم‌اوت و تأیید 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% موارد دلیل در سه مورد اول پیدا می‌شود.

  1. پروتکل را در دیکشنری proxies بررسی کنید — تقریباً همیشه باید http:// باشد، حتی برای درخواست‌های HTTPS.
  2. پروکسی را به طور مستقیم از طریق curl -x آزمایش کنید تا مشکل در سمت کد را رد کنید.
  3. اطمینان حاصل کنید که نام کاربری و رمز عبور شامل کاراکترهای خاص غیر فرار در URL نیستند.
  4. بررسی کنید که آیا پروکسی به IP خاصی (whitelist) مرتبط است یا به احراز هویت بر اساس نام کاربری/رمز عبور.
  5. زمان timeout را به 15-30 ثانیه افزایش دهید قبل از اینکه پروکسی را غیرعملکردی در نظر بگیرید.
  6. اگر خطا فقط در یک سایت خاص ظاهر می‌شود — مشکل در شهرت IP است، نه در کد.
  7. منطق تکرار را با تأخیر نمایی اضافه کنید به جای تلاش‌های یکباره.
  8. در پارس انبوه از چرخش IP استفاده کنید، نه یک پروکسی ثابت برای همه درخواست‌ها.

نتیجه‌گیری

اکثر خطاهای پروکسی در Python requests به سه چیز مربوط می‌شود: پروتکل نادرست در URL پروکسی (http به جای https), عدم وجود یا فرمت نادرست احراز هویت، و کیفیت خود IP-pool. ProxyError معمولاً نشان‌دهنده مشکل اتصال یا مسدودیت IP است، SSLError WRONG_VERSION_NUMBER تقریباً همیشه به پروتکل نادرست پروکسی اشاره دارد، و 407 — به مشکل در نام کاربری/رمز عبور یا whitelist اشاره دارد.

اگر شما به طور مرتب با مسدودیت‌ها و خطاهای 407 هنگام پارس بازارها یا شبکه‌های اجتماعی مواجه می‌شوید، باید منبع پروکسی را دوباره بررسی کنید، نه فقط کد. برای پارس پایدار با درصد پایین مسدودیت‌ها توصیه می‌کنیم پروکسی‌های مسکونی را امتحان کنید — آن‌ها از IP‌های واقعی استفاده می‌کنند و به طور قابل توجهی کمتر باعث ایجاد ProxyError در سایت‌های محافظت‌شده می‌شوند. اگر اولویت شما سرعت و حجم درخواست‌ها باشد، نه دور زدن سیستم‌های ضد ربات پیچیده، به پروکسی‌های مراکز داده نگاه کنید — آن‌ها ارزان‌تر و سریع‌تر برای وظایف ساده پارس هستند.