정중하면서도 확장 가능한 스크래핑: 속도 제한 플레이북

사이트를 무작정 두드리면 차단당하고, 한 번에 하나의 요청을 크롤링하면 아무것도 얻지 못합니다. 확장 가능한 중간 지점은 도메인별 적응형 속도 조절을 통해 깨끗한 IP에 분산시키는 것입니다. 사이트에 정중하면서도 빠르게 접근할 수 있습니다.

현재 인터넷 트래픽의 거의 절반이 자동화되어 있으며, 웹사이트들은 이를 알고 있습니다. 속도 제한은 그들의 첫 번째이자 가장 기본적인 방어 수단입니다. 이는 얼마나 많이 가져갈 수 있고 얼마나 빠르게 가져갈 수 있는지를 결정하는 속도 제한입니다. 이를 무력으로 돌파하려는 유혹이 있지만, 이는 차단당하게 만듭니다. 반대로, 한 번에 신중한 요청을 하나씩 크롤링하는 것은 확장성에서 아무것도 얻지 못합니다. 확장 가능한 답은 정중한 스크래핑입니다: 무거운 합법적인 사용자가 할 것처럼 각 대상을 조절하고, 깨끗한 IP에 부하를 분산시키며, 사이트의 응답에 따라 속도를 조정하세요. 이렇게 하면 모든 사람이 차단당하는 트래픽이 되지 않으면서도 빠르게 유지할 수 있습니다.

어떤 제한에 걸렸는지 파악하세요

서버는 식별자(보통은 IP, 때로는 API 키 또는 계정)를 기준으로 요청을 계산하고, 임계치를 초과하면 조치를 취합니다. 세 가지 일반적인 모델은 다르게 작동합니다: 고정 창은 달력 분당 요청을 계산합니다; 토큰 버킷은 고정된 수의 토큰(예: 분당 100개)을 제공하고, 요청당 하나씩 사용하여 버킷이 비면 대기를 강요합니다; 슬라이딩 창은 언제든지 마지막 60초 동안의 요청을 계산합니다. 실질적인 결과는 폭발적인 요청이 지속적인 흐름보다 더 위험하다는 것입니다 — 1초에 10개의 요청은 1분에 100개로 분산된 요청이 초과하지 않는 제한을 초과할 수 있습니다.

제한은 다양한 형태로 제공됩니다. 소프트 제한은 부드럽습니다: 서버는 속도를 늦추거나 429 Too Many Requests와 함께 Retry-After 헤더를 반환하여 정확히 얼마나 기다려야 하는지를 알려줍니다 (Cloudflare의 오류 1015가 이에 해당합니다). 일부 사이트는 작은 폭발적인 요청을 허용합니다 — 분당 100개를 허용하지만 120개에서만 제한을 가합니다 — 반면 다른 사이트는 일찍 제한을 가합니다, 예를 들어 분당 15개에서 제한을 가하고 30개에서만 강력하게 차단합니다. 하드 제한은 엄격한 상한선입니다: 시간당 1,000개의 요청을 허용하는 API는 이를 초과하면 완전히 차단됩니다. 소프트 제한을 반복적으로 초과하면 점점 강화됩니다: 429는 403이 되고, 몇 분에서 몇 시간까지의 일시적인 차단으로 이어지며, 매번 이를 초과할 때마다 창이 커지고, 결국 IP나 전체 서브넷의 영구적인 블랙리스트로 이어집니다.

응답을 읽고 추측하지 마세요

스크래퍼에 대한 가장 큰 업그레이드는 고정된 속도로 폭발하는 대신 서버가 말하는 것에 반응하는 것입니다. 어휘를 배우세요: 429는 속도를 늦추고 Retry-After를 준수하라는 의미입니다; 403은 이 식별자가 플래그되었음을 의미하므로 재시도하지 말고 회전하세요; 503은 종종 도전이나 일시적인 거부입니다; 그리고 200이 CAPTCHA 페이지를 반환하면 이는 소프트 차단이지 성공이 아닙니다. 각각을 다르게 처리하세요. 잘못된 움직임 — 403에서 소진된 IP를 재시도하거나 Retry-After를 무시하고 429를 강행하는 것은 소프트 경고를 영구적인 차단으로 바꾸는 것입니다. 429 수정에 대한 심층 분석은 응답 처리에 대한 자세한 내용을 다룹니다.

import time, requests

def polite_get(session, url, max_tries=4):
    for attempt in range(max_tries):
        r = session.get(url, timeout=20)
        if r.status_code == 200 and "captcha" not in r.text.lower():
            return r
        if r.status_code == 429:                       # obey the server
            wait = int(r.headers.get("Retry-After", 2 ** attempt))
            time.sleep(wait)
            continue
        if r.status_code in (403, 503):                # this exit is burned
            rotate_ip(session)                         # fresh IP, then retry
            time.sleep(2 ** attempt)                    # exponential backoff
            continue
        return r
    return None
요청을 보내고 상태 코드를 읽고 IP를 회전시키고 후퇴한 다음 속도를 조정하는 적응형 속도 조절 루프의 다이어그램
사이트의 자체 응답 코드가 속도를 조절하도록 하세요: 200에서는 가속하고, 429에서는 Retry-After를 준수하며, 403에서는 회전하세요.

도메인별 요청 예산 설정

여러 사이트에 접근하는 크롤러는 절대 모든 사이트에 하나의 글로벌 속도를 적용해서는 안 됩니다. 작은 블로그와 강화된 마켓플레이스는 매우 다른 부하를 허용하므로 각 도메인에 자체 예산을 부여하세요. 호스트별 토큰 버킷은 깔끔한 패턴입니다: 도메인별로 보수적인 속도를 할당하고, 시간이 지남에 따라 이를 보충하며, 다른 호스트에 대한 요청은 병렬로 실행하고 동일한 호스트에 대한 요청은 그 제한 내에서 유지하세요. 새로운 대상에서는 천천히 시작하고 응답 코드가 속도를 높일 수 있는지 알려주도록 하세요.

import time
from collections import defaultdict

class DomainLimiter:
    def __init__(self, per_min=30):
        self.gap = 60.0 / per_min          # min seconds between hits per host
        self.last = defaultdict(float)
    def wait(self, host):
        now = time.time()
        delay = self.gap - (now - self.last[host])
        if delay > 0:
            time.sleep(delay)
        self.last[host] = time.time()

# 30 req/min to any single host; different hosts proceed independently
limiter = DomainLimiter(per_min=30)
limiter.wait("example.com")

부하를 분산시켜 각 IP가 정중하게 유지되도록 하세요

"정중함"과 "확장성"을 조화시키는 움직임은 다음과 같습니다: 정중함은 IP별로 측정되지만, 총 처리량은 IP 전체의 합계입니다. 대상이 주소당 분당 30개의 요청을 허용한다면, 하나의 IP는 30으로 제한하지만, 각각 30개를 수행하는 10개의 깨끗한 IP는 분당 300개를 제공하며, 각 개별 출구는 정중하게 유지됩니다. 회전하는 주거 게이트웨이는 이를 자동으로 수행하여 90M+ 주소에 걸쳐 각 요청에 새 IP를 제공하여 단일 출구가 공격적으로 보이지 않도록 합니다. 이는 더 강하게 두드리는 트릭이 아닙니다; 이는 진정한 부하를 분산시켜 단일 서버가 의심스러운 급증을 겪지 않도록 하는 것입니다. IP 회전의 기본 사항은 IP 회전이 무엇이며 왜 중요한지에 있습니다.

깨끗한 회전 IP에 부하를 분산시키세요

덜 가져가고, 더 캐시하고, 시간을 선택하세요

가장 정중한 요청은 보내지 않는 요청입니다. 세 가지 습관이 데이터를 잃지 않고 부하를 줄입니다. 첫째, 적극적으로 캐시하고 조건부 요청을 사용하세요 — If-Modified-Since 또는 If-None-Match를 보내 변경되지 않은 페이지가 전체 본문 대신 작은 304를 반환하도록 하여 서버와 대역폭을 절약합니다. 둘째, 동시성을 신중하게 예산하세요: 도메인별로 진행 중인 요청을 제한하는 세마포어는 실수로 폭발적인 요청을 방지합니다. 셋째, 대상의 비피크 시간대에 무거운 작업을 예약하세요, 이때 트래픽은 그들의 트래픽의 작은 부분이며 임계치를 초과할 가능성이 적습니다. 이를 결합하면 작업에 필요한 요청을 절반으로 줄일 수 있습니다 — 우리의 더 넓은 차단 방지 체크리스트의 기초입니다.

한 가지 더 명확히 말할 가치가 있습니다: robots.txt를 확인하고 사이트의 명시된 크롤링 기대치를 준수하세요. 정중함은 단순한 자기 보존이 아닙니다 — 좋은 시민이 되는 것은 모든 사람이 개방형 웹을 스크래핑할 수 있도록 유지하는 것입니다. robots.txt 실천 가이드는 그것이 무엇을 묶고 무엇을 묶지 않는지를 다룹니다.

429 소프트 제한에서 403, 일시적인 차단 및 영구적인 IP 또는 서브넷 블랙리스트로의 에스컬레이션 사다리 다이어그램
소프트 제한을 반복적으로 초과하면 강화됩니다: 429는 403이 되고, 점점 커지는 일시적인 차단이 되며, 결국 영구적인 블랙리스트가 됩니다.

자주 묻는 질문

스크래핑할 때 속도 제한을 어떻게 피할 수 있나요?

각 도메인에 자체 요청 예산을 설정하고, 429에서 Retry-After를 준수하며, 지수적으로 후퇴하고, 깨끗한 IP의 회전 풀에 부하를 분산시켜 단일 주소가 공격적으로 보이지 않도록 하세요. 캐싱과 조건부 요청을 추가하여 전체 요청 수를 줄이고, 대상의 비피크 시간대에 무거운 작업을 예약하세요.

HTTP 429는 무엇을 의미하며 어떻게 처리해야 하나요?

429 Too Many Requests는 소프트 속도 제한입니다 — 서버가 속도를 늦추라고 요청하는 것이지 차단하는 것이 아닙니다. Retry-After 헤더를 읽고 정확히 그만큼 기다린 후 재시도하세요; 없으면 지수적으로 후퇴하세요. 이를 무시하고 계속 두드리면 429가 403으로, 그리고 일시적 또는 영구적인 차단으로 에스컬레이션됩니다.

분당 몇 개의 요청이 안전한가요?

보편적인 숫자는 없습니다 — 전적으로 대상에 따라 다릅니다. 작은 사이트는 분당 몇 개의 요청만 허용할 수 있으며, 큰 사이트는 훨씬 더 많은 요청을 허용할 수 있습니다. 보수적으로 시작하세요 (예: IP당 분당 20–30개) 429를 주시하고 응답에 따라 조정하세요. IP를 추가하여 총 처리량을 확장하세요, 단일 IP의 속도를 올리지 마세요.

회전 프록시는 무례한가요?

진정한 부하를 분산시키는 데 사용될 때는 그렇지 않습니다. 회전은 각 개별 IP를 정중한 속도 내에 유지하면서 총 처리량을 증가시킵니다 — 서버는 어떤 주소에서도 의심스러운 급증을 보지 못합니다. 사이트가 합리적으로 처리할 수 있는 총량을 초과할 때만 무례해집니다; 전체를 조절하세요, 단지 IP별 속도만이 아니라.

정중함과 확장 가능성은 반대가 아닙니다. 신호를 읽고, Retry-After를 준수하며, 도메인별로 예산을 설정하고, 캐시할 수 있는 것을 캐시하고, 나머지를 깨끗한 회전 IP에 분산시키세요. 이렇게 하면 무모한 스크래퍼보다 더 빠르게 됩니다 — 차단당하지 않는 사람이기 때문입니다.

회전하는 주거 프록시로 정중하게 확장하세요