← 블로그로 돌아가기

파서 풀에서 나쁜 프록시 제거: 오류 메트릭 및 파이썬을 통한 로테이션

프록시 풀에서 파서의 열화 신호를 나타내는 오류 메트릭과 IP 자동 필터링 및 회전 시스템 구축 방법을 설명합니다.

📅2026년 10월 19일

Wildberries 또는 Ozon 파서가 갑자기 빈 페이지, 타임아웃 및 403 오류를 반환하기 시작했다면, 문제는 항상 플랫폼 자체에 있는 것은 아닙니다. 대부분의 경우 프록시 풀의 일부 IP 주소가 이미 "만료"되었습니다: 차단되었거나 속도가 느려지거나 CAPTCHA에 걸렸습니다. 어떤 메트릭을 통해 작동하는 프록시와 죽은 프록시를 구별할 수 있는지, 그리고 수동 제어 없이 필터링을 자동화하는 방법을 살펴보겠습니다.

프록시 풀이 시간이 지남에 따라 열화되는 이유

어떤 프록시 풀도 — 신뢰할 수 있는 제공업체에서 구입한 경우에도 — 시간이 지남에 따라 일부 작동 주소를 잃게 됩니다. 그 이유는 다양합니다: Wildberries, Ozon 및 Avito의 안티프로드 시스템은 IP에 대한 평판 태그를 축적하고, 일부 주소는 제공업체의 다른 고객의 대량 요청 후 블랙리스트에 올라가며(프록시가 공유되는 경우), 데이터 센터 범위는 서브넷별로 대량으로 차단되고, 레지던트 IP는 실제 사용자가 장치를 끄면 풀에서 사라지기도 합니다.

풀을 정리하지 않으면 "죽은" 프록시의 비율이 비선형적으로 증가합니다: 처음에는 5%, 일주일의 적극적인 파싱 후에는 20-30%가 됩니다. 파서는 작동하지 않는 IP를 통해 반복 시도하는 데 자원을 낭비하게 되고, 한 페이지를 수집하는 평균 시간이 증가하며, 로그에는 실제 문제(예: 사이트 레이아웃 변경)를 숨기는 오류가 누적됩니다. 필터링 시스템이 없으면 풀을 "예비"로 부풀리거나 데이터 수집에서 정기적으로 실패를 겪게 됩니다.

과제가 단순히 오류 사실을 기록하는 것이 아니라 이를 분류하는 것입니다: 서버의 일시적인 과부하, 특정 IP 차단, 지역 차단 또는 파서 측의 문제(잘못된 헤더, 오래된 쿠키). 오류 유형에 따라 프록시를 영구적으로 풀에서 제거해야 하는지, 일정 시간 동안 "격리"해야 하는지, 아니면 단순히 요청을 반복해야 하는지가 결정됩니다.

필터링을 위한 주요 오류 메트릭

일시적인 실패를 진짜 죽은 프록시와 구별하기 위해서는 각 IP에 대해 여러 메트릭을 수집해야 하며, 단일 HTTP 상태에 의존해서는 안 됩니다. 다음은 각 요청 시 기록해야 할 기본 세트입니다:

메트릭 신호 조치
HTTP 403 / 429 IP 차단 또는 속도 제한 30-60분 격리, 반복 시 — 삭제
연결 타임아웃 프록시가 과부하되었거나 죽음 3회 연속 타임아웃 — 풀에서 삭제
응답에 CAPTCHA 포함 IP가 안티봇의 의심을 받음 요청 빈도 줄이기 또는 IP 교체
응답 시간 > 5초 프록시 측의 속도 저하 풀에서 우선 순위 낮추기
응답 크기가 평균 이하 차단 페이지/대체 페이지가 반환됨 수용 전에 내용 추가 확인
성공적인 요청 비율 (success rate) IP 품질의 종합 평가 50 요청 중 70% 미만 — 삭제

사이트 오류 코드와 네트워크 오류를 혼동하지 않는 것이 중요합니다. 예를 들어, 404는 프록시의 문제가 아니라 Wildberries에서 상품이 없음을 나타냅니다. 그러나 동일한 IP가 다른 IP가 200을 받을 때 403을 정기적으로 받는다면, 이는 해당 주소가 특정 차단의 명백한 신호입니다.

프록시 스코어링 알고리즘

이진적인 "작동 / 비작동" 대신 각 IP에 대해 0에서 100까지의 점수를 계산하는 것이 더 편리하며, 이는 오류가 발생할 때 점차 감소하고 성공적인 요청 시 회복됩니다. 이는 우연한 실패 후 프록시를 즉시 제거하지 않도록 하며(마켓플레이스 서버가 잠시 멈출 수 있음), 체계적으로 쓰레기를 반환하는 IP를 빠르게 제거할 수 있습니다.

논리는 간단합니다: 유효한 내용과 함께 200 코드의 성공적인 요청은 +2를 점수에 추가합니다(단, 100을 초과하지 않음). 타임아웃은 -15입니다. 403/429 코드는 -25입니다. 응답에 CAPTCHA가 포함되면 -20입니다. 점수가 임계값(예: 30) 이하로 떨어지면, 프록시는 고정된 시간 동안 격리되며, 격리에서 복귀한 후 점수가 다시 10 이하로 떨어지면 IP가 풀에서 영구적으로 삭제되고 분석을 위해 기록됩니다.

이러한 접근 방식은 Dolphin Anty 또는 AdsPower와 같은 안티디텍트 브라우저의 평판 시스템과 유사하며, 계정은 활동에 따라 "건강"을 가집니다. 동일한 메커니즘은 파싱의 IP 주소에도 잘 작동합니다.

코드: Python으로 자동 필터링이 있는 ProxyPool

아래는 오류 메트릭을 고려하여 프록시 풀을 관리하기 위한 클래스의 작동 프레임워크입니다. 이를 requests, httpx 또는 Scrapy의 모든 파서에 연결할 수 있으며, 프록시를 가져오는 로직을 pool.get_proxy() 호출로 대체하면 됩니다.

import time
import random
from dataclasses import dataclass, field
from typing import Optional

@dataclass
class ProxyStats:
    address: str
    score: int = 100
    quarantine_until: float = 0.0
    total_requests: int = 0
    total_success: int = 0
    consecutive_timeouts: int = 0
    is_dead: bool = False

    @property
    def success_rate(self) -> float:
        if self.total_requests == 0:
            return 1.0
        return self.total_success / self.total_requests


class ProxyPool:
    def __init__(self, proxies: list[str],
                 min_score: int = 30,
                 dead_score: int = 10,
                 quarantine_seconds: int = 1800):
        self.stats = {p: ProxyStats(address=p) for p in proxies}
        self.min_score = min_score
        self.dead_score = dead_score
        self.quarantine_seconds = quarantine_seconds

    def _is_available(self, stats: ProxyStats) -> bool:
        if stats.is_dead:
            return False
        if stats.quarantine_until > time.time():
            return False
        return True

    def get_proxy(self) -> Optional[str]:
        available = [s for s in self.stats.values() if self._is_available(s)]
        if not available:
            return None
        # 점수에 따라 정렬하고, 상위 30%에서 무작위로 선택하여 하나의 IP를 계속 사용하지 않도록 합니다.
        available.sort(key=lambda s: s.score, reverse=True)
        top_pool = available[:max(1, len(available) // 3)]
        return random.choice(top_pool).address

    def report_success(self, proxy: str):
        s = self.stats[proxy]
        s.total_requests += 1
        s.total_success += 1
        s.consecutive_timeouts = 0
        s.score = min(100, s.score + 2)

    def report_timeout(self, proxy: str):
        s = self.stats[proxy]
        s.total_requests += 1
        s.consecutive_timeouts += 1
        s.score -= 15
        if s.consecutive_timeouts >= 3:
            self._quarantine(s)
        self._check_dead(s)

    def report_blocked(self, proxy: str, captcha: bool = False):
        s = self.stats[proxy]
        s.total_requests += 1
        s.score -= 20 if captcha else 25
        self._quarantine(s)
        self._check_dead(s)

    def _quarantine(self, s: ProxyStats):
        if s.score < self.min_score:
            s.quarantine_until = time.time() + self.quarantine_seconds

    def _check_dead(self, s: ProxyStats):
        if s.score <= self.dead_score:
            s.is_dead = True

    def stats_report(self) -> list[dict]:
        return [
            {
                "proxy": s.address,
                "score": s.score,
                "success_rate": round(s.success_rate, 2),
                "dead": s.is_dead,
            }
            for s in self.stats.values()
        ]

실제 파서에서는 report_success, report_timeout 및 report_blocked 호출이 각 HTTP 요청 후 응답 처리기에 직접 삽입됩니다. requests와의 통합 예시는 다음과 같습니다:

import requests

def fetch(url: str, pool: ProxyPool):
    proxy = pool.get_proxy()
    if not proxy:
        raise RuntimeError("풀에 사용할 수 있는 프록시가 없습니다.")

    proxies = {"http": proxy, "https": proxy}
    try:
        resp = requests.get(url, proxies=proxies, timeout=8)
    except requests.exceptions.Timeout:
        pool.report_timeout(proxy)
        return None

    if resp.status_code in (403, 429):
        pool.report_blocked(proxy)
        return None

    if "captcha" in resp.text.lower():
        pool.report_blocked(proxy, captcha=True)
        return None

    pool.report_success(proxy)
    return resp

이러한 접근 방식은 수동 모니터링의 부담을 덜어줍니다: 한 번 임계값을 설정하면, 풀은 스스로 "회복"하여 죽은 주소를 밀어내고 일시적으로 멈춘 주소를 격리에서 복원합니다.

IP 차단을 위한 임계값

보편적인 숫자는 존재하지 않으며 — 임계값은 플랫폼과 안티봇의 공격성에 따라 다릅니다. 그러나 Wildberries, Ozon 및 Avito 파싱 시 실제로 잘 작동하는 기준이 있습니다:

  • 성공률이 70% 미만인 30-50 요청 샘플 — 현재 점수가 비판적이지 않더라도 IP를 삭제할 이유가 됩니다.
  • 3회 연속 타임아웃 — 프록시가 물리적으로 접근할 수 없거나 과부하 상태임을 거의 항상 의미하므로, 활성 풀에 유지할 필요가 없습니다.
  • 20 요청 중 1회 이상 CAPTCHA — IP가 이미 안티봇의 의심을 받고 있으므로, 요청 빈도를 줄이거나 주소를 교체하는 것이 좋습니다.
  • 응답 평균 시간이 풀의 중앙값보다 2배 이상 — 신호 열화, 비판적이지는 않지만 선택 시 우선 순위를 낮출 이유가 됩니다.

30-60분의 격리는 일반적으로 데이터 센터 및 레지던트 프록시에 충분하여 안티봇이 일시적인 의심스러운 활동을 "잊게" 합니다. 모바일 프록시의 경우 상황이 다릅니다: IP는 운영자가 셀을 변경할 때 변경되므로, 격리는 더 짧게 — 10-15분으로 설정할 수 있습니다. 회전 후에는 물리적으로 다른 주소가 됩니다.

헬스체크 및 백그라운드 회전

오류가 발생한 경우의 반응적 필터링 외에도, 유용한 백그라운드 헬스체크를 실행하는 것이 좋습니다 — 몇 분마다 중립 URL(예: httpbin.org/ip)에 가벼운 요청을 보내 프록시를 점검하고, 프록시가 Wildberries 또는 Ozon에 대한 전투 요청에 들어가기 전에 풀의 통계를 업데이트하는 별도의 프로세스입니다.

import threading

def healthcheck_loop(pool: ProxyPool, interval: int = 300):
    while True:
        for proxy in list(pool.stats.keys()):
            try:
                r = requests.get(
                    "https://httpbin.org/ip",
                    proxies={"http": proxy, "https": proxy},
                    timeout=5
                )
                if r.status_code == 200:
                    pool.report_success(proxy)
                else:
                    pool.report_blocked(proxy)
            except requests.exceptions.RequestException:
                pool.report_timeout(proxy)
        time.sleep(interval)

threading.Thread(target=healthcheck_loop, args=(pool,), daemon=True).start()

이러한 백그라운드 프로세스는 풀의 크기가 크고(수백 개의 IP) 파싱이 지속적으로 이루어지지 않고 배치로 진행될 때 특히 중요합니다 — 헬스체크가 없으면 일부 프록시가 실행 사이에 "만료"될 수 있으며, 파싱의 첫 몇 분이 실시간으로 필터링하는 데 소모되고, 검증된 주소로 즉시 작업하지 못하게 됩니다.

마켓플레이스 파싱을 위한 프록시 유형 선택

프록시 유형 선택은 필터링을 얼마나 자주 실행해야 하는지와 임계값이 얼마나 공격적일지를 직접적으로 영향을 미칩니다. Wildberries 및 Ozon의 안정적인 파싱을 위해, 안티봇이 행동 패턴 및 서브넷의 평판을 분석하는 곳에서는 레지던트 프록시가 가장 잘 작동합니다 — 이들의 IP는 실제 가정 인터넷 제공업체에 속하며, 안티봇이 서브넷별로 대량으로 차단하는 일이 훨씬 적습니다.

더 간단하고 덜 보호된 출처(예: Avito에서 공개 데이터 수집 또는 간단한 진열대의 가격 모니터링)에는 데이터 센터 프록시가 종종 충분합니다 — 이들은 더 빠르고 저렴하여 대량 요청 시 중요하지만, 차단되는 빈도가 약간 더 높으므로 필터링 임계값을 더 엄격하게 설정하는 것이 좋습니다.

프록시 유형 필터링 속도 언제 사용해야 하는지
데이터 센터 높음, 서브넷이 대량으로 차단됨 간단한 진열대, 대량 요청, 낮은 보호
레지던트 중간, IP가 점차적으로 차단됨 Wildberries, Ozon, 안티봇이 있는 사이트
모바일 낮음, IP가 자동으로 운영자에 의해 변경됨 강력하게 보호된 출처, 고빈도 파싱

실제로 많은 팀이 하이브리드 풀을 사용합니다: 주요 요청의 대부분은 레지던트 IP를 통해 이루어지며, 대량 차단 시 모바일 프록시가 "비상" 예비로 연결됩니다. 이 경우 IP 변경이 셀 네트워크의 회전을 통해 자연스럽게 발생하므로 수동 개입이 필요하지 않습니다.

결론

필터링 시스템이 없는 프록시 풀은 필연적으로 열화되며, 정리 없이 작동할수록 파서가 죽은 IP에 낭비하는 자원이 증가합니다. HTTP 코드, 타임아웃, CAPTCHA 및 성공률을 고려한 스코어링 모델은 문제 주소를 자동으로 식별하고 수동 제어 없이 풀을 작동 상태로 유지할 수 있게 합니다. 이 기사에서 제공된 코드는 requests, httpx, Scrapy 또는 자체 비동기 파서와 같은 모든 스택에 맞게 조정할 수 있습니다.

마켓플레이스 파싱을 위한 풀을 처음 구축하고 있으며 차단 빈도를 줄이고 싶다면, 레지던트 프록시를 고려하는 것이 좋습니다 — 이들은 일반 데이터 센터 주소에 비해 Wildberries 및 Ozon과 같은 보호된 플랫폼에서 훨씬 더 높은 성공률을 제공합니다.