fix(lps): 대역폭 비용 정확화 — CDP 실제 전송 바이트로 계측
지금까지 last_bytes=page.content()(렌더된 DOM 크기)라 실제 네트워크 전송량이 아니었다 — 리소스 차단 효과가 안 보이고 ESM DOM(~5MB) 과대계상 → DECODO 비용이 부정확했다. - browser_base: CDP 세션(new_cdp_session) 부착, Network.loadingFinished 의 encodedDataLength 누적 → last_bytes=실제 전송 바이트. 컨텍스트당 1회 부착, 검색마다 리셋. 미지원 시 DOM 폴백. - 실측(gmarket): 콜드(차단해제) 3.44MB → 웜(차단활성) 1.07MB(~3x↓) — 동적 차단 효과가 이제 숫자로 보임. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
8a594b667d
commit
0cd817a61d
@ -92,7 +92,7 @@ curl -X POST localhost:9600/v1/lps/search -H 'Content-Type: application/json' \
|
||||
}
|
||||
```
|
||||
- `output.stages` = 각 단계에서 몇 건이 걸러졌는지(디버깅·품질 확인용).
|
||||
- `output.metrics` = 검색 1건의 원가. `ai`(호출·토큰·추정 비용$), `crawl`(fetch 수·처리 HTML 바이트·크롤한 몰), `source_ms`(소스별 소요), `duration_ms`(전체). ※ `html_bytes`는 처리한 응답 본문 기준(대역폭 근사) — 오픈마켓은 리소스 미차단이라 실제 대역폭보다 작다(하한).
|
||||
- `output.metrics` = 검색 1건의 원가. `ai`(호출·토큰·추정 비용$), `crawl`(fetch 수·**실제 전송 바이트**·크롤한 몰), `source_ms`(소스별 소요), `duration_ms`(전체). 바이트는 CDP `Network.loadingFinished`의 encodedDataLength(실제 프록시 전송량)로 측정. `proxy_bytes`(네이버 직접 제외)로 DECODO 비용 산정.
|
||||
- 없는 job_id/잘못된 형식 → `result.desc = "LPS_JOB_NOT_FOUND"`.
|
||||
|
||||
> **⚠️ 가격의 의미 (배송비)**
|
||||
|
||||
@ -8,8 +8,9 @@
|
||||
핸들러가 각 호출을 타이밍하고, AI 클라이언트/어댑터가 노출하는 last_usage/last_bytes 를 읽어 누적한다.
|
||||
결과는 job.result.metrics 로 적재돼 API/FE 에서 검색 원가를 확인할 수 있다.
|
||||
|
||||
⚠️ HTML 바이트는 '처리한 응답 본문' 기준(대역폭 근사). 쿠팡은 리소스 차단이라 실제와 근접하지만,
|
||||
오픈마켓(리소스 미차단)은 CSS/JS/이미지가 빠져 실제 대역폭보다 작다(하한). 정확 대역폭은 백로그(CDP).
|
||||
바이트는 어댑터가 **CDP Network.loadingFinished 의 실제 전송 바이트(encodedDataLength)** 로 측정한다
|
||||
(DOM 크기가 아님 → 리소스 차단 효과가 정확히 반영됨). DECODO 는 프록시 경유 바이트로만 과금하므로
|
||||
proxy_bytes(=네이버 직접 제외) × cost_per_gb 로 대역폭 비용을 산정한다.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
@ -86,7 +86,9 @@ class BrowserSearchAdapter(SearchAdapter):
|
||||
self._ok = 0
|
||||
self._blocked = 0
|
||||
self._last_used = 0.0 # 마지막 검색 시각(monotonic) — 유휴 브라우저 정리 판단용
|
||||
self.last_bytes = 0 # 직전 search 의 처리 HTML 바이트(계측용) — 호출부가 await 직후 읽는다
|
||||
self._cdp = None # CDP 세션(실제 네트워크 바이트 계측용). 미지원 시 None → DOM 크기 폴백
|
||||
self._net_bytes = 0 # 현재 검색의 실제 전송 바이트(encodedDataLength 누적)
|
||||
self.last_bytes = 0 # 직전 search 의 전송 바이트(계측용) — 호출부가 await 직후 읽는다
|
||||
|
||||
# ---- 사이트별 훅 --------------------------------------------------
|
||||
@abstractmethod
|
||||
@ -152,12 +154,31 @@ class BrowserSearchAdapter(SearchAdapter):
|
||||
self._force_recycle = False
|
||||
|
||||
async def _close_ctx(self):
|
||||
self._cdp = None # 컨텍스트와 함께 CDP 세션도 죽음 → 다음 검색 때 재부착
|
||||
if self._ctx is not None:
|
||||
try:
|
||||
await self._ctx.close()
|
||||
finally:
|
||||
self._ctx = None
|
||||
|
||||
def _add_net(self, event):
|
||||
"""CDP Network.loadingFinished 콜백 — 실제 전송 바이트(encodedDataLength) 누적."""
|
||||
try:
|
||||
self._net_bytes += int(event.get("encodedDataLength", 0) or 0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def _ensure_net_meter(self, page):
|
||||
"""CDP 네트워크 계측 세션 부착(컨텍스트당 1회). 실패(미지원)하면 DOM 크기로 폴백."""
|
||||
if self._cdp is not None:
|
||||
return
|
||||
try:
|
||||
self._cdp = await self._ctx.new_cdp_session(page)
|
||||
await self._cdp.send("Network.enable")
|
||||
self._cdp.on("Network.loadingFinished", self._add_net)
|
||||
except Exception:
|
||||
self._cdp = None
|
||||
|
||||
@property
|
||||
def uses_proxy(self) -> bool:
|
||||
"""이 어댑터가 프록시(DECODO)를 경유하는지 — 대역폭 비용 귀속용."""
|
||||
@ -182,6 +203,8 @@ class BrowserSearchAdapter(SearchAdapter):
|
||||
page = self._ctx.pages[0] if self._ctx.pages else await self._ctx.new_page()
|
||||
url = self._search_url(query, limit)
|
||||
self._block_active = await self._blocking_now() # 챌린지 solving 중이면 차단 해제(Turnstile 보호)
|
||||
await self._ensure_net_meter(page)
|
||||
self._net_bytes = 0 # 이 검색의 전송 바이트만 집계
|
||||
try:
|
||||
await page.goto(url, wait_until="domcontentloaded", timeout=40000)
|
||||
await self._wait_ready(page)
|
||||
@ -194,7 +217,8 @@ class BrowserSearchAdapter(SearchAdapter):
|
||||
continue
|
||||
raise AdapterError(f"{self.source} 검색 실패: {ex}", source=self.source) from ex
|
||||
|
||||
self.last_bytes = len(html.encode("utf-8"))
|
||||
# 실제 프록시 전송 바이트(CDP encodedDataLength) — 미지원 시 DOM 크기 폴백
|
||||
self.last_bytes = self._net_bytes if self._cdp is not None else len(html.encode("utf-8"))
|
||||
products = self._parse(html)
|
||||
if products:
|
||||
self._ok += 1
|
||||
|
||||
Loading…
Reference in New Issue
Block a user