o2o-negosium-original/lps/services/search/proxy.py
민헌 4de85d43ed feat(lps): DECODO residential 프록시 — 쿠팡 sticky 세션 + 주기적 IP 회전
datacenter IP + Akamai 대응. 매 요청 IP 변경은 쿠키-IP 불일치로 재챌린지를
유발하므로, sticky 세션(일정 시간 같은 IP) + 주기적 회전 방식 사용.

- search/proxy.DecodoProxy: username 에 -session-<시간창id>-sessionduration-<분> 부착
  → 창 안에선 같은 IP, 창이 지나면 새 IP. 자격증명은 .env(DECODO_*), 4개 다 있어야 활성
- coupang/adapter: proxy 주입 + 세션창 경과 시 브라우저 재기동(IP 회전). 무프록시면 직접 연결
- worker_main: 쿠팡에만 DecodoProxy 경유(네이버는 공식 API라 미적용)
- .env.example: DECODO_HOST/PORT/USERNAME/PASSWORD/SESSION_MINUTES 슬롯
- tests: 활성조건·sticky username·server형식·세션창 회전 4건 → 전체 43/43

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 09:40:26 +09:00

48 lines
2.0 KiB
Python

"""DECODO(구 Smartproxy) residential 프록시 제공자.
쿠팡(Akamai) 전용. 핵심: **sticky 세션 + 주기적 회전**.
매 요청 IP 변경은 Akamai 가 '쿠키-IP 불일치'로 재챌린지/차단하므로 금물.
→ 일정 시간(session_minutes) 같은 IP 유지 후 회전한다.
DECODO 는 username 에 세션 파라미터를 붙여 sticky 를 구현한다:
<username>-session-<id>-sessionduration-<minutes>
세션 id 를 '시간창(now // window)'으로 만들면 창 안에선 같은 IP, 창이 바뀌면 새 IP 가 된다.
자격증명은 .env(DECODO_*) 에서 로드(시크릿).
"""
import os
import time
class DecodoProxy:
def __init__(self, host=None, port=None, username=None, password=None, session_minutes=None, sticky=True):
self.host = host or os.environ.get("DECODO_HOST")
self.port = port or os.environ.get("DECODO_PORT")
self.username = username or os.environ.get("DECODO_USERNAME")
self.password = password or os.environ.get("DECODO_PASSWORD")
self.session_minutes = int(session_minutes or os.environ.get("DECODO_SESSION_MINUTES") or 10)
self.sticky = sticky
@property
def enabled(self) -> bool:
return all([self.host, self.port, self.username, self.password])
def _session_id(self) -> int:
"""시간창 기반 세션 id — 같은 창에선 동일, 창이 지나면 증가(→새 IP)."""
return int(time.time() // (self.session_minutes * 60))
def _session_username(self) -> str:
if not self.sticky:
return self.username
return f"{self.username}-session-{self._session_id()}-sessionduration-{self.session_minutes}"
def playwright_proxy(self) -> dict | None:
"""Playwright launch(proxy=...) 용 설정. 비활성 시 None(프록시 미사용)."""
if not self.enabled:
return None
return {
"server": f"http://{self.host}:{self.port}",
"username": self._session_username(),
"password": self.password,
}