Scrapy 요청 및 응답: 완전 초보자 가이드 (문서가 알려주지 않는 비밀 포함)

발행: (2025년 12월 23일 오후 04:47 GMT+9)
9 min read
원문: Dev.to

Source: Dev.to

1. 기본

ConceptWhat it means in Scrapy
Request“이 URL을 방문하고 싶어요” 라고 말하는 객체.
Response웹사이트가 반환한 내용(HTML, JSON 등)을 포함하는 객체.

Think of web scraping like a conversation:

Request:  "Hey website, can you show me this page?"
Response: "Sure, here's the HTML!"

2. Scrapy가 내부에서 수행하는 작업

간단한 스파이더:

import scrapy

class MySpider(scrapy.Spider):
    name = 'myspider'
    start_urls = ['https://example.com']

    def parse(self, response):
        # Do something with response
        pass

실제로 일어나는 일

def start_requests(self):
    for url in self.start_urls:
        yield scrapy.Request(url=url, callback=self.parse)

Scrapy는 start_urls에 있는 각 URL에 대해 자동으로 Request를 생성하고 이를 parse에 전달합니다.

3. 요청을 수동으로 만들기

요청을 직접 만들어 모든 세부 사항을 제어할 수 있습니다:

import scrapy

class MySpider(scrapy.Spider):
    name = 'myspider'

    def start_requests(self):
        yield scrapy.Request(
            url='https://example.com',
            callback=self.parse,
            method='GET',
            headers={'User-Agent': 'My Custom Agent'},
            cookies={'session': 'abc123'},
            meta={'page_num': 1},
            dont_filter=False,
            priority=0,
        )

    def parse(self, response):
        # Process response
        pass

3.1 요청 매개변수 (빠른 참고)

ParameterDescription
url대상 URL (필수).
callbackResponse를 받을 함수. 기본값은 parse.
methodHTTP 메서드(GET, POST, PUT, DELETE, …). 기본값: GET.
body원시 요청 본문(POST, PUT에 유용).
headers사용자 정의 요청 헤더.
cookies요청과 함께 보낼 쿠키.
metaResponse에 전달되는 임의의 dict(response.meta). 콜백 간 데이터 공유에 유용.
dont_filterTrue이면 Scrapy가 이 URL을 중복으로 필터링하지 않습니다.
priority정수 우선순위; 값이 클수록 먼저 처리됨(기본값 = 0).

예시

# 1️⃣ Simple URL
yield scrapy.Request(url='https://example.com/products')

# 2️⃣ Custom callback
yield scrapy.Request(
    url='https://example.com/products',
    callback=self.parse_products,
)

def parse_products(self, response):
    # Handle response here
    pass

# 3️⃣ POST request with JSON body
yield scrapy.Request(
    url='https://example.com/api',
    method='POST',
    body='{"key": "value"}',
    headers={'Content-Type': 'application/json'},
)

# 4️⃣ Custom headers
yield scrapy.Request(
    url='https://example.com',
    headers={
        'User-Agent': 'Mozilla/5.0',
        'Accept': 'text/html',
        'Referer': 'https://google.com',
    },
)

# 5️⃣ Cookies
yield scrapy.Request(
    url='https://example.com',
    cookies={'session_id': '12345', 'user': 'john'},
)

# 6️⃣ Passing data via meta
yield scrapy.Request(
    url='https://example.com/details',
    meta={'product_name': 'Widget', 'price': 29.99},
    callback=self.parse_details,
)

def parse_details(self, response):
    name = response.meta['product_name']
    price = response.meta['price']
    # Do something with name & price

# 7️⃣ Bypass duplicate filter
yield scrapy.Request(
    url='https://example.com',
    dont_filter=True,
)

# 8️⃣ Prioritise a request
yield scrapy.Request(
    url='https://example.com/important',
    priority=10,   # processed before priority 0 requests
)

Source:

4. Response 객체

요청이 완료되면 Scrapy는 콜백에 Response 객체를 전달합니다. 일반적으로 다음과 같은 속성을 사용합니다:

def parse(self, response):
    # 기본 속성
    url      = response.url               # 최종 URL (리다이렉트 후)
    body     = response.body              # 원시 바이트
    text     = response.text              # 디코딩된 문자열 (기본 UTF‑8)
    status   = response.status            # HTTP 상태 코드 (200, 404, …)
    headers  = response.headers          # 응답 헤더 (대소문자 구분 없는 dict)

    # 요청과 연결된 정보
    request  = response.request           # 원본 Request 객체
    meta     = response.meta              # 요청에서 전달된 meta dict

4.1 데이터 선택

# CSS 선택자 (가장 가독성이 좋음)
titles = response.css('h1.title::text').getall()
first_title = response.css('h1.title::text').get()

# XPath 선택자 (보다 강력함)
titles = response.xpath('//h1[@class="title"]/text()').getall()

4.2 링크 따라가기

# 수동 방식 (冗長)
next_page = response.css('a.next::attr(href)').get()
if next_page:
    full_url = response.urljoin(next_page)
    yield scrapy.Request(full_url, callback=self.parse)

# 권장 방식 – `response.follow`
next_page = response.css('a.next::attr(href)').get()
if next_page:
    yield response.follow(next_page, callback=self.parse)

# 선택자를 바로 전달할 수도 있습니다:
yield response.follow(
    response.css('a.next::attr(href)').get(),
    callback=self.parse,
)

# 혹은 모든 <a> 태그를 순회하면서:
for link in response.css('a'):
    yield response.follow(link, callback=self.parse_page)

response.follow() 은 자동으로:

  • 상대 URL을 처리합니다 (urljoin 내부적으로 사용).
  • 선택자를 전달하면 href 속성을 추출합니다.
  • Request 객체를 생성해 줍니다 (기본 callback 포함).

5. 디버깅 및 내부 탐색

때때로 응답을 생성한 원본 요청을 살펴볼 필요가 있습니다(특히 리다이렉트가 발생한 경우).

def parse(self, response):
    # Original request data
    original_url     = response.request.url
    original_headers = response.request.headers
    original_meta    = response.request.meta

    # Log useful info
    self.logger.info(f'Requested: {original_url}')
    self.logger.info(f'Got back: {response.url}')   # May differ after redirects

TL;DR

  • Requests는 완전히 구성 가능한 객체(url, method, headers, cookies, meta, priority, …)입니다.
  • Responses는 데이터를 추출하는 데 필요한 모든 정보를 제공합니다(url, body, text, status, headers, 그리고 원본 요청에 대한 역참조).
  • 깔끔하고 간결한 링크 추적 로직을 위해 **response.follow()**를 사용하세요.
  • **meta**를 활용해 콜백 간에 데이터를 전달하고, **priority/dont_filter**를 사용해 크롤링 순서와 중복 처리 방식을 제어하세요.

이러한 세부 사항을 숙지하면 기본을 넘어 견고하고 효율적인 Scrapy 스파이더를 작성할 수 있습니다—필요한 작업을 정확히 수행하고 숨은 놀라움은 없습니다. 즐거운 크롤링 되세요!

Source:

Scrapy 빠른 참고 치트 시트

아래는 정리되고 구조화된 유용한 Scrapy 패턴 모음입니다. 원본 스니펫은 그대로 유지하되, 포맷만 개선했습니다.

1. Response 헤더 다루기

def parse(self, response):
    # Get all headers
    all_headers = response.headers

    # Get a specific header
    content_type = response.headers.get('Content-Type')

    # Check cookies the server sent back
    cookies = response.headers.getlist('Set-Cookie')

    # Useful for debugging blocks
    server = response.headers.get('Server')
    self.logger.info(f'Server type: {server}')

2. 리다이렉트 시 meta 보존하기

def start_requests(self):
    yield scrapy.Request(
        'https://example.com/redirect',
        meta={'important': 'data'},   # custom meta data
        callback=self.parse
    )

def parse(self, response):
    # Even after a redirect, the meta dict is still there!
    data = response.meta['important']

    # The final URL may be different
    self.logger.info(f'Ended up at: {response.url}')

3. priority 로 크롤링 순서 제어하기

def parse_listing(self, response):
    # High priority for product pages (process first)
    for product in response.css('.product'):
        url = product.css('a::attr(href)').get()
        yield response.follow(
            url,
            callback=self.parse_product,
            priority=10               # higher number → earlier processing
        )

    # Low priority for pagination (process later)
    next_page = response.css('.next::attr(href)').get()
    if next_page:
        yield response.follow(
            next_page,
            callback=self.parse_listing,
            priority=0                # default priority
        )

Tip: “반드시 필요” 페이지에는 높은 우선순위를, 페이지네이션이나 보조 콘텐츠에는 낮은 우선순위를 사용하세요.

4. 폼 제출 – FormRequest

a) 간단한 POST 요청

import scrapy

class LoginSpider(scrapy.Spider):
    name = 'login'

    def start_requests(self):
        yield scrapy.FormRequest(
            url='https://example.com/login',
            formdata={
                'username': 'myuser',
                'password': 'mypass'
            },
            callback=self.after_login
        )

    def after_login(self, response):
        if 'Welcome' in response.text:
            self.logger.info('Login successful!')
        else:
            self.logger.error('Login failed!')

b) 페이지에서 자동으로 폼 채우기 (from_response)

class LoginSpider(scrapy.Spider):
    name = 'login'
    start_urls = ['https://example.com/login']

    def parse(self, response):
        # Automatically locate the form, keep hidden fields (e.g., CSRF)
        # and submit the supplied data.
        yield scrapy.FormRequest.from_response(
            response,
            formdata={
                'username': 'myuser',
                'password': 'mypass'
            },
            callback=self.after_login
        )

    def after_login(self, response):
        # Now you're logged in – continue crawling.
        yield response.follow('/dashboard', callback=self.parse_dashboard)

FormRequest.from_response() 가 수행하는 작업

  1. 첫 번째 <form> 요소(또는 formname/formid와 일치하는 요소)를 찾습니다.
  2. 숨겨진 입력 필드(예: CSRF 토큰)를 포함한 모든 폼 필드를 추출합니다.
  3. formdata에 제공한 필드만 덮어씁니다.
  4. 요청을 전송합니다.

5. meta 로 페이지 번호 추적하며 페이지네이션하기

import scrapy

class ProductSpider(scrapy.Spider):
    name = 'products'

    def start_requests(self):
        yield scrapy.Request(
            'https://example.com/products?page=1',
            meta={'page': 1},
            callback=self.parse
        )

    def parse(self, response):
        page = response.meta['page']
        self.logger.info(f'Scrapin

6. 체이닝 요청 – 목록에서 상세 페이지로

import scrapy

class DetailSpider(scrapy.Spider):
    name = 'details'
    start_urls = ['https://example.com/products']

    def parse(self, response):
        """Scrape product listings and queue detail pages."""
        for product in response.css('.product'):
            item = {
                'name': product.css('h2::text').get(),
                'price': product.css('.price::text').get()
            }

            detail_url = product.css('a::attr(href)').get()
            yield response.follow(
                detail_url,
                callback=self.parse_detail,
                meta={'item': item}          # pass the partially‑filled item forward
            )

    def parse_detail(self, response):
        """Enrich the item with data from the detail page."""
        item = response.meta['item']
        item['description'] = response.css('.description::text').get()
        item['rating'] = response.css('.rating::text').get()
        item['reviews'] = response.css('.reviews::text').get()
        yield item
Back to Blog

관련 글

더 보기 »