Vinted를 위한 특가 낚아채기 봇 · 회전하는 무료 프록시 뒤에서 문서화되지 않은 비공개 API를 5초마다 폴링합니다 · 매물이 올라온 지 몇 초 만에 Discord로 알립니다
아키텍처
5초마다 Deal poller는 Proxy refresher가 ProxyScrape로부터 계속 재구성하는 풀에서 무작위 프록시를 하나 뽑은 뒤, patched vinted_scraper를 통해 15개의 Watchlist 검색을 모두 Vinted의 /api/v2/catalog/items에 대해 실행합니다. 결과는 이미 본 모든 항목과 대조되며, 항목 id + channel을 키로 사용합니다. 진짜 새로운 매물마다 판매자 평판과 사진을 위해 /items/{id} 호출이 한 번 추가로 발생하고, 그다음 VPS에 호스팅된 Discord bot API로 POST /article로 전달되어 임베드를 포맷하고 가격대별 channel에 알림을 보냅니다.
배포: Poller → 장시간 실행되는 Python 프로세스 · 로컬 머신 · Discord bot API → VPS · 항상 켜져 있는 Discord 연결
성과
- 5초 · 15개의 저장된 검색에 걸친 폴링 주기
- 120초 · 프록시 풀 자동 갱신, 수동 재시작 없음
- 0 € · 프록시 비용: 회전하는 무료 프록시, 유료 풀 없음
- 15 → 11 · 가격대별 channel로 라우팅된 검색
보여준 역량: 비공개 API 리버스 엔지니어링 · 의존성 패치(프록시, 인증 재시도) · 유료 인프라 없는 프록시 회전 · 즉시 크래시 대신 우아한 성능 저하 · 신원 기반 중복 제거 설계 · 생산자 / 소비자 서비스 분리
문제와 해결책
모든 선택은 하나의 제약에 답합니다: Vinted에는 공개 API가 없고 공격적인 폴러의 속도를 적극적으로 제한하지만, 저평가된 특가는 몇 분 안에 팔립니다.
▤ HTML이 아니라 비공개 API로 말하기
문제: 저평가된 항목은 몇 분 안에 팔리므로 알림은 몇 초 안에 도착해야 합니다. 렌더링된 HTML을 스크래핑하는 것은 느리고, 마크업이 바뀔 때마다 취약하며, 사이트가 지문을 식별하기 쉽습니다.
Vinted 웹 앱 자체가 호출하는 JSON API를 리버스 엔지니어링했습니다: 프론트엔드가 하는 것과 똑같이 실제 세션 쿠키와 브라우저 User-Agent를 /api/v2/catalog/items에 대해 재생합니다.
- 브라우저 자동화(Selenium)를 거부: 훨씬 무겁고, 폴링당 더 느리며, 리디자인마다 깨집니다
- 감수한 트레이드오프: 문서화되지 않은 API는 예고 없이 바뀔 수 있습니다
- 얻은 것: 전체 페이지 대신 작은 JSON 페이로드로 15개 검색을 5초마다 폴링
▒ 백그라운드 리프레셔가 있는 회전하는 무료 프록시
문제: 하나의 가정용 IP에서 5초마다 폴링하면 빠르게 속도 제한되거나 차단되고, 무료 프록시는 끊임없이 죽습니다: 오래된 목록은 실행 도중 봇을 멈추게 합니다.
각 사이클은 daemon 스레드가 ProxyScrape로부터 120초마다 재구성하고 스토어에 맞춰 프랑스 출구로 필터링한 공유 풀에서 무작위 프록시를 고릅니다.
- 유료 레지덴셜 프록시를 거부: 취미 규모에서 반복 비용은 과도합니다
- 감수한 트레이드오프: 무료 프록시는 느리고 불안정하므로 모든 실패는 생존 가능해야 합니다
- 얻은 것: 프록시가 죽을 때 인프라 비용 0과 수동 개입 없음
▓ 클라이언트를 다시 쓰지 말고 라이브러리를 패치하기
문제: 기성 vinted_scraper는 프록시 지원이 없었고 200이 아닌 응답이면 RuntimeError를 던져, 불안정한 프록시 하나에 전체 루프가 죽었습니다.
라이브러리 파일 두 개를 수정했습니다: proxies를 요청까지 스레딩하고, 401에서 세션 쿠키를 자동으로 다시 가져와 재시도하며, 실패 시 None을 반환해 호출자가 죽는 대신 나쁜 사이클을 건너뛰도록 했습니다.
- 처음부터 만드는 requests 클라이언트를 거부: 라이브러리가 이미 잘 처리하는 쿠키와 User-Agent 처리를 다시 구현하게 됩니다
- 모든 호출 지점의 try/except를 거부: 오류 정책을 코드베이스 전체에 흩뿌립니다
- 얻은 것: 재시작 없이 프록시 죽음과 쿠키 만료를 견디는 폴러
▞ 항목 id + channel로 메모리에서 중복 제거하기
문제: 최신순 결과 페이지는 폴링 간에 겹칩니다. 같은 매물을 다시 알리면 channel을 스팸으로 채우고 멤버들이 알림을 무시하도록 길들입니다.
첫 패스는 이미 존재하는 것을 기준선으로 삼기만 합니다. 그 후 모든 결과는 대상 channel과 함께 감싸지고 항목 id + channel에 대한 커스텀 동등성으로 비교되며, 보지 못한 쌍만 알립니다.
- channel을 키의 일부로 삼은 것은 의도적입니다: 하나의 매물이 50 € 버킷과 150 € 버킷 양쪽에서 정당하게 알릴 수 있습니다
- 데이터베이스를 거부: 하나의 장시간 실행 프로세스에는 단순한 인메모리 리스트로 충분합니다
- 감수한 트레이드오프: 재시작 시 조용히 다시 기준선을 잡으며, 실행 간 지속성은 없습니다
░ 2단계 페치: 저렴한 검색, 그다음 하이드레이션
문제: 검색 결과에는 유용한 임베드에 필요한 판매자 평판과 사진이 없지만, 모든 결과의 전체 세부 정보를 가져오면 API 호출과 차단 위험이 배가됩니다.
페이지당 5개 항목의 저렴한 검색으로 폴링하고, 중복 제거로 새롭다고 확인된 매물만 게시 전에 전체 세부 정보를 위한 두 번째 /items/{id} 호출을 받습니다.
- 항상 하이드레이션하기를 거부: 정상 상태는 결과 볼륨과 무관하게 사이클당 15개 요청에 머뭅니다
- 새 항목의 폭주는 드물기 때문에 추가 호출은 규칙이 아니라 예외입니다
- 얻은 것: 기본 요청 속도를 높이지 않고 더 풍부한 알림(별점, 리뷰 수, 국가)
▚ 폴러를 Discord 전달에서 분리하기
문제: 하나의 프로세스에서 스크래핑과 Discord 포맷팅을 섞으면 불안정하고 차단되기 쉬운 스크래퍼가 봇의 가동 시간과 channel 로직에 결합됩니다.
폴러는 생산만 합니다: 임베드, channel 라우팅, 사진 없는 매물을 위한 대체 이미지를 소유하는 VPS에 호스팅된 봇 서비스로 POST /article을 통해 평평한 JSON 페이로드를 POST합니다.
- VPS는 Discord 연결을 24/7로 유지합니다: 스크래퍼 크래시나 재시작이 봇을 절대 떨어뜨리지 않습니다
- 폴러에 discord.py를 임베드하기를 거부: 하나의 프로세스, 하나의 책임
- 감수한 트레이드오프: 실행할 두 개의 배포와 그 사이의 네트워크 홉