docs(lps): 문서 최신화 — 오픈마켓 폴백·Turnstile·비용계측·동시성·프록시복원력
이번 라운드 변경을 README/아키텍처/운영 가이드에 반영. - README: 기능표(오픈마켓 폴백·원가계측·다중병렬), 워크플로우 4-1 폴백 단계, 폴더구조 - architecture: 안티봇 대응(쿠팡 Akamai / ESM Cloudflare Turnstile 웜업·동적차단), 검색 원가 계측(CDP 실측·컴포넌트별 비용), 다중 상품 병렬, 유휴 브라우저 정리 - operations: 로그 항목(프리플라이트·웜업·IP회전·데드라인·유휴정리), 문제해결(407·Turnstile· Chrome 정리·느림/비쌈), 배포유의, 원가/몰별 DB 쿼리 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
77a36c43d4
commit
10eb67e8c6
@ -25,17 +25,22 @@
|
||||
가격 이상치 제거 → "같은 상품"만 선별
|
||||
│
|
||||
▼
|
||||
[5] 최저가 확정 + 기록
|
||||
네이버/쿠팡/최종 최저가 저장 → 그래프용 이력 적재
|
||||
[4-1] 오픈마켓 폴백
|
||||
네이버가 못 덮은 몰(G마켓·옥션·11번가)만 크롤
|
||||
│
|
||||
▼
|
||||
[6] 완료(결과 조회 가능)
|
||||
[5] 최저가 확정 + 기록
|
||||
네이버/쿠팡/최종 + 몰별 최저가 저장 → 이력 적재
|
||||
│
|
||||
▼
|
||||
[6] 완료(결과 + 검색 원가 조회 가능)
|
||||
```
|
||||
|
||||
**핵심 포인트**
|
||||
- **즉시 응답 + 나중 처리**: 요청하면 바로 "접수번호(job_id)"를 주고, 실제 검색은 뒤에서 진행됩니다. (검색은 몇 초~수십 초 걸림)
|
||||
- **못 찾으면 검색어를 바꿔 재시도**: "맥심 커피"로 안 나오면 "맥심 모카골드 커피믹스"처럼 **AI가 검색어를 다듬어** 다시 시도하고, 그래도 없으면 "없음"으로 정리합니다. (무한 재시도 안 함)
|
||||
- **차단 대응**: 쿠팡이 봇으로 감지하면 **다른 IP로 바꿔** 재시도하고, 감지 이력도 기록합니다.
|
||||
- **차단 대응**: 쿠팡(Akamai)·G마켓(Cloudflare 사람확인) 등이 봇으로 감지하면 **다른 IP로 바꿔** 재시도하고, 시작 시 챌린지를 미리 풀어(웜업) 실 작업을 빠르게 합니다.
|
||||
- **원가 투명**: 검색 1건이 쓴 AI 비용·프록시 대역폭·시간을 함께 기록합니다.
|
||||
|
||||
---
|
||||
|
||||
@ -43,12 +48,15 @@
|
||||
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| 멀티 소스 검색 | 네이버 쇼핑 API + 쿠팡(봇 차단 우회) 동시 검색·병합 |
|
||||
| 멀티 소스 검색 | 네이버 쇼핑 API + 쿠팡(Akamai 우회) 동시 검색·병합 |
|
||||
| 오픈마켓 폴백 크롤 | 네이버가 못 덮은 몰만 G마켓·옥션(Cloudflare Turnstile 우회)·11번가 크롤 → 몰별 가격 |
|
||||
| AI 같은 상품 판정 | "진짜 그 상품"만 선별 (액세서리·다른 규격 제외) |
|
||||
| 검색어 자동 정제 | 0건이면 정밀/광역 검색어로 재시도 |
|
||||
| 최저가 이력 그래프 | 조회 시점마다 네이버/쿠팡/최종 최저가를 시계열로 기록 |
|
||||
| 최저가 이력 그래프 | 조회 시점마다 네이버/쿠팡/최종 + 몰별(by_mall) 최저가를 시계열로 기록 |
|
||||
| 검색 원가 계측 | 검색 1건의 AI 토큰·비용 + DECODO 대역폭(실측 CDP) + 시간을 집계 |
|
||||
| 다중 상품 병렬 | 워커별 브라우저 세트로 여러 상품 동시 검색(`WORKER_CONCURRENCY`) |
|
||||
| 안정적 큐 처리 | 작업 유실 없이 순서대로, 실패 시 자동 재시도 |
|
||||
| 프록시 IP 회전 | 차단 회피용 IP 자동 순환(DECODO) |
|
||||
| 프록시 IP 회전 | 봇 감지·전송오류 시 IP 자동 순환 + 시작 웜업(DECODO) |
|
||||
|
||||
---
|
||||
|
||||
@ -98,12 +106,17 @@ lps/
|
||||
├── config/ # 설정(config.local.toml — 포트/DB/API키, 미커밋)
|
||||
├── common/ # 공통(enums, DB 세션, 모델, 로거)
|
||||
│ └── database/model/models.py # DB 테이블 정의
|
||||
├── loadtest.py # 부하 테스트 (N개 상품 → 처리량·지연·비용 집계)
|
||||
├── crud/ # DB 접근 (job_crud, price_history, negative_cache, bot_detection)
|
||||
├── services/
|
||||
│ ├── search/ # 소스 어댑터 (coupang, naver, esm=G마켓·옥션, st11=11번가) + 프록시·필터
|
||||
│ ├── pipeline/ # 필터·이상치·최저가 정렬
|
||||
│ └── ai/ # AI 유사도 판정·검색어 생성 (OpenAI)
|
||||
├── worker/ # 워커 루프·핸들러·알림(NOTIFY)
|
||||
│ ├── search/ # 소스 어댑터 (coupang, naver, esm=G마켓·옥션, st11=11번가)
|
||||
│ │ ├── browser_base.py # patchright 공통(수명·프록시회전·차단감지·CDP 바이트계측)
|
||||
│ │ ├── proxy.py # DECODO(IP 회전·프리플라이트)
|
||||
│ │ └── card_parser.py # 오픈마켓 공용 카드 파서
|
||||
│ ├── pipeline/ # 필터·이상치·최저가 정렬(+몰별 분해)
|
||||
│ ├── ai/ # AI 유사도 판정·검색어 생성 (OpenAI)
|
||||
│ └── metrics.py # 검색 원가 계측(AI/DECODO 비용·시간)
|
||||
├── worker/ # 워커 루프·핸들러(폴백·데드라인)·알림(NOTIFY)
|
||||
├── router/v1/lps/ # API 라우터
|
||||
└── tests/ # 테스트
|
||||
```
|
||||
|
||||
@ -63,23 +63,51 @@
|
||||
|
||||
**무한 재시도 방지**: 기술적 재시도(횟수 상한)·검색어 재시도(라운드 상한) 둘 다 유한합니다. "못 찾음"은 **실패가 아니라 정상적인 답**으로 처리해 쌓이지 않습니다.
|
||||
|
||||
## 4. 쿠팡 봇 차단 대응
|
||||
## 4. 안티봇 대응 (소스별로 다름)
|
||||
|
||||
- 쿠팡은 **Akamai 봇 차단**(JS 챌린지)이 있어, 일반 HTTP로는 못 뚫습니다 → **실제 Chrome 브라우저**(Patchright)로 통과합니다.
|
||||
- **프록시 IP 회전(DECODO)**: 같은 IP로 계속 두드리면 차단되므로, 일정 시간마다 다른 IP로 바꿉니다. (매 요청마다 바꾸면 오히려 의심받아 **일정 시간 유지 후 회전**)
|
||||
- **감지 시 즉시 IP 전환**: 차단이 감지되면 새 IP로 바꿔 재시도하고, **감지 이력**(몇 번째 요청에서 걸렸는지)을 기록해 패턴을 분석합니다.
|
||||
- **대역폭 절약**: 이미지·폰트 등 불필요한 리소스는 받지 않아 프록시 비용을 줄입니다.
|
||||
브라우저 소스는 `BrowserSearchAdapter`(services/search/browser_base.py) 위에서 patchright(스텔스 Chrome)로 뚫고, 사이트별 차이는 훅으로 분리합니다.
|
||||
|
||||
## 5. 최저가 이력 (그래프)
|
||||
| 소스 | 안티봇 | 대응 |
|
||||
|------|--------|------|
|
||||
| **쿠팡** | Akamai(JS 챌린지, 여러 flavor: 챌린지·Edge Access Denied·권한제한) | 실제 Chrome 통과 + 다종 마커 감지→IP 회전. 리소스 차단 OK(대역폭↓) |
|
||||
| **G마켓·옥션**(ESM) | **Cloudflare Turnstile**('사람인지 확인' 체크박스) | patchright가 콜드 ~12초에 **자동 통과**, `cf_clearance` 쿠키로 이후 요청은 웜(~5초). 인터랙티브 체크박스는 best-effort 클릭 |
|
||||
| **11번가** | 경량(모바일은 robot 차단→PC 사용) | PC 크롤. 지연 로딩 → 스크롤 트리거 |
|
||||
| **네이버** | 없음(공식 오픈API) | httpx 직접 호출 + 키 로테이션 |
|
||||
|
||||
- **트리거 기반**: 자동 배치로 전 상품을 주기 조회하지 않고, **실제 조회된 상품만** 그 시점에 기록 → 트래픽·비용 절약.
|
||||
- 검색할 때마다 **네이버 최저가 / 쿠팡 최저가 / 최종 최저가**를 스냅샷으로 남깁니다.
|
||||
- 그래프: X축 = 조회 시각(불규칙), Y축 = 가격, 3개 선. (한쪽 소스에 그 상품이 없던 시점은 선이 비어있음 — 정상)
|
||||
**핵심 메커니즘**
|
||||
- **IP 회전(DECODO)**: 같은 IP로 계속 두드리면 차단 → 시간창 기반 sticky + 봇감지/전송오류 시 즉시 회전. 감지 이력(`bot_detection`)을 기록해 패턴 분석. **프록시 전송오류(407/터널)** 도 사이트 차단과 구분해 회전.
|
||||
- **시작 프리플라이트 + 웜업**: 기동 시 살아있는 프록시 포트를 선점(egress IP 로그)하고, 챌린지 소스를 미리 1회 풀어 **쿠키를 선점**(나쁜 IP는 회전 재시도) → 실 작업은 웜(빠름).
|
||||
- **동적 리소스 차단**: 이미지·폰트 등을 차단해 대역폭↓. 단 **Turnstile은 리소스 차단을 봇 신호로 감지**하므로, ESM은 챌린지 solving 중(콜드)엔 차단을 풀고 **cf_clearance 확보 후(웜)에만 차단**합니다.
|
||||
- **폴백 데드라인**: 오픈마켓 크롤은 '보강'이라 각 크롤에 시간 상한(기본 15초)을 둬, 한 몰이 안 풀려도 전체 지연이 늘지 않게 합니다.
|
||||
- **유휴 브라우저 정리**: 일정 시간(기본 120초) 검색이 없는 소스의 Chrome을 닫아 메모리를 회수(쿠키는 프로필에 남아 재기동해도 웜 유지).
|
||||
|
||||
## 6. 설계 원칙 (참고)
|
||||
## 5. 검색 원가 계측 (리소스·비용·시간)
|
||||
|
||||
- **PostgreSQL을 큐로 제대로 사용**: 별도 메시지 브로커(Redis 등) 없이, 원자적 작업 할당 + 자동 복구로 유실·중복 없이 처리.
|
||||
- **소스 어댑터 패턴**: 네이버·쿠팡의 수집 방식 차이를 어댑터 안에 가두고, 코어는 정규화된 결과만 다룸 → 새 쇼핑몰 추가가 쉬움.
|
||||
- **AI로 매칭**: 상품명 형식이 제각각이라 규칙으로 고정 파싱하지 않고, AI가 "같은 상품인지" 판단.
|
||||
검색 1건이 쓰는 것을 잡 단위로 집계해 `result.metrics`에 남깁니다(API/FE 노출).
|
||||
- **AI**: 호출 수 + 토큰(prompt/completion) + 추정 비용($, 모델 단가)
|
||||
- **크롤 대역폭**: CDP `Network.loadingFinished`의 **실제 전송 바이트**(DOM 크기가 아님). 프록시 경유분(네이버 직접 제외)으로 **DECODO 비용**($/GB) 산정
|
||||
- **컴포넌트별 비용**: `cost = { ai_usd, proxy_usd, total_usd }`
|
||||
- **시간**: 소스별 소요 + 전체
|
||||
|
||||
> 실측(2026-07): 상품당 ~$0.013 (AI ~$0.002 + **DECODO ~$0.011 = 87%**). 대역폭이 원가의 대부분 — 오픈마켓 크롤(브라우저 필수)이 주범.
|
||||
|
||||
## 6. 다중 상품 병렬 처리
|
||||
|
||||
- `POST /search`에 상품 리스트를 주면 상품마다 잡을 큐에 적재.
|
||||
- 워커 동시성(`WORKER_CONCURRENCY=N`)만큼 상품을 **진짜 병렬** 처리 — 워커마다 **자기 브라우저 세트**(프로필 분리 + 다른 프록시 IP)를 가져 공유 lock 병목을 없앰. 권장 N=2~3(로컬, Chrome 최대 4×N개).
|
||||
- 부하 측정: `loadtest.py`(처리량·p50/p95 지연·AI/DECODO/총비용 집계).
|
||||
|
||||
## 7. 최저가 이력 (그래프)
|
||||
|
||||
- **트리거 기반**: 자동 배치 없이 **실제 조회된 상품만** 그 시점에 기록 → 트래픽·비용 절약.
|
||||
- 검색할 때마다 **네이버/쿠팡/최종 최저가** + **몰별 스냅샷(`by_mall`)** 을 남깁니다.
|
||||
- 그래프: X축 = 조회 시각(불규칙), Y축 = 가격, 3개 선(+몰별). (한쪽 소스에 없던 시점은 선이 비어있음 — 정상)
|
||||
|
||||
## 8. 설계 원칙 (참고)
|
||||
|
||||
- **PostgreSQL을 큐로 제대로 사용**: 별도 브로커 없이 원자적 할당 + 자동 복구로 유실·중복 없이 처리.
|
||||
- **소스 어댑터 패턴**: 소스별 수집·안티봇 차이를 어댑터 안에 가두고, 코어는 정규화된 결과만 다룸 → 새 쇼핑몰 추가가 쉬움.
|
||||
- **AI로 매칭**: 상품명 형식이 제각각이라 규칙 고정 파싱 대신 AI가 "같은 상품인지" 판단.
|
||||
- **오픈마켓은 네이버 폴백**: 네이버가 이미 커버하는 몰은 재크롤하지 않고, 못 덮은 몰만 크롤(비용 절약).
|
||||
|
||||
더 깊은 내부 구현은 각 파일 상단 주석에 정리되어 있습니다.
|
||||
|
||||
@ -54,13 +54,17 @@ N=8 python loadtest.py # 상품 8개 제출→처리량·지연(p50/p95)·AI/
|
||||
|
||||
| 로그 | 의미 |
|
||||
|------|------|
|
||||
| `DECODO 프리플라이트 OK — egress IP ...` | 시작 시 살아있는 프록시 포트 선점 성공(egress IP 표시) |
|
||||
| `[warmup:gmarket] 챌린지 통과·쿠키 확보` | 시작 웜업 — 챌린지 미리 풀어 쿠키 선점(실 작업 웜) |
|
||||
| `[naver] query='...' → N건` | 네이버 검색 결과 수 |
|
||||
| `[coupang] query='...' → N건 (ip_req#K)` | 쿠팡 결과 수 / 이 IP로 K번째 요청 |
|
||||
| `[gmarket/auction/st11] query='...' → N건` | 오픈마켓 폴백 크롤 결과 수 |
|
||||
| `[ai] 판정 N건 중 매칭 M건` | AI 같은상품 선별 결과 |
|
||||
| `[ai] 검색어 생성 precise=... broad=...` | 0건이라 검색어 재생성 |
|
||||
| `[coupang][BOT-DETECTED] ...` | **쿠팡 봇 감지** → IP 회전 |
|
||||
| `[coupang][BOT-DETECTED] ... marker='...'` | 봇 감지(마커별) → IP 회전 |
|
||||
| `[gmarket] IP 회전 — 프록시 전송오류/봇 감지` | 프록시 죽음(407/터널) 또는 차단 → 새 IP |
|
||||
| `[fallback:gmarket] 데드라인 15s 초과 → 스킵` | 폴백 크롤이 시간 상한 초과 → 그 몰만 스킵 |
|
||||
| `[coupang] 유휴 120s 초과 → 브라우저 정리` | 유휴 브라우저 닫아 메모리 회수(다음 검색 때 재기동) |
|
||||
| `[worker-0] done <id>` / `fail ... → DEAD` | 작업 완료 / 실패 |
|
||||
| `[reaper] reclaimed N` | 죽은 워커 작업 회수 |
|
||||
|
||||
> 디버그 로그가 안 보이면 `config.local.toml`의 `[LogConfig] log_level = "debug"` 확인.
|
||||
|
||||
@ -78,10 +82,18 @@ SELECT job_id, status, result->>'outcome' AS outcome,
|
||||
result->'lowest'->>'price' AS lowest, result->'sources' AS sources
|
||||
FROM job ORDER BY created_at DESC LIMIT 5;
|
||||
|
||||
-- 특정 상품의 최저가 이력(그래프 원본)
|
||||
SELECT triggered_at, naver_lowest, coupang_lowest, final_lowest, final_source, outcome
|
||||
-- 특정 상품의 최저가 이력(그래프 원본) + 몰별 스냅샷
|
||||
SELECT triggered_at, naver_lowest, coupang_lowest, final_lowest, final_source, by_mall
|
||||
FROM price_history WHERE product_code='T1' ORDER BY triggered_at;
|
||||
|
||||
-- 검색 원가(최근 완료 작업의 metrics)
|
||||
SELECT job_id,
|
||||
result->'metrics'->'cost'->>'total_usd' AS 총비용,
|
||||
result->'metrics'->'cost'->>'proxy_usd' AS DECODO,
|
||||
result->'metrics'->'crawl'->>'proxy_bytes' AS 전송바이트,
|
||||
result->'metrics'->>'duration_ms' AS 소요ms
|
||||
FROM job WHERE status=3 ORDER BY updated_at DESC LIMIT 5;
|
||||
|
||||
-- 봇 감지 패턴 (IP당 평균 몇 요청 만에 감지?)
|
||||
SELECT avg(ip_request_no), count(*) FROM bot_detection;
|
||||
|
||||
@ -105,14 +117,18 @@ python -m pytest
|
||||
|------|------------|
|
||||
| 포트 9600 사용 중 | `lsof -ti:9600 \| xargs kill` 후 재실행 |
|
||||
| 백그라운드 실행 시 로그 안 보임 | `print` 버퍼링 → `PYTHONUNBUFFERED=1` 붙여 실행 |
|
||||
| `ProcessSingleton ... profile is already in use` | Chrome 프로필 중복 — 워커를 **하나만** 실행(또는 워커별 프로필 분리 필요) |
|
||||
| 쿠팡 결과 0건 + `blocked=True` | 봇 차단 → 프록시(DECODO) 설정 확인. 감지 이력은 `bot_detection` 참고 |
|
||||
| AI 매칭이 0건 자주 발생 | 검색어가 모호하거나 스펙이 실제와 다름 → `product_name`/`specification`을 더 정확히 |
|
||||
| `프리플라이트 실패`/모든 크롤 실패 | DECODO 프록시 문제 — **대시보드에서 잔여 트래픽·플랜·자격증명** 확인(407=인증거부). 게이트 다운이면 네이버(직접)만 동작 |
|
||||
| G마켓 결과 계속 0건 | Cloudflare Turnstile 미통과(나쁜 IP는 인터랙티브 체크박스) — 웜업 IP회전 재시도로 완화. 지연 부담이면 폴백 데드라인이 스킵 |
|
||||
| 쿠팡 `blocked=True`(Access Denied 등) | Akamai 차단 → 자동 IP 회전(감지 이력 `bot_detection`). 반복되면 프록시 IP 풀 확대 |
|
||||
| Chrome이 계속 쌓임 | 유휴 정리(120s)가 닫음. 스파이크/이전 워커 잔여는 `pkill -f "user-data-dir=/tmp/lps_"` |
|
||||
| AI 매칭이 0건 자주 발생 | 검색어 모호/스펙 불일치 → `product_name`/`specification`을 더 정확히 |
|
||||
| 검색이 너무 느림/비쌈 | `result.metrics`로 소스별 시간·DECODO 바이트 확인. 대역폭이 대부분(오픈마켓 크롤) |
|
||||
| `result.desc = LPS_JOB_NOT_FOUND` | 존재하지 않거나 잘못된 job_id |
|
||||
|
||||
## 7. 배포 시 유의 (예정)
|
||||
- Chrome을 **headless**로(서버엔 화면 없음) — 튜닝 필요
|
||||
- Chrome을 **headless**로(서버엔 화면 없음) — 단 Cloudflare Turnstile은 headful이 더 잘 통과, 튜닝 필요
|
||||
- Docker 이미지에 **chromium 설치** 필요
|
||||
- **프록시(residential)** 사실상 필수 — 클라우드 IP는 쉽게 차단됨
|
||||
- 워커 여러 개 띄우면 **워커별 Chrome 프로필 분리** 필요
|
||||
- **프록시(residential)** 사실상 필수 — 클라우드 IP는 쉽게 차단됨. 포트(IP) 풀은 넉넉히(회전 여유)
|
||||
- 워커 여러 개는 **워커별 Chrome 프로필 분리 + 다른 프록시 IP**(이미 `WORKER_CONCURRENCY`로 자동 처리)
|
||||
- 시크릿(config.local.toml)은 이미지에 굽지 말고 **마운트** 권장
|
||||
- **비용**: 대역폭이 원가의 대부분(오픈마켓 크롤) — 같은 상품 재크롤을 줄이는 TTL 캐시가 다음 절감 후보
|
||||
|
||||
Loading…
Reference in New Issue
Block a user