웹사이트를 파싱하거나 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://로 지정되며,
대부분의 프록시 서버는 HTTP 프로토콜을 통해 CONNECT 터널을 수용하고 SSL로 감싸지 않습니다.
프록시가 이를 지원하지 않는 경우 https://를 지정하면
SSLError가 발생하며, 이에 대한 분석은 아래에 있습니다.
SOCKS5 프록시를 사용하는 경우 추가 패키지가 필요합니다:
pip install requests[socks]
# 그리고 proxies 구문이 변경됩니다:
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는 클라이언트가 서버가 일반 HTTP 트래픽(암호화되지 않음)을 기대하는 곳에서 SSL/TLS 핸드셰이크를 시도했음을 의미합니다.
대부분의 경우 원인은 proxies 딕셔너리에 잘못된 프록시 스킴이 지정되어 있기 때문입니다.
자주 발생하는 오류: 프록시 서버 주소에 https:// 대신
http://를 지정하는 것입니다. 대부분의 HTTP(S) 프록시는 HTTPS 트래픽을 프록시할 때도 일반 HTTP를 통해 연결을 수용합니다.
# 잘못된 예 — 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를 지원하지 않는 포트입니다. 예를 들어, 프록시 제공자가 HTTP 트래픽 전용으로 포트 8080을 할당했지만 TLS 연결을 직접 설정하려고 하면 서버는 TLS 핸드셰이크 대신 일반 텍스트로 응답하며, 클라이언트 라이브러리 OpenSSL은 이 오류를 발생시킵니다. 프록시 제공자의 문서에서 각 포트에서 사용할 수 있는 포트 및 프로토콜(HTTP/HTTPS/SOCKS5)을 확인하세요.
세 번째 경우는 socket 또는 타사 라이브러리를 사용하여 프록시와의 직접 TCP 연결을 설정하는 경우입니다.
이 경우 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 화이트리스트 대신 로그인/비밀번호 인증으로 전환하는 것입니다.
재시도 로직 및 예외 처리
실제로 프록시 오류는 드물게 단발성입니다 — 특히 대량 파싱 시. 올바른 코드 아키텍처는 지연 및 각 오류를 별도로 로깅하여 재시도를 고려해야 합니다.
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: 로그인/비밀번호 또는 프록시의 화이트리스트를 확인하세요.")
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 목록을 직접 저장할 필요가 없으며, 회전은 하나의 엔드포인트를 통해 투명하게 발생합니다. 이 경우 주거용 프록시가 강력한 탐지 시스템이 있는 사이트에서 안티봇 보호를 우회하는 데 특히 적합하며, 데이터 센터 프록시는 요청 속도가 중요하고 대상 사이트가 데이터 센터 범위를 공격적으로 차단하지 않는 경우 좋은 선택입니다.
| 프록시 유형 | 속도 | 차단 위험 | 작업 |
|---|---|---|---|
| 데이터 센터 프록시 | 높음 | 중간-높음 | 오픈 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%의 경우 원인은 처음 세 가지 항목에서 찾을 수 있습니다.
- 딕셔너리
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를 발생시킬 가능성이 훨씬 낮습니다. 반면, 속도와 요청량이 우선이라면 데이터 센터 프록시를 고려해 보세요 — 이들은 간단한 파싱 작업에 대해 더 저렴하고 빠릅니다.