Scrapy는 웹 스크래핑을 위한 가장 강력한 Python 프레임워크 중 하나이지만, 올바른 프록시 설정이 없으면 파서가 몇 분 안에 차단될 수 있습니다. 이 가이드에서는 Scrapy에 프록시를 통합하는 모든 방법을 보여드리겠습니다: 가장 간단한 설정부터 자동 오류 처리가 포함된 IP 주소 회전의 고급 방법까지.
이 자료는 대형 전자상거래 플랫폼과 보호된 사이트의 실제 파싱 경험을 바탕으로 작성되었습니다. 즉시 프로젝트에 사용할 수 있는 코드 예제를 제공합니다.
왜 Scrapy는 프록시 없이 차단되는가
현대의 웹사이트는 파싱에 대한 다단계 보호를 사용합니다. User-Agent와 요청 간 지연을 설정하더라도, IP 주소는 여러 가지 징후로 자동화를 드러냅니다:
- 요청 빈도: 하나의 IP가 분당 100회 이상의 요청을 하는 것은 명백한 봇의 징후입니다.
- 행동 패턴: 무작위 전환 없이 페이지를 순차적으로 탐색합니다.
- JavaScript 없음: Scrapy는 JS를 실행하지 않으므로 쉽게 감지됩니다.
- 지리적 위치: 자택 네트워크 대신 데이터 센터에서 접근합니다.
결과적으로 IP 차단이 몇 시간 또는 며칠 동안 발생합니다. 특히 공격적인 보호를 사용하는 마켓플레이스(아마존, Wildberries, Ozon), 소셜 미디어 및 Cloudflare가 있는 사이트에서 그렇습니다. 프록시는 여러 IP 주소 간에 요청을 분산시켜 이 문제를 해결합니다.
중요: 프록시를 사용하더라도 속도 제한을 준수해야 합니다. 권장 속도: 하나의 IP당 초당 1-3 요청. 고속 파싱을 위해 50개 이상의 프록시 풀을 사용하여 회전하세요.
Scrapy에서 프록시 기본 설정
가장 간단한 방법은 스파이더 설정에 프록시를 직접 지정하는 것입니다. 이 방법은 테스트 또는 하나의 프록시 서버로 소량의 데이터를 파싱하는 데 적합합니다.
방법 1: Request의 meta를 통해
import scrapy
class MySpider(scrapy.Spider):
name = 'example'
start_urls = ['https://example.com']
def start_requests(self):
proxy = 'http://username:password@proxy.example.com:8080'
for url in self.start_urls:
yield scrapy.Request(
url=url,
callback=self.parse,
meta={'proxy': proxy}
)
def parse(self, response):
# 파싱 로직
self.log(f'Scraped {response.url} via {response.meta["proxy"]}')
프록시 형식은 프로토콜 및 인증 방법에 따라 다릅니다:
http://proxy.example.com:8080— 인증 없음http://user:pass@proxy.example.com:8080— 로그인/비밀번호 포함socks5://user:pass@proxy.example.com:1080— SOCKS5 프록시
방법 2: settings.py에서 전역 설정
# settings.py
# 모든 요청에 대한 HTTP 프록시
HTTPPROXY_ENABLED = True
HTTPPROXY_AUTH_ENCODING = 'utf-8'
# 환경 변수를 통한 설정
HTTP_PROXY = 'http://username:password@proxy.example.com:8080'
HTTPS_PROXY = 'http://username:password@proxy.example.com:8080'
이 방법은 빠른 테스트에 편리하지만 프로덕션에는 적합하지 않습니다: IP 회전이 없고, 프록시가 실패하면 전체 파서가 중지되며, 서로 다른 사이트에 대해 서로 다른 프록시를 사용할 수 없습니다.
커스텀 프록시 미들웨어 만들기
프로덕션 파싱을 위해서는 프록시 풀을 관리하고 오류를 처리하며 IP를 자동으로 회전하는 자체 미들웨어가 필요합니다. 다음은 기본 구현입니다:
# middlewares.py
import random
from scrapy import signals
from scrapy.exceptions import NotConfigured
class RandomProxyMiddleware:
def __init__(self, proxy_list):
self.proxy_list = proxy_list
@classmethod
def from_crawler(cls, crawler):
# 설정에서 프록시 목록을 로드합니다
proxy_list = crawler.settings.getlist('PROXY_LIST')
if not proxy_list:
raise NotConfigured('PROXY_LIST not configured')
return cls(proxy_list)
def process_request(self, request, spider):
# 풀에서 무작위 프록시를 선택합니다
proxy = random.choice(self.proxy_list)
request.meta['proxy'] = proxy
spider.logger.info(f'Using proxy: {proxy}')
def process_exception(self, request, exception, spider):
# 오류가 발생하면 다른 프록시를 시도합니다
proxy = random.choice(self.proxy_list)
request.meta['proxy'] = proxy
spider.logger.warning(
f'Proxy error, switching to: {proxy}'
)
return request
이제 settings.py에서 미들웨어 사용을 설정합니다:
# settings.py
# 프록시 목록 (파일이나 API에서 로드 가능)
PROXY_LIST = [
'http://user1:pass1@proxy1.example.com:8080',
'http://user2:pass2@proxy2.example.com:8080',
'http://user3:pass3@proxy3.example.com:8080',
# ... 효율적인 회전을 위해 50개 이상의 프록시를 추가하세요
]
# 미들웨어 연결
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.RandomProxyMiddleware': 350,
'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 400,
}
# 오류 발생 시 재시도 횟수
RETRY_TIMES = 3
RETRY_HTTP_CODES = [500, 502, 503, 504, 408, 429]
프록시 회전: 세 가지 작업 방법
무작위 프록시 선택(위의 예와 같이)은 가장 간단하지만 가장 효율적인 방법은 아닙니다. 다양한 시나리오에 대한 세 가지 회전 전략을 살펴보겠습니다.
방법 1: 라운드 로빈 (순차 회전)
프록시는 순환적으로 선택됩니다. 부하를 고르게 분산하는 데 적합합니다:
class RoundRobinProxyMiddleware:
def __init__(self, proxy_list):
self.proxy_list = proxy_list
self.current_index = 0
@classmethod
def from_crawler(cls, crawler):
proxy_list = crawler.settings.getlist('PROXY_LIST')
return cls(proxy_list)
def process_request(self, request, spider):
# 순환적으로 다음 프록시를 가져옵니다
proxy = self.proxy_list[self.current_index]
self.current_index = (self.current_index + 1) % len(self.proxy_list)
request.meta['proxy'] = proxy
방법 2: 블랙리스트를 이용한 스마트 회전
문제 있는 프록시를 추적하고 일시적으로 회전에서 제외합니다:
import time
from collections import defaultdict
class SmartProxyMiddleware:
def __init__(self, proxy_list):
self.proxy_list = proxy_list
self.proxy_errors = defaultdict(int)
self.blacklist = set()
self.blacklist_timeout = 300 # 5분
self.blacklist_time = {}
@classmethod
def from_crawler(cls, crawler):
proxy_list = crawler.settings.getlist('PROXY_LIST')
return cls(proxy_list)
def get_working_proxies(self):
# 블랙리스트에서 타임아웃이 만료된 프록시를 제거합니다
current_time = time.time()
expired = [
proxy for proxy, ban_time in self.blacklist_time.items()
if current_time - ban_time > self.blacklist_timeout
]
for proxy in expired:
self.blacklist.discard(proxy)
self.proxy_errors[proxy] = 0
# 작동하는 프록시를 반환합니다
return [p for p in self.proxy_list if p not in self.blacklist]
def process_request(self, request, spider):
working_proxies = self.get_working_proxies()
if not working_proxies:
spider.logger.error('모든 프록시가 블랙리스트에 등록되었습니다!')
return
proxy = random.choice(working_proxies)
request.meta['proxy'] = proxy
def process_response(self, request, response, spider):
# 차단을 받으면 블랙리스트에 추가합니다
if response.status in [403, 429, 503]:
proxy = request.meta.get('proxy')
self.proxy_errors[proxy] += 1
if self.proxy_errors[proxy] >= 3:
self.blacklist.add(proxy)
self.blacklist_time[proxy] = time.time()
spider.logger.warning(
f'프록시 {proxy}가 {self.blacklist_timeout}s 동안 블랙리스트에 등록되었습니다.'
)
return response
방법 3: 공급자의 API를 통한 회전
많은 프록시 공급자(예: 주거용 프록시)는 요청 시 IP를 자동으로 변경하는 회전 엔드포인트를 제공합니다:
# settings.py
# 자동 회전이 가능한 단일 엔드포인트
ROTATING_PROXY = 'http://username:password@rotating.proxy.com:8080'
# 간단한 미들웨어
class RotatingProxyMiddleware:
def __init__(self, proxy):
self.proxy = proxy
@classmethod
def from_crawler(cls, crawler):
proxy = crawler.settings.get('ROTATING_PROXY')
return cls(proxy)
def process_request(self, request, spider):
# 하나의 URL이지만 각 요청은 새로운 IP로 전송됩니다
request.meta['proxy'] = self.proxy
이는 프로덕션에 가장 편리한 방법입니다: 프록시 풀을 관리할 필요가 없고, 공급자가 IP 품질을 모니터링하고 문제 있는 IP를 교체합니다. 특히 주거용 프록시와 함께 사용할 때 효과적이며, IP 풀은 수백만 개의 주소에 이를 수 있습니다.
인증: 로그인/비밀번호 vs IP 화이트리스트
프록시 공급자는 두 가지 인증 방법을 제공합니다. 선택은 연결 속도와 설정의 편리성에 영향을 미칩니다.
User:Pass 인증
로그인과 비밀번호는 프록시 URL에 전달됩니다. Scrapy는 이를 자동으로 HTTP 헤더 Proxy-Authorization로 변환합니다:
proxy = 'http://username:password@proxy.example.com:8080'
request.meta['proxy'] = proxy
# Scrapy가 자동으로 추가할 헤더:
# Proxy-Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
장점: 모든 IP에서 작동하며, 코드에서 프록시를 쉽게 변경할 수 있습니다.
단점: 각 요청에 약간의 오버헤드가 발생합니다 (~50-100ms), 코드에 자격 증명이 노출됩니다.
IP 화이트리스트 인증
서버의 IP를 공급자의 화이트리스트에 추가하면 인증이 필요하지 않습니다:
proxy = 'http://proxy.example.com:8080' # 로그인/비밀번호 없음
request.meta['proxy'] = proxy
장점: 50-100ms 더 빠르며, 안전합니다 (코드에 자격 증명이 없음).
단점: 특정 IP에서만 작동하며, 서버 변경 시 화이트리스트를 업데이트해야 합니다.
프로덕션에 대한 권장 사항:
전용 서버(AWS, Google Cloud, Hetzner)에서 파싱할 때는 IP 화이트리스트를 사용하세요. 로컬 머신에서 개발 및 테스트할 때는 user:pass 인증을 사용하세요.
오류 처리 및 자동 IP 변경
품질이 좋은 프록시를 사용하더라도 오류가 발생할 수 있습니다: 타임아웃, 연결 거부, 차단. 오류를 올바르게 처리하는 것은 파서의 안정적인 작동에 매우 중요합니다.
HTTP 상태 처리
class ProxyMiddleware:
def process_response(self, request, response, spider):
# 프록시를 변경하고 재시도해야 하는 코드
ban_codes = [403, 407, 429, 503]
if response.status in ban_codes:
proxy = request.meta.get('proxy')
spider.logger.warning(
f'{proxy}에서 {response.status}를 받았습니다. 재시도 중...'
)
# 새로운 프록시로 재시도하도록 표시합니다
request.meta['dont_retry'] = False
request.meta['proxy'] = self.get_new_proxy()
return request
return response
네트워크 예외 처리
from twisted.internet.error import TimeoutError, ConnectionRefusedError
from scrapy.exceptions import IgnoreRequest
class ProxyMiddleware:
def process_exception(self, request, exception, spider):
# 프록시 연결 오류
proxy_errors = (
TimeoutError,
ConnectionRefusedError,
ConnectionLost,
)
if isinstance(exception, proxy_errors):
proxy = request.meta.get('proxy')
spider.logger.error(
f'프록시 {proxy} 연결 실패: {exception}'
)
# 프록시를 변경하고 다시 시도합니다
request.meta['proxy'] = self.get_new_proxy()
return request
# 다른 오류에 대해서는 기본 처리를 사용합니다
return None
콘텐츠에 의한 차단 감지
일부 웹사이트는 HTTP 200을 반환하지만 CAPTCHA 또는 차단 페이지를 표시합니다:
class ProxyMiddleware:
def process_response(self, request, response, spider):
# 콘텐츠에서 차단 징후
ban_indicators = [
'captcha',
'access denied',
'blocked',
'unusual traffic',
'robot check',
]
body_text = response.text.lower()
if any(indicator in body_text for indicator in ban_indicators):
spider.logger.warning(
f'{request.meta.get("proxy")}에서 차단 페이지가 감지되었습니다.'
)
# 프록시를 변경하고 재시도합니다
request.meta['proxy'] = self.get_new_proxy()
return request
return response
Scrapy에 적합한 프록시 유형 선택하기
프록시 유형 선택은 대상 웹사이트, 예산 및 필요한 파싱 속도에 따라 달라집니다. 주요 옵션에 대한 비교는 다음과 같습니다:
| 프록시 유형 | 속도 | 비용 | 사용 시기 |
|---|---|---|---|
| 데이터 센터 프록시 | 높음 (50-200ms) | 낮음 ($1-3/IP) | 보호가 없는 간단한 웹사이트, API, 내부 도구 |
| 주거용 프록시 | 중간 (300-800ms) | 중간 ($5-15/GB) | 전자상거래, 소셜 미디어, Cloudflare가 있는 사이트, 지리적 타겟팅 |
| 모바일 프록시 | 낮음 (500-1500ms) | 높음 ($50-150/IP) | 모바일 애플리케이션, Instagram, TikTok, 최대 보호 |
선택에 대한 권장 사항
마켓플레이스 파싱을 위한 경우 (Amazon, Wildberries, Ozon, AliExpress) — 주거용 프록시만 사용하세요. 이 사이트들은 데이터 센터를 공격적으로 차단합니다. 회전과 지리적 타겟팅이 필요합니다 (예: Wildberries를 위한 러시아 IP).
뉴스 사이트, 블로그, 포럼 파싱을 위한 경우 — 데이터 센터 프록시가 적합합니다. 보호가 최소화되어 있으며, 속도와 낮은 트래픽 비용이 중요합니다.
Cloudflare가 있는 사이트 파싱을 위한 경우 — 주거용 프록시가 필수입니다. Cloudflare의 데이터 센터는 거의 즉시 감지합니다. Scrapy에 cloudscraper 라이브러리를 추가하여 JS 챌린지를 우회하세요.
Google 검색, SEO 도구 파싱을 위한 경우 — 지리적 타겟팅이 가능한 주거용 프록시를 사용하세요. Google은 국가와 도시마다 다른 결과를 보여줍니다.
조언: 테스트를 위해 10개의 주거용 프록시 풀로 시작하세요. 차단을 받으면 풀을 50-100 IP로 늘리세요. 고속 파싱(분당 1000회 이상의 요청)을 위해 10,000개 이상의 IP 풀을 가진 회전 엔드포인트를 사용하세요.
고급 기술: 세션 및 스티키 IP
일부 웹사이트를 파싱할 때는 세션 전체에 걸쳐 동일한 IP를 유지해야 합니다 (인증, 장바구니, 다단계 양식). Scrapy에서 스티키 세션을 구현하는 방법은 다음과 같습니다.
하나의 도메인에 대한 스티키 IP
from urllib.parse import urlparse
class StickyProxyMiddleware:
def __init__(self, proxy_list):
self.proxy_list = proxy_list
# 사전: 도메인 -> 프록시
self.domain_proxy_map = {}
@classmethod
def from_crawler(cls, crawler):
proxy_list = crawler.settings.getlist('PROXY_LIST')
return cls(proxy_list)
def process_request(self, request, spider):
# URL에서 도메인을 추출합니다
domain = urlparse(request.url).netloc
# 이 도메인에 이미 프록시가 있으면 사용합니다
if domain in self.domain_proxy_map:
proxy = self.domain_proxy_map[domain]
else:
# 그렇지 않으면 새로 선택하고 기억합니다
proxy = random.choice(self.proxy_list)
self.domain_proxy_map[domain] = proxy
spider.logger.info(f'{domain}에 {proxy}를 할당했습니다.')
request.meta['proxy'] = proxy
세션 타임아웃이 있는 스티키 IP
더 발전된 옵션: 프록시는 특정 시간 동안 도메인에 연결됩니다 (예: 10분), 이후 변경됩니다:
import time
from urllib.parse import urlparse
class SessionProxyMiddleware:
def __init__(self, proxy_list, session_timeout=600):
self.proxy_list = proxy_list
self.session_timeout = session_timeout # 10분
# 사전: 도메인 -> (프록시, 생성 시간)
self.sessions = {}
@classmethod
def from_crawler(cls, crawler):
proxy_list = crawler.settings.getlist('PROXY_LIST')
timeout = crawler.settings.getint('PROXY_SESSION_TIMEOUT', 600)
return cls(proxy_list, timeout)
def get_proxy_for_domain(self, domain):
current_time = time.time()
# 활성 세션이 있는지 확인합니다
if domain in self.sessions:
proxy, created_at = self.sessions[domain]
# 세션이 만료되지 않았다면 동일한 프록시를 사용합니다
if current_time - created_at < self.session_timeout:
return proxy
# 새 프록시로 새로운 세션을 생성합니다
new_proxy = random.choice(self.proxy_list)
self.sessions[domain] = (new_proxy, current_time)
return new_proxy
def process_request(self, request, spider):
domain = urlparse(request.url).netloc
proxy = self.get_proxy_for_domain(domain)
request.meta['proxy'] = proxy
쿠키 미들웨어와의 통합
완전한 세션을 위해서는 프록시와 쿠키를 동기화해야 합니다. Scrapy는 각 도메인에 대해 쿠키를 별도로 저장하지만 프록시를 변경할 때는 쿠키를 지워야 합니다:
# settings.py
# 쿠키 미들웨어 활성화
COOKIES_ENABLED = True
COOKIES_DEBUG = False
# 프록시와 쿠키 동기화를 위한 미들웨어
class ProxyCookieMiddleware:
def process_request(self, request, spider):
# 현재 프록시를 가져옵니다
current_proxy = request.meta.get('proxy')
# 프록시가 변경되면 쿠키를 지웁니다
previous_proxy = request.meta.get('previous_proxy')
if previous_proxy and previous_proxy != current_proxy:
# 이 도메인에 대한 쿠키를 지웁니다
jar = spider.crawler.engine.downloader.middleware.middlewares[0].jars
domain = urlparse(request.url).netloc
if domain in jar:
jar[domain].clear()
spider.logger.info(f'{domain}의 쿠키를 지웠습니다.')
request.meta['previous_proxy'] = current_proxy
결론
Scrapy에서 프록시를 올바르게 설정하는 것은 차단 없이 안정적인 파싱의 기초입니다. 기본 통합부터 고급 회전 및 세션 관리 기술까지 모든 주요 측면을 살펴보았습니다.
주요 요점:
- 프로덕션에서는 스마트 회전과 문제 있는 IP의 블랙리스트가 포함된 커스텀 미들웨어를 사용하세요.
- 모든 유형의 오류를 처리하세요: HTTP 상태, 네트워크 예외, 콘텐츠에 의한 차단.
- 작업에 맞는 프록시 유형을 선택하세요: 간단한 웹사이트에는 데이터 센터, 보호된 사이트에는 주거용.
- 인증이 필요한 사이트에는 도메인에 프록시를 연결하는 스티키 세션을 사용하세요.
- 10-50개의 프록시 풀로 시작하고 부하 증가 시 확장하세요.
보호된 사이트(마켓플레이스, 소셜 미디어, Cloudflare가 있는 사이트)를 파싱할 계획이라면 주거용 프록시를 사용하는 것을 권장합니다 — 최대한의 익명성과 최소한의 차단 위험을 보장합니다. 고속 파싱을 위해서는 회전 엔드포인트와 10,000개 이상의 IP 주소를 가진 공급자를 선택하세요.
이 기사의 모든 코드 예제는 Scrapy 2.x에서 테스트되었으며 프로덕션에서 사용할 준비가 되어 있습니다. 자신의 작업에 맞게 조정하고 프로젝트의 성장에 따라 확장하세요.