Scrapy (스파이더, 파이프라인, 비동기)
requests와 BeautifulSoup로 크롤링 첫 결과를 손에 잡고 나면 거의 반드시 마주치는 한계가 있다. 페이지가 100개를 넘어가는 순간 스크립트는 갑자기 느려지고, 실패 재시도·중복 제거·CSV 저장 같은 부가 코드가 본체보다 길어지기 시작한다. 본 글은 그 한계를 산업 표준 프레임워크 차원에서 풀어내는 도구인 Scrapy를 스파이더 구조, 파이프라인을 통한 수집·가공·저장 분리, 비동기 처리 기반의 성능 차이라는 세 축으로 정리한다(출처: Scrapy 공식 문서). 제가 4학년 비정형 데이터 처리 수업에서 BS4로 학교 채용 공지 100건을 긁어 보다가 단순 직렬 처리만으로 6분 가까이 걸리는 모습을 보고 "이건 도구의 한계가 아니라 구조의 한계"라는 인상을 손끝으로 받아들였고, 같은 작업을 Scrapy로 옮기자 60초 안쪽으로 끝나는 결과를 처음 확인했다.

Scrapy의 스파이더(Spider) 구조와 동작 흐름
Scrapy를 한 문장으로 줄이면 "크롤링을 위한 풀스택 프레임워크"이며, 그 중심에 스파이더(Spider)라는 클래스가 있다. 여기서 스파이더란 어떤 URL을 어떤 순서로 방문하고 그 페이지에서 무엇을 추출할지를 정의한 단위 단위의 크롤러 클래스를 가리킨다. BS4가 "한 페이지를 어떻게 파싱할지"만 정의하는 도구라면, Scrapy의 스파이더는 "어떤 페이지들을 차례로 방문할지"까지 같이 정의한다는 점이 결정적 차이다.
가장 작은 형태의 Scrapy 스파이더는 다음과 같다. 설치는 한 줄로 끝난다.
pip install scrapy
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = ["https://quotes.toscrape.com/"]
def parse(self, response):
for quote in response.css("div.quote"):
yield {
"text": quote.css("span.text::text").get(),
"author": quote.css("small.author::text").get(),
}
next_page = response.css("li.next a::attr(href)").get()
if next_page is not None:
yield response.follow(next_page, callback=self.parse)
여기서 response.follow는 다음 페이지 링크를 추적해 같은 parse 메서드를 재귀적으로 호출하는 표준 패턴이다. BS4와 같은 CSS 선택자(div.quote, span.text::text)를 그대로 쓸 수 있고, ::text·::attr(href) 같은 Scrapy 자체 확장 문법으로 텍스트·속성 추출 코드가 한층 짧아진다. 실행은 명령어 한 줄로 끝나고, 그 자체로 결과가 JSON 파일에 저장된다.
scrapy runspider quotes.py -O quotes.json
다만 Scrapy가 항상 좋은 선택은 아니다. 솔직히 제 경험상 페이지가 1~10개 수준이거나 한 사이트만 빠르게 긁고 끝낼 작업이면 BS4가 압도적으로 빠르다. Scrapy는 프로젝트 구조(스파이더·파이프라인·미들웨어·설정)를 미리 잡아 두어야 효과가 나오는 도구이며, 1회성 작업에선 그 구조 자체가 오버헤드다. 즉 BS4와 Scrapy는 같은 선상의 경쟁자가 아니라 다른 단계의 도구다.
파이프라인(Pipeline)과 미들웨어 (수집·가공·저장 분리)
Scrapy의 두 번째 차별점은 수집된 데이터를 처리하는 흐름을 코드 안에 박지 않고 별도 컴포넌트로 분리한다는 점이다. 핵심이 아이템 파이프라인(Item Pipeline)이며, 여기서 파이프라인이란 스파이더가 추출한 데이터를 정제·검증·저장 등 여러 단계로 차례로 흘려보내는 처리 사슬을 가리킨다.
가장 자주 쓰는 파이프라인 두 가지가 중복 제거와 데이터 검증이다. 다음은 단순한 중복 제거 파이프라인이다.
from scrapy.exceptions import DropItem
class DedupePipeline:
def __init__(self):
self.seen_urls = set()
def process_item(self, item, spider):
if item["url"] in self.seen_urls:
raise DropItem(f"중복: {item['url']}")
self.seen_urls.add(item["url"])
return item
settings.py에 ITEM_PIPELINES = {"myproj.pipelines.DedupePipeline": 300} 한 줄을 추가하면 모든 스파이더가 자동으로 이 파이프라인을 통과시킨다. 여기서 숫자 300은 파이프라인 실행 우선순위로, 낮을수록 먼저 실행된다. 검증·중복 제거·필드 정제·DB 저장을 별도 파이프라인으로 쪼개면 스파이더 코드는 "데이터 추출"이라는 단일 책임에만 집중할 수 있다는 점이 구조적 이득이다.
미들웨어(Middleware)는 요청·응답 자체를 가공하는 또 한 층이다. User-Agent 회전, 프록시 적용, robots.txt 자동 검사, 재시도 정책 같은 횡단 관심사(cross-cutting concern)가 모두 미들웨어로 처리된다. Scrapy는 기본으로 RobotsTxtMiddleware가 활성화돼 있어 settings.py의 ROBOTSTXT_OBEY = True 한 줄만으로 모든 요청 전 robots.txt를 자동 점검한다(출처: Scrapy 미들웨어 문서).
흔한 오해 하나를 짚으면, Scrapy를 쓰면 robots.txt가 알아서 다 지켜진다는 인식이 있다. 그러나 기본 설정만으론 충분치 않다. 실서비스 크롤러는 DOWNLOAD_DELAY(요청 간 지연), AUTOTHROTTLE_ENABLED(서버 응답 시간 기반 자동 속도 조절), CONCURRENT_REQUESTS_PER_DOMAIN(도메인당 동시 요청 수)을 함께 설정해야 비로소 "예의 있는 크롤러"가 된다. 솔직히 이건 시험엔 자주 안 나오지만 실무에서 가장 자주 문제가 되는 지점이다.
비동기(Twisted) 기반 성능 (동시성, 처리량)
Scrapy의 마지막 차별점이 비동기 처리 엔진이다. 내부적으로 Twisted라는 이벤트 기반 네트워킹 프레임워크 위에서 동작하며, 한 번에 수십
수백 개의 요청을 동시에 처리한다(출처: Twisted 공식 문서). 여기서 비동기란 한 요청의 응답을 기다리는 동안 다른 요청을 동시에 처리해 CPU와 네트워크 자원을 놀리지 않게 하는 처리 방식을 가리킨다. BS4·requests 조합이 한 번에 한 요청만 직렬로 처리하는 동기 방식이라면, Scrapy는 한 사이클 안에서 16
32개 요청을 동시에 흘려보내는 게 기본값이다.
직관적인 차이는 다음 표가 보여 준다. 100페이지를 긁을 때 평균 응답 시간이 0.5초라고 가정하면, 동기 직렬 처리는 100 × 0.5초 = 50초가 걸리지만, 동시 16 요청 비동기 처리는 약 100/16 × 0.5초 ≒ 3.2초로 끝난다. 실측에서도 비슷한 비율이 나오며, 본인이 학교 수업 과제에서 측정한 BS4 직렬 vs Scrapy 비동기의 처리 시간 차이도 거의 이 비율이었다.
# settings.py — 운영 시 자주 쓰는 설정
CONCURRENT_REQUESTS = 16 # 전체 동시 요청 수
CONCURRENT_REQUESTS_PER_DOMAIN = 8 # 도메인당 동시 요청 수
DOWNLOAD_DELAY = 0.5 # 요청 간 지연(초)
AUTOTHROTTLE_ENABLED = True # 응답 시간 기반 자동 속도 조절
ROBOTSTXT_OBEY = True # robots.txt 자동 준수
다만 비동기가 만능은 아니다. 사이트가 자바스크립트로 콘텐츠를 그리는 SPA(Single Page Application)라면 Scrapy 단독으론 빈 컨테이너만 받게 되고, 별도 도구(Playwright 통합 플러그인 scrapy-playwright)를 붙여야 한다. 또한 Twisted는 학습 곡선이 일반 asyncio보다 더 가팔라서 콜백·Deferred 개념에 익숙해지기까지 시간이 든다는 점도 입문자에겐 진입 장벽이다.
Scrapy를 정리하면 "다섯 페이지 긁기에는 과하고, 다섯만 페이지 긁기에는 적절한" 도구다. 본인의 채용 공지 100건 케이스처럼 페이지 규모가 두 자릿수를 넘어가고, 정제·중복 제거·저장이 한 스크립트 안에서 끝나야 하는 순간이 오면 BS4의 한계가 명확해진다. 그때 Scrapy의 스파이더·파이프라인·비동기 세 축이 정확히 그 빈자리를 메운다.