데모 영상이 곧 공개됩니다...
Vinted 특가 탐지 봇 · 무료 프록시를 바꿔가며 문서화되지 않은 비공개 API를 5초마다 확인 · 새 매물이 올라오면 몇 초 안에 Discord로 알림
아키텍처
- ProxyScrape, 외부 · 프록시 소스, 외부 서비스, api.proxyscrape.com
- Vinted 비공개 API, 외부 · 대상 플랫폼, 외부 서비스, vinted.fr/api/v2
- Watchlist, 구성 · 저장된 검색, script.py · url_data
- Proxy refresher, 백그라운드 · 프록시 풀, script.py · update_proxies_thread
- Patched vinted_scraper, HTTP 클라이언트 · 수정한 포크, vintedWrapperModified.py
- Deal poller, 코어 · 폴링 루프, script.py · main(), 5초 주기, 사이클당 무작위 프록시, ID + 채널 중복 제거, 첫 패스 기준선, 상세 정보 조회, 실패 시 건너뛰기
- Discord bot API, 전달 · 별도 서비스, 외부 서비스, separate project · hosted on a VPS
- Discord 채널, 전달 · 목적지, 외부 서비스, 11 price-bucketed channels
- Proxy refresher에서 ProxyScrape 방향으로: 프랑스 프록시 목록 · 120초마다
- Patched vinted_scraper에서 Vinted 비공개 API 방향으로: 검색 + 매물 JSON
- Patched vinted_scraper에서 Vinted 비공개 API 방향으로: 재인증 · 401 응답 시
- Watchlist에서 Deal poller 방향으로: 검색 설정 15개
- Proxy refresher에서 Deal poller 방향으로: 공용 프록시 목록
- Deal poller에서 Patched vinted_scraper 방향으로: 검색 · 5초마다
- Deal poller에서 Discord bot API 방향으로: POST /ARTICLE · 새 매물마다
- Discord bot API에서 Discord 채널 방향으로: 임베드 + 알림
5초마다 Deal poller는 Proxy refresher가 ProxyScrape에서 계속 갱신하는 목록에서 프록시 하나를 무작위로 고릅니다. 이어 수정한 vinted_scraper로 15개의 Watchlist 검색을 Vinted의 /api/v2/catalog/items에 요청합니다. 각 결과는 항목 ID와 채널을 키로 삼아 이미 확인한 매물과 비교합니다. 새 매물만 판매자 평점과 사진을 가져오기 위해 /items/{id}를 한 번 더 호출합니다. 이후 VPS에서 실행 중인 Discord bot API의 POST /article로 보내 임베드를 만들고 가격대별 채널에 알립니다.
배포: Poller → 장시간 실행되는 Python 프로세스 · 로컬 머신 · Discord bot API → VPS · 항상 켜져 있는 Discord 연결
성과
- 5초 · 15개의 저장된 검색에 걸친 폴링 주기
- 120초 · 프록시 풀 자동 갱신, 수동 재시작 없음
- 0 € · 프록시 비용: 회전하는 무료 프록시, 유료 풀 없음
- 15 → 11 · 가격대별 채널로 분류한 검색
보여준 역량: 비공개 API 리버스 엔지니어링 · 의존성 수정(프록시, 인증 재시도) · 유료 인프라 없는 프록시 순환 · 오류 격리와 점진적 성능 저하 · 식별자 기반 중복 제거 · 생산자와 소비자 서비스 분리
문제와 해결책
핵심 제약은 Vinted에 공개 API가 없고 잦은 요청을 적극적으로 제한하지만 저렴한 매물은 몇 분 안에 팔린다는 점입니다.
▤ HTML 대신 비공개 API 사용
문제: 저평가된 항목은 몇 분 안에 팔리므로 알림은 몇 초 안에 도착해야 합니다. 렌더링된 HTML을 스크래핑하는 것은 느리고, 마크업이 바뀔 때마다 취약하며, 사이트가 지문을 식별하기 쉽습니다.
Vinted 웹 앱이 호출하는 JSON API를 리버스 엔지니어링했습니다. 프론트엔드와 같은 방식으로 실제 세션 쿠키와 브라우저 User-Agent를 /api/v2/catalog/items 요청에 포함합니다.
- 브라우저 자동화(Selenium)는 훨씬 무겁고 느리며 사이트 디자인이 바뀔 때마다 깨질 수 있어 제외했습니다
- 문서화되지 않은 API가 예고 없이 바뀔 수 있다는 위험은 감수했습니다
- 전체 페이지 대신 작은 JSON만 받아 15개 검색을 5초마다 확인할 수 있습니다
▒ 백그라운드에서 갱신하는 무료 프록시 목록
문제: 하나의 가정용 IP에서 5초마다 폴링하면 빠르게 속도 제한되거나 차단되고, 무료 프록시는 끊임없이 죽습니다: 오래된 목록은 실행 도중 봇을 멈추게 합니다.
매 주기마다 공용 목록에서 프록시 하나를 무작위로 선택합니다. 데몬 스레드는 120초마다 ProxyScrape에서 목록을 다시 받아오고 Vinted 지역에 맞는 프랑스 프록시만 남깁니다.
- 취미 프로젝트에서 지속적인 비용을 감당하기 어려워 유료 가정용 프록시는 제외했습니다
- 무료 프록시는 느리고 불안정하므로 개별 요청이 실패해도 전체 프로세스가 멈추지 않게 했습니다
- 인프라 비용 없이 죽은 프록시를 자동으로 교체합니다
▓ 클라이언트를 다시 쓰지 말고 라이브러리를 패치하기
문제: 기성 vinted_scraper는 프록시 지원이 없었고 200이 아닌 응답이면 RuntimeError를 던져, 불안정한 프록시 하나에 전체 루프가 죽었습니다.
라이브러리 파일 두 개를 수정했습니다. proxies 값을 실제 요청까지 전달하고, 401 응답이 오면 세션 쿠키를 다시 받아 재시도합니다. 그래도 실패하면 None을 반환해 전체 프로세스를 종료하지 않고 해당 주기만 건너뜁니다.
- requests 클라이언트를 처음부터 만들면 라이브러리가 이미 처리하는 쿠키와 User-Agent 로직까지 다시 구현해야 하므로 제외했습니다
- 모든 호출부에 try/except를 넣으면 오류 처리 정책이 코드 전체에 흩어지므로 제외했습니다
- 프로세스를 재시작하지 않아도 프록시 장애와 쿠키 만료를 처리할 수 있습니다
▞ 항목 ID와 채널로 메모리에서 중복 제거
문제: 최신순 결과 페이지는 확인할 때마다 일부 매물이 겹칩니다. 같은 매물을 반복해서 알리면 채널이 도배되고 사용자도 알림을 무시하게 됩니다.
첫 실행에서는 현재 매물을 기준값으로 저장하고 알리지 않습니다. 이후 결과는 대상 채널과 묶어 항목 ID + 채널 조합으로 비교하고, 처음 보는 조합만 알립니다.
- 매물 하나가 50 €와 150 € 가격대 채널에 모두 포함될 수 있어 채널을 키에 포함했습니다
- 장시간 실행되는 프로세스 하나에는 단순한 메모리 목록이면 충분해 데이터베이스는 쓰지 않았습니다
- 재시작하면 현재 매물을 다시 기준값으로 잡으며 실행 사이에는 상태가 유지되지 않습니다
░ 2단계 요청: 가벼운 검색 후 상세 정보 조회
문제: 검색 결과에는 유용한 임베드에 필요한 판매자 평판과 사진이 없지만, 모든 결과의 전체 세부 정보를 가져오면 API 호출과 차단 위험이 배가됩니다.
페이지당 매물 5개를 반환하는 가벼운 검색 요청을 보내고, 중복 제거 후 새 매물로 확인된 항목만 /items/{id}로 상세 정보를 가져옵니다.
- 모든 매물의 상세 정보를 가져오지 않아 평소에는 결과 수와 관계없이 주기당 요청 15개만 보냅니다
- 새 매물이 한꺼번에 많이 올라오는 일은 드물어 추가 요청은 예외적으로만 발생합니다
- 기본 요청 수를 늘리지 않고 별점, 리뷰 수, 국가가 포함된 알림을 만들 수 있습니다
▚ 폴러를 Discord 전달에서 분리하기
문제: 하나의 프로세스에서 데이터 수집과 Discord 메시지 생성을 함께 처리하면 불안정하고 차단되기 쉬운 수집기가 봇의 가동 시간과 채널 로직에 영향을 줍니다.
폴러는 데이터 수집만 담당합니다. POST /article로 단순한 JSON을 VPS의 봇 서비스에 보내면 봇이 임베드 생성, 채널 분류, 사진이 없는 매물의 대체 이미지 처리를 맡습니다.
- VPS는 Discord 연결을 항상 유지하므로 스크래퍼가 중단되거나 재시작돼도 봇은 계속 동작합니다
- 프로세스마다 한 가지 책임만 맡도록 폴러에 discord.py를 포함하지 않았습니다
- 배포 대상이 두 개로 늘고 둘 사이에 네트워크 요청이 추가되는 비용은 감수했습니다