현대 Scrapy 개발자 가이드 (파트 1): 첫 번째 Spider 만들기

발행: (2025년 12월 17일 오전 03:41 GMT+9)
10 min read
원문: Dev.to

Source: Dev.to

Scrapy는 벅차 보일 수 있지만 – 반드시 그래야 하는 것은 아니다

거대한 강력한 프레임워크이며, 문서가 초보자에게는 압도적으로 느껴질 수 있습니다. 어디서부터 시작해야 할까요?

이 결정적인 가이드에서는 실제 다중 페이지 크롤링 스파이더를 단계별로 만드는 방법을 안내합니다. 빈 폴더에서 구조화된 JSON 파일까지 약 15 분 안에 완성할 수 있습니다. 최신 async/await 파이썬을 사용하고 다음 내용을 다룹니다:

  • 프로젝트 설정
  • 셀렉터 찾기
  • 링크 따라가기(크롤링)
  • 데이터 저장

우리는 books.toscrape.com“Fantasy” 카테고리를 크롤링하고, “Next” 버튼을 따라 해당 카테고리의 모든 페이지를 순회하며, 각 책의 링크를 따라가 name, price, URL을 스크랩하는 Scrapy 스파이더를 만들 것입니다. 결과는 깔끔한 books.json 파일에 저장됩니다.

최종 스파이더 구축

# tutorial/spiders/books.py
import scrapy


class BooksSpider(scrapy.Spider):
    name = "books"
    allowed_domains = ["toscrape.com"]

    # Starting URL (first page of the Fantasy category)
    start_urls = [
        "https://books.toscrape.com/catalogue/category/books/fantasy_19/index.html"
    ]

    # ------------------------------------------------------------------
    # Async version of start_requests – Scrapy will call this automatically
    # ------------------------------------------------------------------
    async def start_requests(self):
        for url in self.start_urls:
            yield scrapy.Request(url, callback=self.parse_listpage)

    # ------------------------------------------------------------------
    # Parse a category list page, follow book links and pagination
    # ------------------------------------------------------------------
    async def parse_listpage(self, response):
        # 1️⃣ Extract all book detail page URLs on the current list page
        product_urls = response.css("article.product_pod h3 a::attr(href)").getall()
        for url in product_urls:
            # `response.follow` correctly joins relative URLs
            yield response.follow(url, callback=self.parse_book)

        # 2️⃣ Follow the “Next” button, if it exists
        next_page_url = response.css("li.next a::attr(href)").get()
        if next_page_url:
            yield response.follow(next_page_url, callback=self.parse_listpage)

    # ------------------------------------------------------------------
    # Parse an individual book page and yield the scraped data
    # ------------------------------------------------------------------
    async def parse_book(self, response):
        yield {
            "name": response.css("h1::text").get(),
            "price": response.css("p.price_color::text").get(),
            "url": response.url,
        }

1️⃣ 프로젝트 설정

Prerequisite: Python 3.x가 설치되어 있어야 합니다.
의존성을 격리하기 위해 가상 환경을 사용할 것입니다. 표준 venv + pip 워크플로우 또는 최신 uv 도구를 사용할 수 있습니다.

프로젝트 폴더 및 가상 환경 만들기

# Create a new folder
mkdir scrapy_project
cd scrapy_project

# Option 1: Standard venv + pip
python -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate

# Option 2: Using uv (fast, modern alternative)
uv init

Scrapy 설치

# Option 1: pip
pip install scrapy

# Option 2: uv
uv add scrapy
# (If you used uv to create the env, it’s already activated)

2️⃣ Scrapy 프로젝트 보일러플레이트 생성

# '.'은 Scrapy에게 현재 폴더에 프로젝트를 생성하도록 알려줍니다
scrapy startproject tutorial .

이제 tutorial/ 패키지와 scrapy.cfg 파일이 보일 것입니다. tutorial/ 폴더에는 프로젝트 로직이 모두 들어 있습니다.

첫 번째 스파이더 생성

# tutorial/spiders/books.py 파일을 생성합니다
scrapy genspider books toscrape.com

3️⃣ 프로젝트 설정 조정

tutorial/settings.py 파일을 열고 다음과 같이 변경합니다.

robots.txt 비활성화 (테스트 사이트 전용)

# By default Scrapy obeys robots.txt – turn it off for this demo site
ROBOTSTXT_OBEY = False

크롤링 속도 향상 (테스트 사이트 전용)

# Increase concurrency and remove download delay
CONCURRENT_REQUESTS = 16
DOWNLOAD_DELAY = 0

⚠️ Warning: 이 설정은 테스트 사이트 toscrape.com에 대해서만 안전합니다. 실제 웹사이트를 스크래핑할 때는 항상 대상 사이트의 robots.txt를 준수하고, 적절한 동시성/지연 값을 사용하여 예의를 지키세요.

4️⃣ scrapy shell로 사이트 탐색하기

Scrapy 셸은 CSS 선택자를 찾기에 최적입니다.

Fantasy 카테고리 페이지에서 셸 열기

scrapy shell https://books.toscrape.com/catalogue/category/books/fantasy_19/index.html

이제 쿼리할 수 있는 response 객체가 있습니다.

모든 책 링크 찾기

>>> response.css("article.product_pod h3 a::attr(href)").getall()
[
    '../../../../the-host_979/index.html',
    '../../../../the-hunted_978/index.html',
    # …
]

“다음” 페이지 링크 찾기 (페이지네이션)

>>> response.css("li.next a::attr(href)").get()
'page-2.html'

단일 책 페이지에서 셸을 열어 데이터 선택자 가져오기

scrapy shell https://books.toscrape.com/catalogue/the-host_979/index.html
>>> response.css("h1::text").get()
'The Host'

>>> response.css("p.price_color::text").get()
'£25.82'

이제 스파이더에 필요한 모든 선택자를 확보했습니다.

5️⃣ Write the Spider

tutorial/spiders/books.py에 있는 보일러플레이트 코드를 이 가이드 상단에 표시된 최종 스파이더 코드(비동기 버전)로 교체합니다. 파일을 저장합니다.

6️⃣ 스파이더 실행 및 JSON으로 내보내기

scrapy crawl books -o books.json

Scrapy는 판타지 카테고리의 모든 페이지를 크롤링하고, 각 책 링크를 따라가며 이름, 가격, URL을 추출한 뒤 결과를 books.json에 기록합니다.

이렇게 하면 48개의 항목이 들어 있는 깔끔한 JSON 파일이 생성됩니다. 예시:

[
  {
    "name": "The Host",
    "price": "£25.82",
    "url": "https://books.toscrape.com/catalogue/the-host_979/index.html"
  },
  {
    "name": "The Hunted",
    "price": "£23.45",
    "url": "https://books.toscrape.com/catalogue/the-hunted_978/index.html"
  }
  // …
]

🎉 성공했습니다!

이제 완전하게 작동하는 비동기 Scrapy 스파이더가 있습니다:

  • 카테고리 페이지에서 시작합니다
  • 페이지네이션을 자동으로 따라갑니다
  • 각 제품 페이지를 방문합니다
  • 구조화된 데이터를 추출합니다
  • 모든 데이터를 깔끔한 JSON 파일에 저장합니다

자유롭게 실험해 보세요—필드를 더 추가하거나, 데이터를 데이터베이스에 저장하거나, 다른 사이트에 맞게 스파이더를 조정해 보세요. 즐거운 크롤링 되세요!

Scrapy 스파이더 개요

아래는 책 카탈로그를 크롤링하고, 페이지네이션을 따라가며, 각 제품 페이지에서 기본 정보를 추출하는 최소한의 async Scrapy 스파이더입니다.

import scrapy

class BooksSpider(scrapy.Spider):
    name = "books"
    start_urls = ["http://books.toscrape.com/"]

    # ------------------------------------------------------------------
    # 1️⃣  Spider entry point – called once when the spider starts.
    # ------------------------------------------------------------------
    async def start(self):
        # Yield the first request; its response will be handled by `parse_listpage`.
        yield scrapy.Request(self.url, callback=self.parse_listpage)

    # ------------------------------------------------------------------
    # 2️⃣  Parse the *category* (list) page.
    # ------------------------------------------------------------------
    async def parse_listpage(self, response):
        # 1️⃣  Get all product URLs from the current page.
        product_urls = response.css("article.product_pod h3 a::attr(href)").getall()

        # 2️⃣  Follow each product URL and send the response to `parse_book`.
        for url in product_urls:
            yield response.follow(url, callback=self.parse_book)

        # 3️⃣  Locate the “Next” page link (if any).
        next_page_url = response.css("li.next a::attr(href)").get()

        # 4️⃣  If a next page exists, follow it and recurse back to this method.
        if next_page_url:
            yield response.follow(next_page_url, callback=self.parse_listpage)

    # ------------------------------------------------------------------
    # 3️⃣  Parse the *product* (book) page.
    # ------------------------------------------------------------------
    async def parse_book(self, response):
        # Yield a dictionary containing the data we want to export.
        yield {
            "name":  response.css("h1::text").get(),
            "price": response.css("p.price_color::text").get(),
            "url":   response.url,
        }

Note: response.follow 은 상대 URL(e.g., page-2.html)을 자동으로 해결하므로 전체 URL을 직접 만들 필요가 없습니다.

스파이더 실행

  1. 프로젝트 루트에서 터미널을 엽니다.
  2. 스파이더를 실행합니다:
scrapy crawl books

Scrapy가 시작되는 것을 확인하고 로그에 48개 아이템이 모두 스크랩되는 것을 볼 수 있습니다.

데이터 내보내기

Scrapy의 내장 Feed Exporter는 결과 저장을 매우 간단하게 해줍니다. -o (output) 플래그를 사용하여 스크랩된 아이템을 파일에 기록합니다:

scrapy crawl books -o books.json

이 명령으로 스파이더를 실행하면 프로젝트 루트에 books.json 파일이 생성되고, 48개의 아이템이 깔끔하고 구조화된 JSON 형식으로 들어갑니다.

배운 내용

  • 최신 async Scrapy 프로젝트를 설정했습니다.
  • 필요한 데이터에 대한 CSS 선택자를 찾았습니다.
  • 링크를 따라가고 페이지네이션을 자동으로 처리했습니다.
  • 한 명령으로 스크랩한 데이터를 내보냈습니다.

이것은 시작에 불과합니다!

💬 TALK: 이 Scrapy 코드가 막히셨나요? 유지보수자와 Discord의 5천 명 이상의 개발자에게 물어보세요.

▶️ WATCH: 이 게시물은 우리 영상에 기반합니다—전체 walkthrough를 YouTube 채널에서 시청하세요.

📩 READ: 더 보고 싶나요? Part 2에서는 Scrapy ItemsPipelines를 다룰 예정입니다. 놓치지 않으려면 Extract 뉴스레터를 구독하세요.

Back to Blog

관련 글

더 보기 »