Scrapy captcha 미들웨어: 스파이더 안에서 captcha 풀기 (2026)
Scrapy는 요청과 콜백을 중심으로 만들어져 있어서, captcha가 깔끔하게 들어맞지 않습니다. 이를 다루는 깔끔한 방법은 다운로더 미들웨어입니다. 모든 응답을 보고, 챌린지를 알아차리고, 토큰을 받고, 스파이더가 그 토큰으로 폼을 제출하게 합니다. 이 가이드는 PopularCaptcha 페이지를 위해 그 미들웨어를 만듭니다. Scarnox는 이 유형을 오늘 프록시리스 방식으로 풉니다.
무료 체험 크레딧으로 카드 없이 시작하고, 다른 곳에 적용하기 전에 테스트 페이지에서 코드를 먼저 시험해 보세요.
미들웨어의 작동 방식
미들웨어는 모든 응답에서 실행됩니다.
- 응답에
data-sitekey가 없으면 응답을 그대로 통과시킵니다. - 있으면 미들웨어가 captcha를 풀고, 같은 요청의 사본을 토큰을
meta에 담아 반환합니다. - Scrapy가 그 요청을 다시 가져옵니다. 미들웨어는
meta에 있는 토큰을 보고 응답을 통과시킵니다. - 스파이더는
meta에서 토큰을 읽고FormRequest.from_response로 폼을 제출합니다.
풀이 호출은 deferToThread를 통해 스레드에서 실행되므로 Scrapy의 reactor를 블로킹하지 않습니다. captcha를 푸는 동안에도 다른 요청은 계속 진행됩니다.
프로젝트 설정
키는 코드가 아니라 환경 변수에 넣으세요.
# settings.py
import os
SCARNOX_API_KEY = os.environ["SCARNOX_API_KEY"]
SCARNOX_TIMEOUT = 185
DOWNLOADER_MIDDLEWARES = {
"myproject.middlewares.PopularCaptchaMiddleware": 543,
}
ROBOTSTXT_OBEY = True
DOWNLOAD_DELAY = 3
CONCURRENT_REQUESTS_PER_DOMAIN = 1
다운로드 지연과 도메인당 단일 동시성은 일부러 넣은 것입니다. 사이트의 부하를 낮게 유지하고, 마주치는 챌린지 수를 줄여 줍니다.
미들웨어
# myproject/middlewares.py
import requests
from scrapy import Request
from scrapy.http import Response
from twisted.internet.threads import deferToThread
API_URL = "https://api.scarnox.com/api/tasks/create"
class PopularCaptchaMiddleware:
def __init__(self, api_key: str, timeout: int):
self.api_key = api_key
self.timeout = timeout
@classmethod
def from_crawler(cls, crawler):
return cls(
api_key=crawler.settings.get("SCARNOX_API_KEY"),
timeout=crawler.settings.getint("SCARNOX_TIMEOUT", 185),
)
def _solve(self, sitekey: str, page_url: str) -> str:
r = requests.post(
API_URL,
headers={"Authorization": f"Bearer {self.api_key}"},
json={
"captcha_type": "PopularCaptchaTaskProxyless",
"site_key": sitekey,
"site_url": page_url,
},
timeout=self.timeout, # 리액터가 아니라 워커 스레드에서 실행됩니다
)
data = r.json()
if data.get("errorId") != 0:
raise RuntimeError(f"{data.get('errorCode')}: {data.get('errorDescription')}")
return data["solution"]["token"]
def process_response(self, request: Request, response: Response, spider):
# 이미 풀었거나, 우리가 직접 만든 폼 제출이라면 통과시킵니다.
if request.meta.get("captcha_token") or request.meta.get("skip_captcha"):
return response
sitekey = response.css("[data-sitekey]::attr(data-sitekey)").get()
if not sitekey:
return response
spider.logger.info("PopularCaptcha on %s, solving", response.url)
def retry_with_token(token: str) -> Request:
# Request를 반환하면 다시 스케줄됩니다. dont_filter로 중복 필터를 피합니다.
return request.replace(
dont_filter=True,
meta={**request.meta, "captcha_token": token},
)
return deferToThread(self._solve, sitekey, response.url).addCallback(retry_with_token)
Scrapy는 process_response에서 Deferred를 반환하는 것을 지원합니다. 콜백은 토큰을 재시도 요청으로 바꾸고, Scrapy는 그것을 다른 요청과 똑같이 스케줄합니다.
스파이더
# myproject/spiders/search.py
import scrapy
from scrapy.http import FormRequest
class SearchSpider(scrapy.Spider):
name = "search"
start_urls = ["https://search.example.com/search?q=web+scraping"]
def parse(self, response):
token = response.meta.get("captcha_token")
if token and response.css("[data-sitekey]"):
# Scrapy가 폼의 자체 필드를 채우고, 우리는 토큰을 추가합니다.
yield FormRequest.from_response(
response,
formdata={
"h-captcha-response": token,
"g-recaptcha-response": token,
},
callback=self.parse_results,
meta={"skip_captcha": True},
dont_filter=True,
)
return
yield from self.parse_results(response)
def parse_results(self, response):
for item in response.css("div.result"):
yield {
"title": item.css("h3::text").get(),
"url": item.css("a::attr(href)").get(),
}
셀렉터는 자리표시자입니다. 스크래핑하는 사이트의 실제 셀렉터로 바꾸세요.
skip_captcha 플래그가 미들웨어가 폼 제출을 다시 풀지 않도록 막는 장치입니다. 이것이 없으면 제출할 때마다 풀이 비용이 추가로 듭니다.
실행하기
export SCARNOX_API_KEY=scarnox_YOURAPIKEY
scrapy crawl search -O results.json
로그를 지켜보세요. PopularCaptcha on ..., solving 같은 줄은 챌린지가 발견되었고 토큰을 요청했다는 뜻입니다. 모든 페이지에서 그 줄이 보인다면 속도를 낮추세요. 잦은 챌린지는 사이트가 크롤을 알아챘다는 신호입니다.
토큰 타이밍
PopularCaptcha 토큰은 약 2분 안에 만료됩니다. 미들웨어는 토큰이 도착하자마자 재시도를 요청하고, 스파이더는 같은 콜백 체인 안에서 제출합니다. 체인을 짧게 유지하세요. 토큰과 폼 제출 사이에 느린 다운로드나 긴 처리를 넣지 마세요. 이 시간 창에 대한 자세한 내용은 captcha 토큰 만료를 참고하세요.
오류
풀이가 실패하면 스레드 안에서 RuntimeError가 발생합니다. Scrapy는 이를 실패한 요청으로 기록합니다. 실패한 풀이는 요금이 청구되지 않으므로, 재시도 비용은 낮습니다. 한도가 있는 재시도 정책이 필요하다면, Scrapy 내장 RetryMiddleware를 쓰거나 _solve를 짧은 루프로 감싸세요. 운영 환경에서 검증된 패턴은 captcha 솔버 오류 처리에 정리되어 있습니다.
책임 있게 스크래핑하기
- robots.txt를 따르세요. 위의
ROBOTSTXT_OBEY = True가 이를 처리합니다. 켜 두세요. - 지연을 길게 유지하세요. 챌린지는 사이트가 속도를 늦춰 달라고 말하는 신호입니다. 더 빨리 끝내려고 지연을 우회하지 마세요.
- 크롤하기 전에 이용 약관을 읽으세요. 일부 사이트는 자동화된 접근을 아예 금지합니다.
- 개인 데이터를 조심스럽게 다루세요. 필요한 필드만 저장하고, 보관하는 것은 보호하세요.
법적 배경은 웹 스크래핑은 합법인가를 참고하세요.
요금
Scarnox는 성공한 풀이마다 과금하며, 실패한 풀이는 무료입니다. 요율은 현재 요금을 확인하세요. 무료 체험 크레딧으로 카드 없이 시작하세요.