블로그로 돌아가기

대량 요청 시 차단을 피하는 방법

자동화 탐지 메커니즘과 대량 요청 시 차단 방지를 위한 구체적인 보호 기술을 다룹니다: 기본 프록시 회전부터 인간 행동 모방까지.

📅2025년 12월 21일
```html

대량 요청 시 차단 방지: 기술 및 도구

계정 및 IP 차단은 파싱, 자동화 및 소셜 미디어에서의 대량 작업 시 주요 문제입니다. 현대의 안티봇 시스템은 요청 빈도부터 브라우저 지문까지 수십 가지 매개변수를 분석합니다. 이 가이드에서는 자동화 탐지의 구체적인 메커니즘과 이를 우회하는 실용적인 방법을 살펴보겠습니다.

자동화 탐지 메커니즘

현대의 보호 시스템은 봇을 식별하기 위해 다단계 분석을 사용합니다. 이러한 메커니즘을 이해하는 것은 올바른 우회 전략을 선택하는 데 매우 중요합니다.

분석의 주요 매개변수

IP 평판: 안티봇 시스템은 IP 주소의 이력, 데이터 센터 소속, 블랙리스트 여부를 확인합니다. 알려진 프록시 풀에서의 IP는 더 자주 차단됩니다.

요청 빈도(Request Rate): 사람은 물리적으로 1분에 100개의 요청을 보낼 수 없습니다. 시스템은 총 요청 수뿐만 아니라 시간에 따른 분포도 분석합니다. 요청 간 균일한 간격은 봇을 드러냅니다.

행동 패턴: 행동의 순서, 스크롤 깊이, 마우스 움직임, 페이지에서의 시간. 지연 없이 즉시 링크를 클릭하는 봇은 쉽게 인식됩니다.

기술적 지문: User-Agent, HTTP 헤더, 헤더의 순서, TLS 지문, Canvas/WebGL 지문. 이러한 매개변수의 불일치는 안티봇 시스템에 대한 경고 신호입니다.

매개변수 분석되는 내용 탐지 위험
IP 주소 평판, ASN, 지리적 위치 높음
User-Agent 브라우저 버전, OS, 장치 중간
TLS 지문 암호 집합, 확장 높음
HTTP/2 지문 헤더 순서, 설정 높음
Canvas/WebGL 그래픽 렌더링 중간
행동 클릭, 스크롤, 시간 높음

요청 속도 제한 및 요청 빈도 제어

요청 전송 속도 제어는 차단 방지의 첫 번째 방어선입니다. 프록시 회전이 있더라도 너무 공격적인 파싱은 차단으로 이어질 수 있습니다.

동적 지연

고정 간격(예: 요청 간 정확히 2초)은 쉽게 인식됩니다. 정규 분포를 사용하여 무작위 지연을 적용하세요:

import time
import random
import numpy as np

def human_delay(min_delay=1.5, max_delay=4.0, mean=2.5, std=0.8):
    """
    인간 행동을 모방하는 정규 분포의 지연 생성
    """
    delay = np.random.normal(mean, std)
    # 범위 제한
    delay = max(min_delay, min(delay, max_delay))
    
    # 현실성을 위해 마이크로 지연 추가
    delay += random.uniform(0, 0.3)
    
    time.sleep(delay)

# 사용 예
for url in urls:
    response = session.get(url)
    human_delay(min_delay=2, max_delay=5, mean=3, std=1)

적응형 요청 속도 제한

보다 발전된 접근 방식은 서버의 응답을 기반으로 속도를 조정하는 것입니다. 429(요청이 너무 많음) 또는 503 코드를 받으면 자동으로 속도를 줄이세요:

class AdaptiveRateLimiter:
    def __init__(self, initial_delay=2.0):
        self.current_delay = initial_delay
        self.min_delay = 1.0
        self.max_delay = 30.0
        self.error_count = 0
        
    def wait(self):
        time.sleep(self.current_delay + random.uniform(0, 0.5))
        
    def on_success(self):
        # 성공적인 요청 시 점진적으로 속도를 높입니다
        self.current_delay = max(
            self.min_delay, 
            self.current_delay * 0.95
        )
        self.error_count = 0
        
    def on_rate_limit(self):
        # 차단 시 급격히 속도를 줄입니다
        self.error_count += 1
        self.current_delay = min(
            self.max_delay,
            self.current_delay * (1.5 + self.error_count * 0.5)
        )
        print(f"요청 속도 제한에 도달했습니다. 새로운 지연: {self.current_delay:.2f}s")

# 사용 예
limiter = AdaptiveRateLimiter(initial_delay=2.0)

for url in urls:
    limiter.wait()
    response = session.get(url)
    
    if response.status_code == 429:
        limiter.on_rate_limit()
        time.sleep(60)  # 재시도 전 대기
    elif response.status_code == 200:
        limiter.on_success()
    else:
        # 기타 오류 처리
        pass

실용적인 조언: 다양한 사이트에 따라 최적의 속도는 다릅니다. 대형 플랫폼(구글, 페이스북)은 하나의 IP에서 분당 5-10개의 요청을 허용합니다. 소규모 사이트는 시간당 20-30개의 요청으로도 차단할 수 있습니다. 항상 보수적으로 시작하고 점진적으로 부하를 증가시키며 오류 비율을 추적하세요.

프록시 회전 및 IP 주소 관리

대량 요청에 하나의 IP 주소를 사용하면 차단이 보장됩니다. 프록시 회전은 부하를 분산시키고 탐지 위험을 줄입니다.

회전 전략

1. 요청 기반 회전: 각 요청 후 또는 N 요청마다 IP를 변경합니다. 각 요청의 익명성이 중요한 검색 엔진 파싱에 적합합니다.

2. 시간 기반 회전: 5-15분마다 IP를 변경합니다. 세션의 안정성이 중요한 소셜 미디어 작업에 효과적입니다.

3. 스티키 세션: 전체 사용자 세션(인증, 행동 순서)에 하나의 IP를 사용합니다. CSRF 보호가 있는 사이트에 중요합니다.

import requests
from itertools import cycle

class ProxyRotator:
    def __init__(self, proxy_list, rotation_type='request', rotation_interval=10):
        """
        rotation_type: 'request' (각 요청) 또는 'time' (시간 기준)
        rotation_interval: 요청 수 또는 초
        """
        self.proxies = cycle(proxy_list)
        self.current_proxy = next(self.proxies)
        self.rotation_type = rotation_type
        self.rotation_interval = rotation_interval
        self.request_count = 0
        self.last_rotation = time.time()
        
    def get_proxy(self):
        if self.rotation_type == 'request':
            self.request_count += 1
            if self.request_count >= self.rotation_interval:
                self.current_proxy = next(self.proxies)
                self.request_count = 0
                print(f"회전됨: {self.current_proxy}")
                
        elif self.rotation_type == 'time':
            if time.time() - self.last_rotation >= self.rotation_interval:
                self.current_proxy = next(self.proxies)
                self.last_rotation = time.time()
                print(f"회전됨: {self.current_proxy}")
                
        return {'http': self.current_proxy, 'https': self.current_proxy}

# 사용 예
proxy_list = [
    'http://user:pass@proxy1.example.com:8000',
    'http://user:pass@proxy2.example.com:8000',
    'http://user:pass@proxy3.example.com:8000',
]

rotator = ProxyRotator(proxy_list, rotation_type='request', rotation_interval=5)

for url in urls:
    proxies = rotator.get_proxy()
    response = requests.get(url, proxies=proxies, timeout=10)

프록시 유형 선택

프록시 유형 신뢰 수준 속도 용도
데이터 센터 낮음 높음 간단한 파싱, API
주거용 높음 중간 소셜 미디어, 보호된 사이트
모바일 매우 높음 중간 인스타그램, 틱톡, 안티 프로드

소셜 미디어 및 강력한 보호가 있는 플랫폼에서 대량 작업을 수행할 때는 주거용 프록시를 사용하세요. 이들은 일반 가정용 연결처럼 보이며 블랙리스트에 오르는 일이 드뭅니다. 데이터 센터는 속도가 중요한 덜 보호된 리소스에 적합합니다.

브라우저 지문 인식 및 TLS 지문

IP 회전이 있더라도 기술적 브라우저 지문 및 TLS 연결로 인해 추적될 수 있습니다. 이러한 매개변수는 각 클라이언트에 대해 고유하며 위조하기 어렵습니다.

TLS 지문 인식

HTTPS 연결을 설정할 때 클라이언트는 지원하는 암호 및 확장의 조합을 포함한 ClientHello를 보냅니다. 이 조합은 각 라이브러리에 대해 고유합니다. 예를 들어, Python requests는 OpenSSL을 사용하며, 이 지문은 Chrome과 쉽게 구별됩니다.

문제: 표준 라이브러리(requests, urllib, curl)는 실제 브라우저와 다른 지문을 가지고 있습니다. Cloudflare, Akamai, DataDome과 같은 서비스는 봇 차단을 위해 TLS 지문 인식을 적극적으로 사용합니다.

해결책: 브라우저의 TLS 지문을 모방하는 라이브러리를 사용하세요. Python의 경우 curl_cffi, tls_client 또는 playwright/puppeteer를 사용하여 브라우저를 완전히 에뮬레이트할 수 있습니다.

# 설치: pip install curl-cffi
from curl_cffi import requests

# Chrome 110 모방
response = requests.get(
    'https://example.com',
    impersonate="chrome110",
    proxies={'https': 'http://proxy:port'}
)

# 대안: tls_client
import tls_client

session = tls_client.Session(
    client_identifier="chrome_108",
    random_tls_extension_order=True
)

response = session.get('https://example.com')

HTTP/2 지문 인식

TLS 외에도 안티봇 시스템은 HTTP/2의 매개변수: 헤더 순서, SETTINGS 프레임 설정, 스트림 우선 순위를 분석합니다. 표준 라이브러리는 Chrome이나 Firefox의 정확한 헤더 순서를 준수하지 않습니다.

# Chrome에 대한 올바른 헤더 순서
headers = {
    ':method': 'GET',
    ':authority': 'example.com',
    ':scheme': 'https',
    ':path': '/',
    'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"Windows"',
    'upgrade-insecure-requests': '1',
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)...',
    'accept': 'text/html,application/xhtml+xml...',
    'sec-fetch-site': 'none',
    'sec-fetch-mode': 'navigate',
    'sec-fetch-user': '?1',
    'sec-fetch-dest': 'document',
    'accept-encoding': 'gzip, deflate, br',
    'accept-language': 'en-US,en;q=0.9',
}

Canvas 및 WebGL 지문 인식

브라우저는 GPU, 드라이버 및 OS에 따라 그래픽을 다르게 렌더링합니다. 사이트는 이를 사용하여 장치의 고유한 지문을 생성합니다. 헤드리스 브라우저(Selenium, Puppeteer)를 사용할 때는 자동화의 징후를 숨기는 것이 중요합니다:

// Puppeteer: 헤드리스 모드 숨기기
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');

puppeteer.use(StealthPlugin());

const browser = await puppeteer.launch({
    headless: true,
    args: [
        '--disable-blink-features=AutomationControlled',
        '--no-sandbox',
        '--disable-setuid-sandbox',
        `--proxy-server=${proxyUrl}`
    ]
});

const page = await browser.newPage();

// navigator.webdriver 재정의
await page.evaluateOnNewDocument(() => {
    Object.defineProperty(navigator, 'webdriver', {
        get: () => false,
    });
});

헤더, 쿠키 및 세션 관리

HTTP 헤더 및 쿠키를 올바르게 처리하는 것은 실제 사용자 모방에 매우 중요합니다. 이러한 매개변수의 오류는 차단의 일반적인 원인입니다.

필수 헤더

Chrome 브라우저를 모방하기 위한 최소한의 헤더 세트:

import requests

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
    'Accept-Language': 'en-US,en;q=0.9',
    'Accept-Encoding': 'gzip, deflate, br',
    'DNT': '1',
    'Connection': 'keep-alive',
    'Upgrade-Insecure-Requests': '1',
    'Sec-Fetch-Dest': 'document',
    'Sec-Fetch-Mode': 'navigate',
    'Sec-Fetch-Site': 'none',
    'Sec-Fetch-User': '?1',
    'Cache-Control': 'max-age=0',
}

session = requests.Session()
session.headers.update(headers)

쿠키 관리

많은 사이트가 첫 방문 시 추적 쿠키를 설정하고 후속 요청 시 이를 확인합니다. 쿠키가 없거나 불일치하는 경우 봇의 징후입니다.

import requests
import pickle

class SessionManager:
    def __init__(self, session_file='session.pkl'):
        self.session_file = session_file
        self.session = requests.Session()
        self.load_session()
        
    def load_session(self):
        """저장된 세션 로드"""
        try:
            with open(self.session_file, 'rb') as f:
                cookies = pickle.load(f)
                self.session.cookies.update(cookies)
        except FileNotFoundError:
            pass
            
    def save_session(self):
        """재사용을 위한 쿠키 저장"""
        with open(self.session_file, 'wb') as f:
            pickle.dump(self.session.cookies, f)
            
    def request(self, url, **kwargs):
        response = self.session.get(url, **kwargs)
        self.save_session()
        return response

# 사용 예
manager = SessionManager('instagram_session.pkl')
response = manager.request('https://www.instagram.com/explore/')

중요: 프록시 회전 시 특정 IP에 연결된 쿠키를 재설정하는 것을 잊지 마세요. IP와 쿠키의 불일치(예: 미국 지리적 위치의 쿠키와 독일의 IP)는 의심을 유발할 수 있습니다.

Referer 및 Origin

RefererOrigin 헤더는 사용자가 어디에서 왔는지를 보여줍니다. 이들이 없거나 잘못된 값은 경고 신호입니다.

# 올바른 순서: 메인 → 카테고리 → 상품
session = requests.Session()

# 단계 1: 메인 페이지 방문
response = session.get('https://example.com/')

# 단계 2: 카테고리로 이동
response = session.get(
    'https://example.com/category/electronics',
    headers={'Referer': 'https://example.com/'}
)

# 단계 3: 상품 보기
response = session.get(
    'https://example.com/product/12345',
    headers={'Referer': 'https://example.com/category/electronics'}
)

인간 행동 모방

기술적 매개변수는 일의 절반에 불과합니다. 현대의 안티봇 시스템은 사용자가 페이지와 상호작용하는 방식, 페이지에서 보내는 시간, 마우스 움직임과 같은 행동 패턴을 분석합니다.

스크롤 및 마우스 움직임

Selenium 또는 Puppeteer를 사용할 때는 무작위 마우스 움직임과 페이지 스크롤을 추가하세요:

from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
import random
import time

def human_like_mouse_move(driver):
    """페이지에서 무작위로 마우스 움직임"""
    action = ActionChains(driver)
    
    for _ in range(random.randint(3, 7)):
        x = random.randint(0, 1000)
        y = random.randint(0, 800)
        action.move_by_offset(x, y)
        action.pause(random.uniform(0.1, 0.3))
    
    action.perform()

def human_like_scroll(driver):
    """자연스러운 스크롤 모방"""
    total_height = driver.execute_script("return document.body.scrollHeight")
    current_position = 0
    
    while current_position < total_height:
        # 무작위 스크롤 단계
        scroll_step = random.randint(100, 400)
        current_position += scroll_step
        
        driver.execute_script(f"window.scrollTo(0, {current_position});")
        
        # 변동이 있는 대기
        time.sleep(random.uniform(0.5, 1.5))
        
        # 때때로 조금 뒤로 스크롤(사람들이 하는 것처럼)
        if random.random() < 0.2:
            back_scroll = random.randint(50, 150)
            current_position -= back_scroll
            driver.execute_script(f"window.scrollTo(0, {current_position});")
            time.sleep(random.uniform(0.3, 0.8))

# 사용 예
driver = webdriver.Chrome()
driver.get('https://example.com')

human_like_mouse_move(driver)
time.sleep(random.uniform(2, 4))
human_like_scroll(driver)

페이지에서의 시간

실제 사용자는 페이지에서 시간을 보내며 콘텐츠를 읽고 이미지를 살펴봅니다. 링크를 즉시 클릭하는 봇은 쉽게 인식됩니다.

def realistic_page_view(driver, url, min_time=5, max_time=15):
    """
    활동이 있는 현실적인 페이지 보기
    """
    driver.get(url)
    
    # 초기 지연 (로드 및 "읽기")
    time.sleep(random.uniform(2, 4))
    
    # 스크롤
    human_like_scroll(driver)
    
    # 추가 활동
    total_time = random.uniform(min_time, max_time)
    elapsed = 0
    
    while elapsed < total_time:
        action_choice = random.choice(['scroll', 'mouse_move', 'pause'])
        
        if action_choice == 'scroll':
            # 위/아래로 약간 스크롤
            scroll_amount = random.randint(-200, 300)
            driver.execute_script(f"window.scrollBy(0, {scroll_amount});")
            pause = random.uniform(1, 3)
            
        elif action_choice == 'mouse_move':
            human_like_mouse_move(driver)
            pause = random.uniform(0.5, 2)
            
        else:  # pause
            pause = random.uniform(2, 5)
        
        time.sleep(pause)
        elapsed += pause

탐색 패턴

의심스러운 패턴을 피하세요: 깊은 페이지로의 직접 이동, 메인 페이지 무시, 모든 요소를 건너뛰지 않고 순차적으로 탐색.

좋은 관행:

  • 메인 페이지 또는 인기 있는 섹션에서 시작하세요
  • 직접 URL을 사용하는 대신 사이트의 내부 탐색을 사용하세요
  • 때때로 뒤로 돌아가거나 다른 섹션으로 이동하세요
  • 탐색 깊이를 다양화하세요: 항상 끝까지 가지 마세요
  • "오류" 추가: 존재하지 않는 링크로 이동, 돌아가기

Cloudflare, DataDome 및 기타 보호 우회

전문 안티봇 시스템은 종합적인 접근 방식을 요구합니다. 이들은 JavaScript 챌린지, CAPTCHA, 실시간 행동 분석을 사용합니다.

Cloudflare

Cloudflare는 여러 보호 수준을 사용합니다: 브라우저 무결성 검사, JavaScript 챌린지, CAPTCHA. 기본 보호를 우회하려면 올바른 TLS 지문과 JavaScript 실행이 필요합니다:

# 옵션 1: cloudscraper (JS 챌린지 자동 해결)
import cloudscraper

scraper = cloudscraper.create_scraper(
    browser={
        'browser': 'chrome',
        'platform': 'windows',
        'desktop': True
    }
)

response = scraper.get('https://protected-site.com')

# 옵션 2: undetected-chromedriver (복잡한 경우)
import undetected_chromedriver as uc

options = uc.ChromeOptions()
options.add_argument('--proxy-server=http://proxy:port')

driver = uc.Chrome(options=options)
driver.get('https://protected-site.com')

# 챌린지 통과 대기
time.sleep(5)

# requests를 위한 쿠키 가져오기
cookies = driver.get_cookies()
session = requests.Session()
for cookie in cookies:
    session.cookies.set(cookie['name'], cookie['value'])

DataDome

DataDome은 사용자의 행동을 실시간으로 분석합니다: 마우스 움직임, 키보드 타이핑, 타이밍. 우회를 위해서는 활동을 모방하는 완전한 브라우저가 필요합니다:

from playwright.sync_api import sync_playwright
import random

def bypass_datadome(url, proxy=None):
    with sync_playwright() as p:
        browser = p.chromium.launch(
            headless=False,  # DataDome은 헤드리스 모드를 감지합니다
            proxy={'server': proxy} if proxy else None
        )
        
        context = browser.new_context(
            viewport={'width': 1920, 'height': 1080},
            user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64)...'
        )
        
        page = context.new_page()
        
        # 자동화 숨기기 위한 스크립트 주입
        page.add_init_script("""
            Object.defineProperty(navigator, 'webdriver', {get: () => false});
            window.chrome = {runtime: {}};
        """)
        
        page.goto(url)
        
        # 인간 행동 모방
        time.sleep(random.uniform(2, 4))
        
        # 무작위 마우스 움직임
        for _ in range(random.randint(5, 10)):
            page.mouse.move(
                random.randint(100, 1800),
                random.randint(100, 1000)
            )
            time.sleep(random.uniform(0.1, 0.3))
        
        # 스크롤
        page.evaluate(f"window.scrollTo(0, {random.randint(300, 800)})")
        time.sleep(random.uniform(1, 2))
        
        content = page.content()
        browser.close()
        
        return content

CAPTCHA

CAPTCHA를 자동으로 해결하려면 인식 서비스(2captcha, Anti-Captcha)를 사용하거나 회피 전략을 사용하세요:

  • CAPTCHA를 유발하지 않는 수준으로 요청 빈도를 줄이세요
  • 좋은 평판을 가진 깨끗한 주거용 IP를 사용하세요
  • 인증된 계정을 통해 작업하세요 (이들은 CAPTCHA 임계값이 더 높습니다)
  • 시간에 따라 부하를 분산하세요 (피크 시간을 피하세요)

모니터링 및 차단 처리

최고의 관행을 사용하더라도 차단은 불가피합니다. 이를 신속하게 발견하고 올바르게 처리하는 것이 중요합니다.

차단 신호

신호 설명 조치
HTTP 429 요청이 너무 많음 지연 증가, IP 변경
HTTP 403 금지됨 (IP 차단) 프록시 변경, 지문 확인
CAPTCHA 검증 필요 해결 또는 활동 줄이기
빈 응답 콘텐츠가 로드되지 않음 JavaScript, 쿠키 확인
/blocked로 리디렉션 명백한 차단 전략 완전 변경

재시도 시스템

import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry

def create_session_with_retries():
    """
    자동 재시도 및 오류 처리가 있는 세션
    """
    session = requests.Session()
    
    retry_strategy = Retry(
        total=5,
        backoff_factor=2,  # 2, 4, 8, 16, 32 초
        status_forcelist=[429, 500, 502, 503, 504],
        method_whitelist=["GET", "POST"]
    )
    
    adapter = HTTPAdapter(max_retries=retry_strategy)
    session.mount("http://", adapter)
    session.mount("https://", adapter)
    
    return session

def safe_request(url, session, max_attempts=3):
    """
    차단 처리와 함께 요청
    """
    for attempt in range(max_attempts):
        try:
            response = session.get(url, timeout=15)
            
            # 차단 확인
            if response.status_code == 403:
                print(f"IP 차단됨. 프록시 회전 중...")
                # 프록시 변경 로직
                continue
                
            elif response.status_code == 429:
                wait_time = int(response.headers.get('Retry-After', 60))
                print(f"요청 속도 제한. {wait_time}s 대기 중...")
                time.sleep(wait_time)
                continue
                
            elif 'captcha' in response.text.lower():
                print("CAPTCHA 감지됨")
                # CAPTCHA 해결 또는 건너뛰기 로직
                return None
                
            return response
            
        except requests.exceptions.Timeout:
            print(f"시도 {attempt + 1}에서 시간 초과")
            time.sleep(5 * (attempt + 1))
            
        except requests.exceptions.ProxyError:
            print("프록시 오류. 회전 중...")
            # 프록시 변경
            continue
            
    return None

로깅 및 분석

전략 최적화를 위한 메트릭을 추적하세요:

import logging
from collections import defaultdict
from datetime import datetime

class ScraperMetrics:
    def __init__(self):
        self.stats = {
            'total_requests': 0,
            'successful': 0,
            'rate_limited': 0,
            'blocked': 0,
            'captcha': 0,
            'errors': 0,
            'proxy_failures': defaultdict(int)
        }
        
    def log_request(self, status, proxy=None):
        self.stats['total_requests'] += 1
        
        if status == 200:
            self.stats['successful'] += 1
        elif status == 429:
            self.stats['rate_limited'] += 1
        elif status == 403:
            self.stats['blocked'] += 1
            if proxy:
                self.stats['proxy_failures'][proxy] += 1
                
    def get_success_rate(self):
        if self.stats['total_requests'] == 0:
            return 0
        return (self.stats['successful'] / self.stats['total_requests']) * 100
        
    def print_report(self):
        print(f"\n=== 스크래핑 보고서 ===")
        print(f"총 요청: {self.stats['total_requests']}")
        print(f"성공률: {self.get_success_rate():.2f}%")
        print(f"요청 속도 제한: {self.stats['rate_limited']}")
        print(f"차단됨: {self.stats['blocked']}")
        print(f"CAPTCHA: {self.stats['captcha']}")
        
        if self.stats['proxy_failures']:
            print(f"\n문제가 있는 프록시:")
            for proxy, count in sorted(
                self.stats['proxy_failures'].items(), 
                key=lambda x: x[1], 
                reverse=True
            )[:5]:
                print(f"  {proxy}: {count} 실패")

# 사용 예
metrics = ScraperMetrics()

for url in urls:
    response = safe_request(url, session)
    if response:
        metrics.log_request(response.status_code, current_proxy)
    
metrics.print_report()

최적의 지표: 성공률 95% 이상은 훌륭한 결과입니다. 80-95%는 수용 가능하지만 개선할 여지가 있습니다. 80% 이하인 경우 전략을 재검토하세요: 너무 공격적인 요청 속도 제한, 나쁜 프록시 또는 지문 문제일 수 있습니다.

결론

차단 방지 전략은 복잡하고 지속적으로 발전하는 환경에서 효과적으로 작동해야 합니다. 위에서 언급한 기술과 도구를 활용하여 보다 안전하고 효과적인 자동화 작업을 수행할 수 있습니다.

```