쿠팡이 봇으로 감지(Akamai 차단 페이지)하면, 같은 IP로 재시도하던 것을 '브라우저 끄고 새 IP로 켜서 재시도'로 전환. 감지 패턴도 축적한다. - DecodoProxy.rotate(): 시간창 무관 즉시 다음 포트(=새 IP). current_port 노출 - coupang/adapter: 0건+차단마커 감지 시 → 감지기록 → proxy.rotate()+강제재기동 → 인라인 재시도(max_block_retries=1). IP당 요청수(ip_request_no) 추적, 재기동 시 리셋 - bot_detection 테이블 + crud: source/query/ip_request_no/proxy_port/elapsed_sec/marker/ headless/html_len 기록 → 'IP당 몇 요청 만에 감지되나' 분석 가능 - worker_main: on_detect=BotDetectionLog.record 주입 - 로그: [coupang][BOT-DETECTED] ip_req#N port=... elapsed=...s marker=... - tests: proxy rotate 즉시회전 + 감지기록 CRUD → 전체 47/47 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
55 lines
2.2 KiB
Python
55 lines
2.2 KiB
Python
"""DECODO(구 Smartproxy) residential 프록시 제공자 — 쿠팡(Akamai) 전용.
|
|
|
|
Decodo residential 은 **포트 기반 sticky** 모델이다:
|
|
gate.decodo.com : 10001..N (각 포트 = 별도 sticky 세션, 대시보드에서 지속시간 지정: 예 10분)
|
|
username/password 는 고정. → **IP 회전 = 포트를 바꾸는 것.**
|
|
|
|
매 요청 IP 변경은 Akamai 가 '쿠키-IP 불일치'로 재챌린지하므로 금물.
|
|
→ 시간창(now // window)으로 포트를 고른다: 창 안에선 같은 포트=같은 IP, 창이 지나면 다음 포트=새 IP.
|
|
자격증명/엔드포인트는 config.local.toml [DecodoConfig] 에서 로드(시크릿).
|
|
"""
|
|
|
|
import time
|
|
|
|
from config.server_configs import decodo_config
|
|
|
|
|
|
class DecodoProxy:
|
|
def __init__(self, cfg=None):
|
|
cfg = cfg if cfg is not None else decodo_config
|
|
self.host = cfg.host
|
|
self.username = cfg.username
|
|
self.password = cfg.password
|
|
self.port_start = cfg.port_start
|
|
self.port_end = cfg.port_end
|
|
self.session_minutes = cfg.session_minutes or 10
|
|
self._rotate_offset = 0 # 봇 감지 등으로 '즉시 회전'이 필요할 때 증가
|
|
|
|
@property
|
|
def enabled(self) -> bool:
|
|
return all([self.host, self.username, self.password, self.port_start, self.port_end])
|
|
|
|
def rotate(self):
|
|
"""시간창과 무관하게 즉시 다음 포트(=새 IP)로 회전. 봇 감지 시 호출."""
|
|
self._rotate_offset += 1
|
|
|
|
def _port(self) -> int:
|
|
"""시간창 + 수동 오프셋 기반 포트 선택. 창 안에선 동일 IP, rotate()나 창 변화 시 다음 IP."""
|
|
n = self.port_end - self.port_start + 1
|
|
bucket = int(time.time() // (self.session_minutes * 60))
|
|
return self.port_start + ((bucket + self._rotate_offset) % n)
|
|
|
|
@property
|
|
def current_port(self):
|
|
return self._port() if self.enabled else None
|
|
|
|
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.username,
|
|
"password": self.password,
|
|
}
|