Crawling

SCRAPY 프레임워크의 사용 방법 정리 (1)

bluebamus 2025. 2. 27.

1. 개요

   1.1. 핵심 특징

      1.1.1. 비동기식 아키텍처: Twisted 비동기 네트워킹 프레임워크를 기반으로 하여 빠른 성능 제공
      1.1.2. 확장성: 대규모 웹 사이트를 크롤링하도록 설계됨
      1.1.3. 내장 선택기(Selector): XPath와 CSS 표현식을 사용하여 데이터 추출 가능
      1.1.4. 미들웨어 시스템: 요청 및 응답 처리를 위한 확장 가능한 미들웨어 제공
      1.1.5. 파이프라인 시스템: 수집된 데이터를 처리하고 저장하기 위한 구조화된 방식
      1.1.6. Shell 인터페이스: 대화형으로 크롤링 규칙을 테스트할 수 있는 도구 제공
      1.1.3. 통합 지원: 다양한 데이터베이스, 프레임워크와 통합 가능

 

   1.2. 성능 특징

      1.2.1. 동시성: 여러 요청을 병렬로 처리하여 효율성 향상
      1.2.2. 자동 스로틀링(Auto Throttling): 서버 부하를 줄이기 위한 요청 속도 자동 조절
      1.2.3. 캐싱: 응답을 캐싱하여 중복 요청 방지
      1.2.4. 재시도 메커니즘: 실패한 요청을 자동으로 재시도

 

   1.3 편의성 특징

      1.3.1. 명령줄 도구: 프로젝트 생성, 실행 등을 위한 CLI 도구
      1.3.2. 스파이더 계약(Spider Contracts): 스파이더 코드 테스트 기능
      1.3.3. 광범위한 문서화: 상세한 문서와 예제
      1.3.4. 활발한 커뮤니티: 지속적인 개발과 지원

 

2. 설치 및 프로젝트 구조

   2.1. 설치

pip install scrapy

 

   2.2. 프로젝트 구조

tutorial/
    scrapy.cfg            # 프로젝트 설정 파일
    tutorial/             # 프로젝트 모듈
        __init__.py
        items.py          # 아이템 정의
        middlewares.py    # 미들웨어 정의
        pipelines.py      # 파이프라인 정의
        settings.py       # 설정 파일
        spiders/          # 스파이더 폴더
            __init__.py

 

3. 전역 명령어 (프로젝트 외부에서도 사용 가능)

   3.1. startproject

      3.1.1. 설명:

         - 새로운 Scrapy 프로젝트를 생성한다.

      3.1.2.  사용법:

         - project_name:

            - 생성할 프로젝트의 이름 (필수)
         - project_dir:

            - 프로젝트 디렉토리의 경로 (선택, 기본값은 project_name과 동일)

scrapy startproject <프로젝트명> [프로젝트_디렉토리]

 

         -  예시:

scrapy startproject myproject

 

         - 설명:

            - 위 명령어를 실행하면, scrapy.cfg 파일과 함께 프로젝트 구조(프로젝트 모듈, items.py, settings.py, spiders/ 폴더 등)가 자동으로 생성된다.

 

   3.2. genspider

      3.2.1. 설명:

         - 지정한 도메인을 대상으로 하는 새 스파이더 템플릿을 생성한다.

      3.2.1.  사용법:

         - 주요 옵션:

            - -l, --list: 사용 가능한 스파이더 템플릿 목록을 출력한다.

            - -t, --template: 특정 스파이더 템플릿을 선택한다. 사용 가능한 템플릿은 basic, crawl, csvfeed, xmlfeed 등이 있다.
            - -d, --dump: 스파이더 생성 과정을 보여준다.

            - -e, --edit: 스파이더를 생성한 후에 에디터를 열어 편집할 수 있다.

            - -f, --force: 같은 이름의 스파이더가 이미 존재할 경우 덮어쓸 수 있도록 한다.

            - -v, --verbosity: 로깅 레벨을 설정한다. DEBUG, INFO, WARNING, ERROR, CRITICAL 중 선택할 수 있다.

 

         - 템플릿 옵션:

            - csvfeed: CSV 파일에서 데이터를 가져와 스크랩한다.
            - xmlfeed: XML 파일에서 데이터를 가져와 스크랩한다.
            - crawl: 일반적인 스파이더 템플릿으로, 규칙(rules)을 사용해 웹사이트를 순회하며 데이터를 수집한다.
            - basic: 가장 기본적인 스파이더 템플릿으로, 스크래핑 시작점 URL만을 처리한다. (단일 도메인 페이지 크롤링)

scrapy genspider <스파이더이름> <도메인> [ -t 템플릿 ]

 

         - 예시:

scrapy genspider example example.com
scrapy genspider -t crawl books books.toscrape.com

 

   3.3. settings

      3.3.1. 설명:

         - 현재 프로젝트의 설정 값을 쉽게 확인할 수 있으며, 특정 스파이더에 대한 설정도 확인할 수 있다.

      3.3.2. 사용법:

         - 주요 옵션
            - --get=NAME

               - 지정된 설정 값을 가져온다.

            - --getbool=NAME

               - 지정된 설정의 불리언 값을 가져온다.

            - --getint=NAME
                - 지정된 설정의 정수 값을 가져온다.

            - --getfloat=NAME
                - 지정된 설정의 부동 소수점 값을 가져온다.

            - --getlist=NAME

                - 지정된 설정의 리스트 값을 가져온다.

            - --spider=SPIDER

                - 특정 스파이더에 대한 설정을 가져온다.

            - -h, --help

                - 도움말 메시지를 표시한다.

scrapy settings [옵션] [설정_키]

 

         - 예시:

scrapy settings --get BOT_NAME
scrapy settings --getbool ROBOTSTXT_OBEY
scrapy settings --getint CONCURRENT_REQUESTS
scrapy settings --spider=myspider --get DOWNLOAD_DELAY

 

         - 설명: 

             - 사용자가 설정한 값이 없으면 Scrapy의 기본값을 반환한다.

 

   3.4. runspider

      3.4.1. 설명: Scrapy 프로젝트 없이 단일 스파이더 파일을 실행한다.

         - 사용법:

            - 주요 옵션

               - -a NAME=VALUE
                  - 스파이더에 인자를 전달합니다. 여러 번 사용할 수 있다.
               - -o FILE 또는 --output=FILE
                  - 크롤링한 아이템을 FILE에 추가한다.
               - -t FORMAT 또는 --output-format=FORMAT
                  - 출력 형식을 지정한다 (예: json, csv, xml).

               - -s NAME=VALUE

                  - Scrapy 설정을 오버라이드한다.
               - --nolog

                  - 로깅을 비활성화한다.
               - --loglevel=LEVEL 또는 -L LEVEL

                  - 로그 레벨을 설정한다 (예: DEBUG, INFO, WARNING).
               - -h 또는 --help

                  - 도움말 메시지를 표시한다.

scrapy runspider <spider_file.py> [options]

 

            -  예시:

scrapy runspider myspider.py
scrapy runspider myspider.py -o output.json -t json
scrapy runspider myspider.py -a category=electronics -s DOWNLOAD_DELAY=2

 

            - 설명: 

               - 간단한 테스트나 스크립트 실행 시 유용하다.

 

   3.5. shell

      3.5.1. 설명:

         - 대화형 쉘을 시작하여 웹사이트를 쉽게 스크래핑, 테스트할 수 있다. 셀렉터 테스트와 응답 분석에 매우 유용하다.

 

      3.5.2. 사용법:

         - 주요 옵션
             - -h, --help
                - 도움말 메시지를 표시한다.

             - --spider=SPIDER

                - 특정 스파이더를 사용한다.
             - -c CODE

                - 셸에서 실행할 코드를 지정한다.

             - --no-redirect

                - HTTP 3xx 리다이렉션을 따르지 않는다.
             - --nolog

                - 로깅을 비활성화한다.
             - -s NAME=VALUE
                - Scrapy 설정을 오버라이드한다.

 

         - 셸 내부에서는 다음과 같은 객체를 사용할 수 있다:
             - response: 현재 응답 객체
             - request: 현재 요청 객체
             - selector: 응답의 셀렉터 객체
             - scrapy: scrapy 모듈

scrapy shell [URL 또는 파일경로]

 

         - 예시:

scrapy shell http://books.toscrape.com

 

         - 설명:

             - 셸 내에서 response 객체를 활용하여 페이지 구조를 분석할 수 있으며, 예를 들어 response.xpath() 또는 response.css() 등을 사용해 데이터를 추출해볼 수 있다.

 

   3.6. fetch

      3.6.1. 설명:

         - 지정한 URL에 대한 HTTP 응답을 가져와 셸에서 바로 확인할 수 있다. Scrapy가 웹 페이지를 어떻게 가져오는지 확인하고 디버깅하는 데 유용하다. 특히 헤더 정보나 리다이렉션 동작을 확인할 때 활용할 수 있다.

 

      3.6.2. 사용법:

         - 주요 옵션

            - --spider=SPIDER
               - 특정 스파이더를 사용하여 URL을 처리한다.
            - --headers
               - 응답 헤더를 출력한다.
            - --no-redirect
               - HTTP 3xx 리다이렉션을 따르지 않는다.
            - --raw

               - 원시 응답 본문을 출력한다 (헤더 포함).
            - -h, --help

               - 도움말 메시지를 표시한다.

            - --nolog

               - 로깅을 비활성화한다.
            - -s NAME=VALUE
               - Scrapy 설정을 오버라이드한다.

scrapy fetch [옵션] <URL>

 

         - 예시:

scrapy fetch --nolog http://www.example.com
scrapy fetch --headers --spider=myspider http://www.example.com

 

         - 설명: 

             - 디버깅용으로 유용하며, 응답의 상태, 헤더, 바디 등을 확인할 수 있다.

 

   3.7. view

      3.7.1. 설명:

         - Scrapy가 특정 URL을 어떻게 "보는지" 확인할 수 있다. 이 명령어는 브라우저로 렌더링된 페이지를 열어 Scrapy가 다운로드한 페이지와 비교할 수 있게 해준다.

 

      3.7.2. 사용법:

         - 주요 옵션

            - --spider=SPIDER

               - 특정 스파이더를 사용하여 URL을 처리한다.

            - --no-redirect

               - HTTP 3xx 리다이렉션을 따르지 않는다.
            - -h, --help

               - 도움말 메시지를 표시한다.

            - --nolog

               - 로깅을 비활성화한다.
            - -s NAME=VALUE

               - Scrapy 설정을 오버라이드한다.

scrapy view <URL>

 

         - 예시:

scrapy view http://books.toscrape.com
scrapy view https://www.example.com
scrapy view https://www.example.com --no-redirect
scrapy view https://www.example.com --spider=myspider

 

         - 설명: 

            - 주로 셸에서 fetch한 결과를 실제 브라우저에서 렌더링하여 확인할 때 사용한다.

 

   3.8. version

      3.8.1. 설명:

         - 설치된 Scrapy의 버전을 출력한다.


      3.8.2. 사용법:

scrapy version [-v]

 

      3.8.3. 예시:

scrapy version -v

 

      3.8.4. 설명: 

         - -v 옵션을 주면 상세한 버전 정보(라이브러리 버전 등)도 출력된다.

 

4. 프로젝트 전용 명령어 (scrapy.cfg 파일이 있는 프로젝트 디렉토리 내에서 사용)

   4.1. crawl

      4.1.1. 설명:

         - 프로젝트 내에 정의된 스파이더를 실행하여 크롤링을 수행한다.

      4.1.2. 사용법:

         - 주요 옵션

            - -a NAME=VALUE
               - 스파이더에 인자를 전달한다. 여러 번 사용할 수 있다.
            - --output FILE 또는 -o FILE

               - 크롤링한 아이템을 FILE의 끝에 추가합니다. 표준 출력의 경우 '-'를 사용한다.
            - --overwrite-output FILE 또는 -O FILE

               - 크롤링한 아이템을 FILE에 덮어쓴다.

            - -s SETTING=VALUE

               - Scrapy 설정을 오버라이드한다.
            - -L LOGLEVEL
                - 로깅 레벨을 설정한다 (예: DEBUG, INFO, WARNING)

scrapy crawl [스파이더이름] [options]

 

         - 예시:

scrapy crawl myspider
scrapy crawl myspider -o output.json:json
scrapy crawl myspider -O output.csv:csv

 

         - 설명: 스파이더의 이름은 스파이더 클래스 내의 name 속성에 정의된 값과 일치해야 한다.

 

   4.2. check

      4.2.1. 설명:

         - 스파이더의 계약을 검사하여 스파이더가 예상대로 작동하는지 확인하는 데 유용하다. 이를 통해 개발자는 스파이더의 동작을 검증하고 잠재적인 문제를 사전에 발견할 수 있다.

 

      4.2.2. 사용법:

         - 주요 옵션

            - -l, --list

               - 사용 가능한 스파이더 목록을 표시한다.

            - -v, --verbose

               - 자세한 출력을 활성화한다.

            -h, --help

               - 도움말 메시지를 표시한다.

            - --nolog

               - 로깅을 비활성화한다.

            - -s NAME=VALUE

               - Scrapy 설정을 오버라이드한다.

scrapy check [options] <spider>

 

         - 예시:

scrapy check myspider
scrapy check -l
scrapy check -v myspider

 

         - 설명: 

            - -l 옵션은 모든 스파이더에 대해 체크를 수행한다. 

 

   4.3. list

      4.3.1. 설명:

         - 현재 프로젝트의 모든 스파이더 목록을 보여준다. 이 명령어는 프로젝트 디렉토리 내에서 실행해야 한다.

      4.3.2. 사용법:

scrapy list

 

      4.3.3. 설명: 

         - 프로젝트 내에서 작성한 스파이더 이름들이 한눈에 표시되어, 실행 전에 확인하기 좋다.

 

   4.4. edit

      4.4.1. 설명:

         - 설정된 기본 에디터(환경 변수 EDITOR에 지정된 에디터)를 통해 지정한 스파이더 파일을 열어 수정한다.

 

      4.4.2. 사용법:

scrapy edit <스파이더이름>

 

      4.3.3. 예시:

scrapy edit myspider

 

      4.3.4. 설명: 

         - 빠르게 코드를 수정하고 저장한 후, 바로 테스트할 수 있다.

 

   4.5. parse

      4.5.1. 설명:

         - 특정 URL을 파싱하고 그 결과를 보여준다. 스파이더의 파싱 메서드를 테스트하는 데 유용하다.

         - 주어진 URL을 가져와 특정 콜백(예: parse 메소드)을 실행시켜 응답을 처리해보고, 그 결과(추출된 item 등)를 확인할 수 있다.

 

      4.5.2. 사용법:

         - 주요 옵션

            - --spider=SPIDER

               - 사용할 spider를 지정한다.

            - --a NAME=VALUE

               - spider 인자를 설정한다. 여러 번 사용할 수 있다.

            - --callback 또는 -c

               - spider에서 사용할 콜백 함수를 지정한다.

            - --pipelines

               - 항목 파이프라인을 사용한다.

            - --rules 또는 -r

               - CrawlSpider 규칙을 사용하여 링크를 추출한다.

            - --noitems

               - 항목 출력을 숨긴다.

            - --nolinks

               - 링크 출력을 숨긴다.

            - --nocolour

               - 색상 출력을 비활성화한다.

            - --depth 또는 -d

               - 재귀적 요청의 깊이 제한을 설정한다.

            - --verbose 또는 -v

               - 자세한 출력을 활성화한다.

scrapy parse <URL> [옵션]

 

         - 예시:

scrapy parse http://books.toscrape.com -c parse_item
scrapy parse http://www.example.com/ -c parse_item
scrapy parse http://www.example.com/ --spider=myspider -c parse_item -d 2

 

         - 옵션:

             - -c 또는 --callback: 사용할 콜백 메소드를 지정
         - 설명:

             - 스파이더가 어떻게 응답을 파싱하는지 미리 확인할 때 유용하다.

 

   4.6. bench

      4.6.1. 설명:

         - 간단한 벤치마킹을 수행하는 도구이다. 이 명령어는 아무 작업도 하지 않고 링크만 따라가는 간단한 스파이더를 사용하여 Scrapy의 성능을 측정하다.

 

      4.6.2. 사용법:

         - -h, --help

            - 도움말 메시지를 표시하다.

         - -s NAME=VALUE

            - Scrapy 설정을 오버라이드한다.

         - --logfile=FILE

            - 로그 출력을 지정된 파일로 보낸다.

         - --loglevel=LEVEL, -L LEVEL

            - 로그 레벨을 설정한다 (기본값: INFO).

         - --nolog

            - 로깅을 비활성화한다.

scrapy bench

 

      4.6.3. 설명: 

         - 내부적으로 간단한 스파이더를 실행해 다운로드 속도 등을 측정하여 출력한다.

 

   4.7. 추가 팁

      4.7.1. 설정값 재정의:

         - 대부분의 명령어는 --set 옵션을 통해 개별 설정을 재정의할 수 있다. 예를 들어, 다음과 같이 설정할 수 있다.

scrapy shell http://daum.net --set="ROBOTSTXT_OBEY=False"

 

      4.7.2. 로그 옵션:

      - --nolog 옵션을 사용하면 로그 출력을 생략하여 콘솔 출력만 집중할 수 있다.

 

5. 공통 메소드

   5.1. Response 객체의 기본 메소드

      - Scrapy의 Response 객체는 요청에 대한 응답을 나타내며, 이를 통해 HTML/XML 문서에서 데이터를 추출할 수 있다.

      5.1.1. response.css()

         - 설명:

            - HTML 요소를 CSS 선택자(selector)를 사용하여 선택한다.
            - 내부적으로 선택된 요소들을 SelectorList 객체로 반환하며, 추가 메소드(get(), getall(), 등)를 체이닝하여 사용할 수 있다.

         - 파라미터:

            - query (str): CSS 선택자 문자열. 예를 들어, 'p.price_color::text'처럼 텍스트 노드를 선택할 수도 있음.

         - 반환값:

            - SelectorList: 선택된 요소들의 리스트.

         - 사용 예시:

# HTML에서 <p class="price_color"> 요소의 텍스트만 추출
prices_selector = response.css('p.price_color::text')

# 첫 번째 가격만 추출 (문자열 반환)
first_price = prices_selector.get()

# 모든 가격을 리스트 형태로 추출
all_prices = prices_selector.getall()

# 결과 출력
print("첫 번째 가격:", first_price)
print("모든 가격:", all_prices)

 

      5.1.2. response.xpath()

         - 설명:

            - XPath 표현식을 사용하여 HTML/XML 문서 내의 요소를 선택한다.
            - 선택된 요소들은 역시 SelectorList로 반환되며, 추가 추출 메소드와 체이닝하여 사용한다.

         - 파라미터:

            - query (str): XPath 표현식. 예를 들어, '//article[@class="product_pod"]/h3/a/text()'와 같이 요소의 텍스트를 선택할 수 있음.

         - 반환값:

            - SelectorList: XPath로 선택된 요소들의 리스트.
         - 사용 예시:

# HTML에서 article 태그 내부의 h3 > a 요소의 텍스트 추출
product_name_selector = response.xpath('//article[@class="product_pod"]/h3/a/text()')

# 첫 번째 제품 이름 추출
first_product_name = product_name_selector.get()

print("첫 번째 제품 이름:", first_product_name)

 

      5.1.3. .get() 메소드

         - 설명:

            - SelectorList에서 선택된 요소들 중 첫 번째 항목을 문자열로 반환한다.
            - 만약 해당하는 요소가 없으면 None을 반환한다.

         - 파라미터:

            - 선택적인 default 인자: 요소가 없을 때 반환할 기본 값(예: default='').

         - 반환값:

            - str 또는 지정한 기본 값.
         - 사용 예시:

# CSS 선택자를 사용하여 첫 번째 가격 요소의 텍스트를 추출
first_price = response.css('p.price_color::text').get()

# 요소가 없을 경우 기본값으로 '가격 없음' 반환
first_price = response.css('p.price_color::text').get(default='가격 없음')

print("첫 번째 가격:", first_price)

 

      5.1.4. .getall() 메소드

         - 설명:

            - SelectorList 내의 모든 선택된 요소들을 문자열 리스트로 반환한다.
         - 파라미터:

            - 별도의 파라미터가 필요하지 않음.

         - 반환값:

            - List[str]: 모든 선택된 요소의 문자열 리스트.
         - 사용 예시:

# 모든 가격 요소의 텍스트를 리스트로 추출
all_prices = response.css('p.price_color::text').getall()

print("모든 가격:", all_prices)

 

      5.1.5. .attrib 속성

         - 설명:
            - 선택된 HTML 요소의 속성들을 사전(dictionary) 형태로 제공한다.
            - 이를 통해 특정 속성 값(예: href, src 등)을 쉽게 추출할 수 있다.

         - 사용 방법:

            - 선택된 요소에 대해 .attrib를 사용하면, 해당 요소의 모든 HTML 속성과 값을 담은 딕셔너리를 얻을 수 있다.
            - 개별 속성을 추출하려면 딕셔너리의 key 접근 방식(['속성명'])을 사용한다.
         - 반환값:

            - dict: 선택된 요소의 속성 key-value 쌍.
         - 사용 예시:

# 첫 번째 제품 링크를 CSS 선택자로 추출
product_link_selector = response.css('article.product_pod h3 a')

# .attrib를 사용하여 'href' 속성 값 추출
product_link = product_link_selector.attrib['href']

print("제품 링크:", product_link)

 

   5.2. 실무 기반 시나리오 예제

      - 다음은 실제 프로젝트에서 자주 사용되는 시나리오 기반의 스크래핑 코드 예제 2가지를 설명과 함께 제공한다.

      5.2.1. 예제 1: 이커머스 사이트의 상품 정보 스크래핑

import scrapy

class ProductItem(scrapy.Item):
    # 스크래핑한 상품 정보를 저장할 필드 정의
    name = scrapy.Field()
    price = scrapy.Field()
    link = scrapy.Field()

class EcommerceSpider(scrapy.Spider):
    name = 'ecommerce'
    start_urls = ['http://example-ecommerce.com/products']

    def parse(self, response):
        # 모든 상품 목록을 순회하며 정보 추출
        for product in response.css('article.product_pod'):
            item = ProductItem()
            # 제품 이름을 CSS 선택자로 추출 (get()으로 첫 번째 요소만)
            item['name'] = product.css('h3 a::text').get(default='이름 없음')
            # 제품 가격을 CSS 선택자로 모두 추출 후 첫 번째 값을 get()
            item['price'] = product.css('p.price_color::text').get(default='가격 없음')
            # 제품 상세 페이지 링크를 attrib를 사용해 추출
            item['link'] = product.css('h3 a').attrib.get('href', '링크 없음')
            yield item

# 주석: 이 스파이더는 이커머스 사이트의 상품 목록 페이지에서 각 상품의 이름, 가격, 링크를 추출하여 ProductItem에 저장합니다.

 

      5.2.2. 예제 2: 블로그 게시글 제목 및 링크 스크래핑

import scrapy

class BlogPostItem(scrapy.Item):
    # 블로그 게시글 정보를 저장할 필드 정의
    title = scrapy.Field()
    url = scrapy.Field()

class BlogSpider(scrapy.Spider):
    name = 'blog'
    start_urls = ['http://example-blog.com/']

    def parse(self, response):
        # 블로그 게시글 목록을 CSS와 XPath를 혼합하여 추출하는 예제
        posts = response.css('div.post')  # 각 게시글이 포함된 div 선택
        for post in posts:
            item = BlogPostItem()
            # 게시글 제목을 추출 (CSS 선택자 사용)
            item['title'] = post.css('h2 a::text').get(default='제목 없음')
            # 게시글 링크를 추출 (XPath 사용, attrib 활용)
            item['url'] = post.xpath('.//h2/a').attrib.get('href', '링크 없음')
            yield item

# 주석: 이 스파이더는 블로그 메인 페이지에서 각 게시글의 제목과 URL을 추출합니다.
# CSS와 XPath를 함께 사용하여 상황에 따라 유연하게 데이터를 선택하는 방법을 보여줍니다.

 

6. Spider

   - Scrapy의 핵심 구성 요소인 Spider는 웹사이트에서 데이터를 추출하는 역할을 한다.

   - Spider는 URL 목록(혹은 시작 URL)을 바탕으로 요청을 보내고, 응답을 처리하는 parse() 메서드를 구현한다.

 

   - 기본 Spider 예제

import scrapy

class QuotesSpider(scrapy.Spider):
    # 스파이더의 이름 정의 (필수)
    name = "quotes"
    
    # 크롤링을 시작할 URL 목록
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
    ]

    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(),
                'tags': quote.css('div.tags a.tag::text').getall(),
            }

        # 다음 페이지로 이동
        next_page = response.css('li.next a::attr(href)').get()
        if next_page is not None:
            # 절대 URL로 변환하여 다음 페이지 요청
            next_page = response.urljoin(next_page)
            yield scrapy.Request(next_page, callback=self.parse)

 

   6.1. 기본 Spider 메소드

      6.1.1. __init__(self, *args, **kwargs)

         - Spider 클래스의 인스턴스가 생성될 때 호출되는 초기화 메소드이다.

class MySpider(scrapy.Spider):
    name = 'myspider'
    
    def __init__(self, category=None, *args, **kwargs):
        super(MySpider, self).__init__(*args, **kwargs)
        self.start_urls = [f'https://example.com/category/{category}']
        self.category = category

 

      - 이 메소드를 통해 명령줄 인수를 받아 Spider의 동작을 제어할 수 있다.

scrapy crawl myspider -a category=electronics

 

      6.1.2. start_requests(self)

         - Spider가 크롤링을 시작할 때 호출되는 메소드로, 첫 번째 Request 객체들을 생성한다.

def start_requests(self):
    urls = [
        'https://example.com/page1',
        'https://example.com/page2',
    ]
    for url in urls:
        yield scrapy.Request(url=url, callback=self.parse)

 

         - start_urls 속성이 정의되어 있다면 이 메소드는 기본적으로 해당 URL들을 방문한다. 커스텀 헤더, 쿠키, 메타데이터 등을 추가할 때 이 메소드를 오버라이드한다.

 

      6.1.3. parse(self, response)

         - Request의 응답을 처리하는 기본 콜백 메소드이다. 주로 데이터를 추출하거나 추가 Request를 생성한다.

def parse(self, response):
    # 데이터 추출
    for product in response.css('div.product'):
        yield {
            'name': product.css('h2::text').get(),
            'price': product.css('span.price::text').get(),
            'url': product.css('a::attr(href)').get(),
        }
    
    # 다음 페이지로 이동
    next_page = response.css('a.next-page::attr(href)').get()
    if next_page:
        yield response.follow(next_page, callback=self.parse)

 

   6.2. 고급 콜백 메소드

      6.2.1. parse_item(self, response) (관례적 이름)

         - 특정 아이템 페이지를 처리하는 메소드이다. 이름은 관례적인 것으로, 어떤 이름이든 사용 가능하다.

def parse(self, response):
    # 상품 목록 페이지에서 각 상품 URL 추출
    for product_url in response.css('div.product a::attr(href)').getall():
        yield response.follow(product_url, callback=self.parse_item)

def parse_item(self, response):
    # 상품 상세 페이지에서 정보 추출
    yield {
        'title': response.css('h1.title::text').get(),
        'description': response.css('div.description::text').get(),
        'price': response.css('span.price::text').get(),
        'sku': response.css('span.sku::text').get(),
    }

 

      6.2.2. errback(self, failure)

         - Request 처리 중 오류가 발생했을 때 호출되는 콜백 메소드이다.

def start_requests(self):
    urls = ['https://example.com/page1']
    for url in urls:
        yield scrapy.Request(
            url=url, 
            callback=self.parse,
            errback=self.errback_handler
        )

def errback_handler(self, failure):
    # Request 실패 처리
    self.logger.error(f"Request failed: {failure.request.url}")
    self.logger.error(repr(failure))

 

   6.3. 생명주기 메소드

      6.3.1. from_crawler(cls, crawler, *args, **kwargs)

         - 클래스 메소드로, Spider 인스턴스를 생성하는 팩토리 메소드이다. 주로 크롤러 설정에 접근할 때 사용한다.

@classmethod
def from_crawler(cls, crawler, *args, **kwargs):
    spider = super().from_crawler(crawler, *args, **kwargs)
    spider.settings = crawler.settings
    
    # 크롤러 이벤트에 시그널 핸들러 연결
    crawler.signals.connect(spider.spider_closed, signal=signals.spider_closed)
    return spider

 

      6.3.2. closed(self, reason)

         - Spider가 종료될 때 호출되는 메소드이다.

def closed(self, reason):
    self.logger.info(f"Spider closed: {reason}")
    # 결과 요약, 정리 작업 수행
    with open('results_summary.txt', 'w') as f:
        f.write(f"Total items scraped: {self.item_count}")

 

      6.3.3. spider_opened(self)와 spider_closed(self, spider, reason)

         - 시그널 핸들러로, Spider가 시작되거나 종료될 때 호출된다.

def __init__(self, *args, **kwargs):
    super(MySpider, self).__init__(*args, **kwargs)
    self.item_count = 0

def spider_opened(self, spider):
    spider.logger.info('Spider opened: %s' % spider.name)
    
def spider_closed(self, spider, reason):
    spider.logger.info('Spider closed: %s, Reason: %s' % (spider.name, reason))
    spider.logger.info(f'Items scraped: {self.item_count}')

 

   6.4. 미들웨어 관련 메소드

      6.4.1. process_request(self, request, spider)

         - Request 미들웨어에서 사용되며, Request가 엔진에서 다운로더로 전달되기 전에 호출된다.

def process_request(self, request, spider):
    # 커스텀 헤더 추가
    request.headers['User-Agent'] = 'Custom User Agent'
    return None  # None을 반환하면 처리를 계속 진행

 

      6.4.2. process_response(self, request, response, spider)

      - Response 미들웨어에서 사용되며, 다운로더에서 엔진으로 Response가 전달되기 전에 호출됩니다.

def process_response(self, request, response, spider):
    if response.status == 404:
        spider.logger.warning(f"Page not found: {request.url}")
    return response  # 응답 객체를 반환하면 처리를 계속 진행

 

   6.5. 유틸리티 메소드

      6.5.1. make_requests_from_url(self, url)

         - start_urls에 있는 각 URL에 대한 Request 객체를 생성한다. 이 메소드는 1.7.0 버전부터 권장되지 않으며, start_requests()를 사용하는 것이 좋다.

def make_requests_from_url(self, url):
    return scrapy.Request(url, dont_filter=True)

 

      6.5.2. update_settings(self, settings)

         - Spider별 설정을 업데이트할 때 사용한다.

def update_settings(self, settings):
    settings.set('DOWNLOAD_DELAY', 2)
    settings.set('ROBOTSTXT_OBEY', False)

 

   6.6. 아이템 관련 메소드

      6.6.1. process_item(self, item, spider)

         - Item 파이프라인에서 사용된다. 각 아이템을 처리하는 방법을 정의한다.

def process_item(self, item, spider):
    # 아이템 필드 검증 및 처리
    if not item.get('price'):
        item['price'] = 'Unknown'
    
    # 아이템 카운트 증가
    spider.item_count += 1
    return item

 

   6.7. 로깅 메소드

      6.7.1. Spider 내장 로거 사용

         - Spider 클래스에는 내장 로거가 있어 다양한 레벨로 로깅이 가능하다.

def parse(self, response):
    self.logger.debug("Parsing product page: %s" % response.url)
    self.logger.info("Found %d products" % len(response.css('div.product')))
    self.logger.warning("Price missing for some products")
    self.logger.error("Failed to extract category information")
    self.logger.critical("Spider faced critical error")

 

   6.8. CrawlSpider 특수 메소드

      - CrawlSpider는 일반 Spider를 확장하여 링크를 쉽게 탐색할 수 있게 한다.

 

      6.8.1. _parse_response(self, response, callback, cb_kwargs, follow)

         - 내부 메소드로, Response를 처리하고 패턴에 맞는 링크를 따라간다.

         - 예제 코드 동작:

            - /category/ 패턴의 링크는 따라가지만(follow=True), 특별한 콜백 함수는 호출하지 않는다.
            - /product/ 패턴의 링크는 parse_item 함수를 콜백으로 사용한다.

            - _parse_response 메소드는 CrawlSpider의 내부 구현체로, 이 규칙들을 처리하는 역할을 담당한다. 일반적으로 이 메소드를 직접 오버라이드할 필요는 없으며, 대부분의 경우 rules와 필요한 콜백 함수만 정의하면 된다.

class MySpider(CrawlSpider):
    name = 'myspider'
    allowed_domains = ['example.com']
    start_urls = ['https://example.com']
    
    rules = (
        # 카테고리 페이지는 따라가지만 콜백은 하지 않음
        Rule(LinkExtractor(allow=r'/category/'), follow=True),
        # 상품 페이지는 parse_item으로 처리
        Rule(LinkExtractor(allow=r'/product/'), callback='parse_item'),
    )
    
    def parse_item(self, response):
        yield {
            'title': response.css('h1::text').get(),
            'price': response.css('span.price::text').get(),
        }

 

         - CrawlSpider의 동작 이해:

            - CrawlSpider는 parse 메소드를 오버라이드하여, 응답을 처리하고 규칙(rules)에 따라 링크를 따라간다.

            - 이 parse 메소드 내부에서 _parse_response 메소드를 호출한다.
            - _parse_response 메소드는 다음과 같은 작업을 수행한다.

               - 설정된 규칙(rules)을 확인한다.
               - 각 규칙의 LinkExtractor를 사용하여 응답에서 링크를 추출한다.
               - 추출된 각 링크에 대해 규칙에 따라 follow 할지, callback 함수를 호출할지 결정한다.
               - 필요한 경우 새 요청을 생성하여 스파이더의 엔진에 전달한다.

 

      6.8.2. _requests_to_follow(self, response)

         - 추출된 링크에서 새 Request 객체를 생성한다.

         - CrawlSpider에서는 이 메소드를 직접 호출하지는 않지만, 내부적으로 링크를 따라가는 메커니즘을 구현한다.
         - 예제:

            - 다음은 위에서 설명한 여러 메소드를 활용한 완전한 Spider 구현 예제이다.

import scrapy
from scrapy import signals
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor

class CompleteSpider(CrawlSpider):
    name = 'complete_spider'
    allowed_domains = ['example.com']
    
    def __init__(self, category=None, *args, **kwargs):
        super(CompleteSpider, self).__init__(*args, **kwargs)
        self.item_count = 0
        if category:
            self.start_urls = [f'https://example.com/category/{category}']
        else:
            self.start_urls = ['https://example.com']
    
    rules = (
        Rule(LinkExtractor(allow=r'/category/'), follow=True),
        Rule(LinkExtractor(allow=r'/product/\d+'), callback='parse_item'),
        Rule(LinkExtractor(allow=r'/page/\d+'), follow=True),
    )
    
    @classmethod
    def from_crawler(cls, crawler, *args, **kwargs):
        spider = super(CompleteSpider, cls).from_crawler(crawler, *args, **kwargs)
        crawler.signals.connect(spider.spider_opened, signal=signals.spider_opened)
        crawler.signals.connect(spider.spider_closed, signal=signals.spider_closed)
        return spider
    
    def start_requests(self):
        for url in self.start_urls:
            yield scrapy.Request(
                url=url, 
                callback=self._parse_response,
                errback=self.errback_handler,
                headers={'User-Agent': 'Mozilla/5.0 (compatible; MyBot/1.0)'}
            )
    
    def parse_item(self, response):
        self.logger.info(f"Parsing product: {response.url}")
        
        item = {
            'title': response.css('h1.title::text').get(),
            'price': response.css('span.price::text').get(),
            'description': response.css('div.description::text').get(),
            'category': response.xpath('//ul[@class="breadcrumbs"]/li[2]/a/text()').get(),
            'url': response.url,
        }
        
        self.item_count += 1
        yield item
        
        # 관련 상품 추출
        for related_url in response.css('div.related-products a::attr(href)').getall():
            yield response.follow(
                related_url, 
                callback=self.parse_item, 
                meta={'referrer': response.url}
            )
    
    def errback_handler(self, failure):
        self.logger.error(f"Request failed: {failure.request.url}")
        self.logger.error(repr(failure))
    
    def spider_opened(self, spider):
        spider.logger.info(f"Spider started: {spider.name}")
    
    def spider_closed(self, spider, reason):
        spider.logger.info(f"Spider closed: {spider.name}, reason: {reason}")
        spider.logger.info(f"Total items scraped: {self.item_count}")
        
        # 결과 요약 저장
        with open(f'{spider.name}_summary.txt', 'w') as f:
            f.write(f"Spider name: {spider.name}\n")
            f.write(f"Spider run completed at: {datetime.now()}\n")
            f.write(f"Total items scraped: {self.item_count}\n")
            f.write(f"Reason for closure: {reason}")

 

         - 동작 과정:

            - CrawlSpider가 페이지를 크롤링할 때, 정의된 Rule에 따라 LinkExtractor가 링크를 추출한다.

            - 추출된 각 링크에 대해, CrawlSpider는 내부적으로 _requests_to_follow 메소드를 호출한다.

            - _requests_to_follow 메소드는 추출된 링크를 기반으로 새로운 Request 객체를 생성합니다.
            - 이 Request 객체들은 Scrapy의 스케줄러에 추가되어 후속 크롤링에 사용됩니다.

 

         - 예제 코드에서 이 메소드가 명시적으로 나타나지 않지만, rules에 정의된 Rule 객체들이 이 프로세스를 제어한다.

rules = (
    Rule(LinkExtractor(allow=r'/category/'), follow=True),
    Rule(LinkExtractor(allow=r'/product/\d+'), callback='parse_item'),
    Rule(LinkExtractor(allow=r'/page/\d+'), follow=True),
)

 

         - 이 rules 정의에 따라 CrawlSpider는 자동으로 링크를 추출하고, _requests_to_follow를 통해 새로운 요청을 생성한다. 개발자가 직접 이 메소드를 호출할 필요 없이, CrawlSpider의 내부 로직이 이를 처리한다.

 

   6.9. 실무 시나리오 1: 로그인이 필요한 사이트 크롤링

import scrapy
from scrapy.http import FormRequest

class LoginSpider(scrapy.Spider):
    name = 'login_spider'
    start_urls = ['https://example.com/login']
    
    def parse(self, response):
        # CSRF 토큰 추출 (많은 사이트에서 필요)
        csrf_token = response.css('input[name="csrf_token"]::attr(value)').get()
        
        # 로그인 폼 데이터 준비
        return FormRequest.from_response(
            response,
            formdata={
                'csrf_token': csrf_token,
                'username': 'your_username',  # 실제 계정으로 대체
                'password': 'your_password',  # 실제 비밀번호로 대체
            },
            callback=self.after_login
        )
    
    def after_login(self, response):
        # 로그인 성공 여부 확인
        if '로그인 실패' in response.text:
            self.logger.error('로그인 실패')
            return
        
        # 로그인 성공 후 크롤링할 페이지로 이동
        yield scrapy.Request(
            url='https://example.com/protected-area',
            callback=self.parse_protected_area
        )
    
    def parse_protected_area(self, response):
        # 보호된 영역에서 데이터 추출
        for item in response.css('div.item'):
            yield {
                'title': item.css('h2::text').get(),
                'content': item.css('p.content::text').get(),
                'date': item.css('span.date::text').get(),
            }

 

   6.10. 실무 시나리오 2: 동적 콘텐츠(JavaScript 렌더링) 처리

import scrapy
from scrapy_splash import SplashRequest

class DynamicContentSpider(scrapy.Spider):
    name = 'dynamic_content'
    
    def start_requests(self):
        # JavaScript가 렌더링되는 페이지 목록
        urls = [
            'https://example.com/dynamic-page-1',
            'https://example.com/dynamic-page-2',
        ]
        
        # 각 URL에 대해 Splash 요청 생성
        for url in urls:
            # Splash를 사용하여 JavaScript 렌더링
            yield SplashRequest(
                url=url,
                callback=self.parse,
                args={
                    # 5초 동안 기다려 JavaScript가 로드되도록 함
                    'wait': 5,
                    # 스크롤을 페이지 끝까지 내림
                    'lua_source': """
                    function main(splash, args)
                        splash:go(args.url)
                        splash:wait(args.wait)
                        
                        -- 페이지를 끝까지 스크롤
                        splash:runjs([[
                            function scrollToBottom() {
                                window.scrollTo(0, document.body.scrollHeight);
                                setTimeout(scrollToBottom, 500);
                            }
                            scrollToBottom();
                        ]])
                        
                        splash:wait(2)
                        return splash:html()
                    end
                    """
                }
            )
    
    def parse(self, response):
        # 이제 모든 동적 콘텐츠가 로드된 상태에서 데이터 추출
        for product in response.css('div.product-item'):
            yield {
                'name': product.css('h3.title::text').get(),
                'price': product.css('span.price::text').get(),
                'rating': product.css('div.rating::attr(data-rating)').get(),
                'description': product.css('div.description::text').get(),
                'image_url': product.css('img::attr(src)').get(),
            }
            
        # 더 많은 제품을 로드하는 "더 보기" 버튼이 있는지 확인
        load_more = response.css('button.load-more')
        if load_more:
            # 버튼이 있다면 동적으로 처리하는 콜백을 호출
            yield SplashRequest(
                url=response.url,
                callback=self.parse_next_page,
                args={
                    'wait': 5,
                    'lua_source': """
                    function main(splash, args)
                        splash:go(args.url)
                        splash:wait(args.wait)
                        
                        -- "더 보기" 버튼 클릭
                        local button = splash:select('button.load-more')
                        if button then
                            button:click()
                            splash:wait(2)
                        end
                        
                        return splash:html()
                    end
                    """
                }
            )
    
    def parse_next_page(self, response):
        # 추가 로드된 제품 처리
        for product in response.css('div.product-item'):
            yield {
                'name': product.css('h3.title::text').get(),
                'price': product.css('span.price::text').get(),
                'rating': product.css('div.rating::attr(data-rating)').get(),
                'description': product.css('div.description::text').get(),
                'image_url': product.css('img::attr(src)').get(),
            }

 

7. Items

   7.1. Items란?

      - Scrapy에서는 Items를 사용하여 크롤링한 데이터를 저장하고 관리한다. 각각의 Item은 사전(dictionary) 형식으로 정의되며, 크롤링하고자 하는 데이터의 구조를 정의하는 역할을 한다.

      - 사용 방법:

import scrapy

class MyItem(scrapy.Item):
    title = scrapy.Field()
    link = scrapy.Field()
    description = scrapy.Field()

 

   7.2. Field 정의

      - 각각의 Item은 여러 개의 Field를 가질 수 있다. 각 Field는 해당 데이터의 속성을 나타내며, 데이터의 유형을 지정할 수 있다 (예: 문자열, 숫자 등).

      - 사용 방법:

class MyItem(scrapy.Item):
    name = scrapy.Field()
    price = scrapy.Field(serializer=float)
    category = scrapy.Field()

 

   7.3. Item 사용하기

      - 크롤러에서는 정의된 Item을 사용하여 데이터를 추출하고 저장할 수 있다. 각각의 추출된 데이터는 해당 Item의 인스턴스로 생성된다.

class MySpider(scrapy.Spider):
    name = 'example'
    
    def parse(self, response):
        item = MyItem()
        item['name'] = response.css('h1::text').get()
        item['price'] = float(response.css('.price::text').get())
        item['category'] = response.css('.category::text').get()
        yield item

 

   7.4 실무 기반의 시나리오 기반 코드 예시 (설명과 함께)

      7.4.1. 동적 웹사이트에서 데이터 추출하기

         - 동적으로 생성되는 웹 페이지에서 각 제품의 이름, 가격, 카테고리를 추출하여 MyItem으로 저장한다.

class DynamicSpider(scrapy.Spider):
    name = 'dynamic_example'
    
    def start_requests(self):
        urls = [
            'http://example.com/page1',
            'http://example.com/page2',
        ]
        for url in urls:
            yield scrapy.Request(url=url, callback=self.parse)
    
    def parse(self, response):
        for product in response.css('.product'):
            item = MyItem()
            item['name'] = product.css('.name::text').get()
            item['price'] = float(product.css('.price::text').get())
            item['category'] = product.css('.category::text').get()
            yield item

 

      7.4.2. 다중 페이지 크롤링

class MultiPageSpider(scrapy.Spider):
    name = 'multi_page_example'
    
    def start_requests(self):
        urls = [
            'http://example.com/page1',
            'http://example.com/page2',
        ]
        for url in urls:
            yield scrapy.Request(url=url, callback=self.parse)
    
    def parse(self, response):
        for item in response.css('.item'):
            yield {
                'name': item.css('.name::text').get(),
                'price': float(item.css('.price::text').get()),
                'category': item.css('.category::text').get(),
            }
        next_page = response.css('a.next_page::attr(href)').get()
        if next_page is not None:
            yield response.follow(next_page, callback=self.parse)

 

      7.4.3. 중첩된 항목 및 로더 사용

import scrapy
from scrapy.loader import ItemLoader
from scrapy.loader.processors import TakeFirst, MapCompose, Join, Identity
from w3lib.html import remove_tags

# 중첩된 Item 정의
class AuthorItem(scrapy.Item):
    name = scrapy.Field()
    birth_date = scrapy.Field()
    bio = scrapy.Field()
    
class BookItem(scrapy.Item):
    title = scrapy.Field()
    price = scrapy.Field()
    description = scrapy.Field()
    published_date = scrapy.Field()
    author = scrapy.Field()  # AuthorItem의 인스턴스가 들어갈 예정

# 항목 로더 프로세서 정의
class AuthorLoader(ItemLoader):
    default_output_processor = TakeFirst()
    
    # HTML 태그 제거 및 공백 정리
    bio_in = MapCompose(remove_tags, str.strip)
    
class BookLoader(ItemLoader):
    default_output_processor = TakeFirst()
    
    # 제목에서 특수 문자 제거
    title_in = MapCompose(remove_tags, str.strip, lambda x: x.replace('(특별판)', '').strip())
    
    # 가격 정제 (숫자만 추출)
    price_in = MapCompose(remove_tags, str.strip, lambda x: ''.join(filter(str.isdigit, x)))
    
    # 설명 텍스트 정제 및 병합
    description_in = MapCompose(remove_tags, str.strip)
    description_out = Join(' ')

# Spider에서 사용 예제
class BookStoreSpider(scrapy.Spider):
    name = 'bookstore'
    start_urls = ['https://example.com/books']
    
    def parse(self, response):
        for book_div in response.css('div.book'):
            # 책 로더 초기화
            book_loader = BookLoader(item=BookItem(), selector=book_div)
            
            # 기본 책 정보 로드
            book_loader.add_css('title', 'h2.title')
            book_loader.add_css('price', 'span.price')
            book_loader.add_css('description', 'div.description')
            book_loader.add_css('published_date', 'span.date')
            
            # 저자 페이지 URL 가져오기
            author_url = book_div.css('a.author::attr(href)').get()
            
            # 책 항목을 생성하여 저자 페이지 요청 수행
            yield response.follow(
                author_url,
                self.parse_author,
                meta={'book_loader': book_loader}
            )
    
    def parse_author(self, response):
        # 이전 요청에서 책 로더 가져오기
        book_loader = response.meta['book_loader']
        
        # 저자 로더 초기화
        author_loader = AuthorLoader(item=AuthorItem(), response=response)
        author_loader.add_css('name', 'h1.author-name')
        author_loader.add_css('birth_date', 'span.birth-date')
        author_loader.add_css('bio', 'div.biography')
        
        # 저자 항목 로드
        author = author_loader.load_item()
        
        # 책 로더에 저자 추가
        book_loader.add_value('author', dict(author))
        
        # 완성된 책 항목 반환
        yield book_loader.load_item()

 

      7.4.4. 사용자 정의 데이터 검증 및 변환

import re
import datetime
from scrapy import Item, Field
from scrapy.loader import ItemLoader
from scrapy.loader.processors import MapCompose, TakeFirst
from itemloaders.processors import SelectJmes

# 데이터 검증 및 변환을 위한 사용자 정의 함수
def validate_price(value):
    """가격 문자열을 정수로 변환하고 유효성 검사"""
    try:
        # 가격 문자열에서 숫자만 추출
        numeric_str = ''.join(filter(str.isdigit, value))
        return int(numeric_str)
    except ValueError:
        # 유효하지 않은 가격은 None 반환
        return None

def parse_date(value):
    """다양한 날짜 형식을 처리하여 ISO 형식으로 변환"""
    # 여러 가능한 날짜 형식 정의
    date_formats = [
        r'(\d{4})년 (\d{1,2})월 (\d{1,2})일',  # 2023년 5월 15일
        r'(\d{4})-(\d{1,2})-(\d{1,2})',       # 2023-5-15
        r'(\d{1,2})/(\d{1,2})/(\d{4})'        # 5/15/2023
    ]
    
    for pattern in date_formats:
        match = re.search(pattern, value)
        if match:
            # 패턴에 따라 그룹 순서 조정
            if pattern == date_formats[0] or pattern == date_formats[1]:
                year, month, day = match.groups()
            else:  # 미국식 날짜 형식
                month, day, year = match.groups()
                
            try:
                date_obj = datetime.date(int(year), int(month), int(day))
                return date_obj.isoformat()  # ISO 형식 (YYYY-MM-DD)
            except ValueError:
                pass
    
    return None  # 어떤 형식과도 일치하지 않으면 None 반환

def clean_text(value):
    """텍스트 청소: HTML 태그 제거, 공백 정리, 특수 문자 처리"""
    # HTML 태그 제거
    value = re.sub(r'<[^>]+>', '', value)
    # 여러 개의 공백을 하나로 병합
    value = re.sub(r'\s+', ' ', value)
    # 특수 문자 처리 (예: 따옴표)
    value = value.replace('&quot;', '"').replace('&amp;', '&')
    return value.strip()

class ProductItem(Item):
    # 기본 필드 정의
    id = Field()
    name = Field()
    price = Field()
    description = Field()
    categories = Field()
    availability = Field()
    rating = Field()
    reviews_count = Field()
    release_date = Field()
    manufacturer = Field()
    specifications = Field()  # JSON 형식의 사양 데이터
    
class ProductLoader(ItemLoader):
    default_output_processor = TakeFirst()
    
    # 입력 프로세서 정의
    price_in = MapCompose(clean_text, validate_price)
    description_in = MapCompose(clean_text)
    categories_in = MapCompose(clean_text)
    categories_out = list  # 카테고리는 목록으로 유지
    release_date_in = MapCompose(clean_text, parse_date)
    rating_in = MapCompose(
        lambda x: float(x) if re.match(r'^\d+(\.\d+)?$', x.strip()) else None
    )
    reviews_count_in = MapCompose(
        lambda x: int(''.join(filter(str.isdigit, x))) if x else 0
    )
    specifications_in = MapCompose(clean_text)
    specifications_out = SelectJmes('specs')  # JSON 데이터에서 'specs' 키 선택

class ProductSpider(scrapy.Spider):
    name = 'product_details'
    start_urls = ['https://example.com/products/category']
    
    def parse(self, response):
        # 제품 목록 페이지에서 각 제품 링크 추출
        for product_link in response.css('a.product-link::attr(href)').getall():
            yield response.follow(product_link, self.parse_product)
        
        # 페이지네이션 처리
        next_page = response.css('a.next-page::attr(href)').get()
        if next_page:
            yield response.follow(next_page, self.parse)
    
    def parse_product(self, response):
        loader = ProductLoader(ProductItem(), response)
        
        # 기본 제품 정보 추출
        loader.add_css('id', 'div.product::attr(data-product-id)')
        loader.add_css('name', 'h1.product-name')
        loader.add_css('price', 'span.price')
        loader.add_css('description', 'div.description')
        loader.add_css('availability', 'span.availability::text')
        loader.add_css('rating', 'div.rating::attr(data-rating)')
        loader.add_css('reviews_count', 'span.reviews-count::text')
        loader.add_css('release_date', 'span.release-date::text')
        loader.add_css('manufacturer', 'span.manufacturer::text')
        
        # 카테고리는 여러 개일 수 있음
        loader.add_css('categories', 'ul.categories li::text')
        
        # 규격 정보는 JSON으로 저장된 경우가 많음
        loader.add_css('specifications', 'script#product-specs::text')
        
        yield loader.load_item()

 

8. Middleware

   8.1 개요

      - Scrapy 미들웨어는 크롤러의 요청(Request), 응답(Response), 예외(Exception) 흐름에 개입하여 사용자 정의 로직을 추가할 수 있는 훅(Hook) 역할을 한다. 이를 통해 요청 헤더를 수정하거나, 프록시 설정, 로깅, 에러 핸들링 등 다양한 기능을 구현할 수 있다.

 

      - 스파이더 미들웨어: 

         - 크롤러가 다운받은 응답(response)이 스파이더로 전달되기 전후에 처리할 수 있도록 도와준다.

      - 역할: 

         - 응답이 Spider로 전달되기 전과 Spider가 반환한 결과를 처리할 수 있음
      - 예시 메소드: 

         - process_spider_input, process_spider_output, process_start_requests, spider_opened

      - 다운로더 미들웨어: 

         - 실제 HTTP 요청(request) 및 응답(response)의 전후처리를 담당한다.

      - 역할: 

         - Downloader와 Scrapy 엔진 사이에서 요청과 응답을 중간에 가로채고 처리한다.
      - 예시 메소드: 

         - process_request, process_response, process_exception, spider_opened

 

      - 또한, 요청/응답 관련 작업 외에도 사용자 에이전트나 프록시를 동적으로 변경하는 등 다양한 기능을 구현할 수 있다.

 

      - 미들웨어는 설정 파일(settings.py)에 등록하여 활성화하며, 각 미들웨어는 특정 이벤트(예: 요청 전송, 응답 수신, 예외 발생)에 개입하여 사용자 정의 로직을 실행할 수 있다.

 

      8.1.2. 기본으로 생성되는 middleware 메소드들의 구조 및 주석

# 스파이더 미들웨어를 위한 모델들을 여기에 정의합니다.
#
# 문서는 다음 링크를 참고하세요:
# https://docs.scrapy.org/en/latest/topics/spider-middleware.html

from scrapy import signals

# 다양한 아이템 타입을 하나의 인터페이스로 다루기 위해 사용
from itemadapter import is_item, ItemAdapter


class TempSpiderMiddleware:
    # 모든 메서드를 정의할 필요는 없습니다. 만약 어떤 메서드가 정의되지 않으면,
    # Scrapy는 해당 스파이더 미들웨어가 전달된 객체들을 수정하지 않는 것으로 간주합니다.

    @classmethod
    def from_crawler(cls, crawler):
        # 이 메서드는 Scrapy가 스파이더를 생성할 때 사용됩니다.
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    def process_spider_input(self, response, spider):
        # 스파이더 미들웨어를 통과하여 스파이더로 들어가는 각 응답(response)에 대해 호출됩니다.
        #
        # None을 반환하거나 예외를 발생시켜야 합니다.
        return None

    def process_spider_output(self, response, result, spider):
        # 응답(response)을 처리한 후 스파이더가 반환한 결과(result)들과 함께 호출됩니다.
        #
        # Request 객체나 아이템 객체들이 포함된 반복 가능한(iterable) 객체를 반환해야 합니다.
        for i in result:
            yield i

    def process_spider_exception(self, response, exception, spider):
        # 스파이더 또는 다른 스파이더 미들웨어의 process_spider_input() 메서드가 예외를 발생시켰을 때 호출됩니다.
        #
        # None 또는 Request나 아이템 객체들이 포함된 반복 가능한(iterable) 객체를 반환해야 합니다.
        pass

    def process_start_requests(self, start_requests, spider):
        # 스파이더의 시작 요청(start requests)과 함께 호출되며,
        # process_spider_output() 메서드와 유사하게 작동하지만 응답(response)과는 관련이 없습니다.
        #
        # 오직 Request 객체만 반환해야 합니다(아이템은 아님).
        for r in start_requests:
            yield r

    def spider_opened(self, spider):
        # 스파이더가 열릴 때 호출됩니다.
        spider.logger.info("Spider opened: %s" % spider.name)


class TempDownloaderMiddleware:
    # 모든 메서드를 정의할 필요는 없습니다. 만약 어떤 메서드가 정의되지 않으면,
    # Scrapy는 해당 다운로더 미들웨어가 전달된 객체들을 수정하지 않는 것으로 간주합니다.

    @classmethod
    def from_crawler(cls, crawler):
        # 이 메서드는 Scrapy가 스파이더를 생성할 때 사용됩니다.
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    def process_request(self, request, spider):
        # 다운로더 미들웨어를 통과하는 각 요청(request)에 대해 호출됩니다.
        #
        # 다음 중 하나를 반환해야 합니다:
        # - None: 이 요청의 처리를 계속 진행
        # - 또는 Response 객체 반환
        # - 또는 Request 객체 반환
        # - 또는 IgnoreRequest 예외 발생: 이 경우 설치된 다운로더 미들웨어의 process_exception() 메서드가 호출됩니다.
        return None

    def process_response(self, request, response, spider):
        # 다운로더로부터 반환된 응답(response)과 함께 호출됩니다.
        #
        # 다음 중 하나를 반환해야 합니다:
        # - Response 객체
        # - Request 객체
        # - 또는 IgnoreRequest 예외 발생
        return response

    def process_exception(self, request, exception, spider):
        # 다운로드 핸들러 또는 다른 다운로더 미들웨어의 process_request() 메서드가 예외를 발생시켰을 때 호출됩니다.
        #
        # 다음 중 하나를 반환해야 합니다:
        # - None: 이 예외의 처리를 계속 진행
        # - 또는 Response 객체 반환: process_exception() 체인을 중단
        # - 또는 Request 객체 반환: process_exception() 체인을 중단
        pass

    def spider_opened(self, spider):
        # 스파이더가 열릴 때 호출됩니다.
        spider.logger.info("Spider opened: %s" % spider.name)

 

      8.1.3. 전체 처리 순서 (요청/응답 흐름 기준)

         8.1.3.1. 스파이더 시작 시:
            - from_crawler:
               - 각 미들웨어 인스턴스가 생성되고, spider_opened 시그널이 등록

 

            - spider_opened:
               - 스파이더가 열릴 때 호출되어 초기화 및 로깅 작업 수행

 

         8.1.3.2. 스파이더의 시작 요청 처리:
            - process_start_requests (Spider Middleware):
               - 스파이더가 생성한 시작 요청(Request)들을 수정, 추가, 필터링 후 엔진에 전달

 

         8.1.3.3. 네트워크 요청 전 처리
            - process_request (Downloader Middleware):
               - 각 요청이 네트워크로 전송되기 전에 호출되어 요청 전처리(헤더, 프록시 등) 수행

 

         8.1.3.4. 네트워크 요청 및 응답:

            - 다운로더가 요청을 처리하고 응답(Response)을 받음

 

         8.1.3.5. 네트워크 응답 후 처리:
            - process_response (Downloader Middleware):
               - 다운로더에서 받은 응답을 처리하거나 수정 후 스파이더로 전달

 

         8.1.3.6. 스파이더 입력 처리:

            - process_spider_input (Spider Middleware):
               - 응답이 스파이더 콜백에 전달되기 전에 전처리(검증, 수정 등) 수행

 

         8.1.3.7. 스파이더 콜백 실행 및 결과 처리:
            - 스파이더 콜백(예: parse 함수)이 응답을 처리하여 아이템이나 새로운 요청(Request)을 반환
            - process_spider_output (Spider Middleware):
               - 스파이더 콜백의 결과를 추가 처리 후 반환

 

         8.1.3.8. 예외 처리:
            - 스파이더 처리 중 예외가 발생하면 process_spider_exception가 호출됨
            - 다운로더 처리 중 예외가 발생하면 process_exception이 호출됨

 

   8.2. 클래스별 미들웨어 설명

      8.2.1. RotateUserAgentAndProxyMiddleware

         - 이 미들웨어는 요청마다 랜덤하게 User-Agent와 Proxy를 선택하여 적용하며, 응답 상태가 200이 아닐 경우나 예외가 발생한 경우 새로운 User-Agent와 Proxy로 재요청을 수행한다.

 

         8.2.1.1. 주요 메소드 및 파라미터

            8.2.1.1.1.  __init__(self, user_agents, proxies)

               - 설명:

                  - 인스턴스를 초기화하며, 사용할 User-Agent 리스트와 Proxy 리스트를 인자로 받는다.

                  - 인스턴스 초기화 시 전달된 리스트를 저장하여 이후 요청/응답 처리에 사용한다.


               - 파라미터:
                   - user_agents: 문자열로 구성된 User-Agent 리스트
                   - proxies: 문자열로 구성된 Proxy 리스트

 

            8.2.1.1.2. @classmethod from_crawler(cls, crawler)

               - 설명:

                   - Scrapy가 미들웨어를 생성할 때 호출되는 클래스 메소드로, 크롤러 설정에서 USER_AGENT_LIST와 PROXY_LIST를 가져온다.

               - 파라미터:

                   - crawler: Scrapy의 크롤러 객체로, 설정(settings) 등 설정 및 신호 연결 기능과 다양한 정보를 포함한다.

# settings.py에 다음과 같이 설정되어 있어야 함
USER_AGENT_LIST = ['agent1', 'agent2']
PROXY_LIST = ['http://proxy1', 'http://proxy2']

 

            8.2.1.1.3. process_request(self, request, spider)

               - 설명:

                  - 각 요청(request)이 다운로더에 전달되기 전 호출된다. 요청 헤더에 랜덤 User-Agent를 설정하고, meta에 Proxy를 지정한다.

               - 파라미터:

                  - request: 현재 처리 중인 처리할 HTTP 요청 객체
                  - spider: 현재 실행 중인 요청을 발생시킨 스파이더 객체

 

            8.2.1.1.4. process_response(self, request, response, spider)

               - 설명:

                  - 다운로더에서 응답(response)을 받은 후 호출됩니다. 응답 상태가 200이 아닌 경우 새로운 요청으로 교체하여 재시도한다.

               - 주의사항:

                  - 재시도 시 dont_filter=True를 설정하여 중복 필터링을 방지한다.

               - 반환값:

                  - 정상 응답인 경우 response 그대로 반환

                  - 오류인 경우 새로운 요청(new_request) 객체를 반환하여 재요청 진행

               - 파라미터:

                  - request: 원본 Request 객체

                  - response: 다운로더가 반환한 Response 객체

                  - spider: 현재 실행 중인 Spider 객체

 

            8.2.1.1.5. process_exception(self, request, exception, spider)

               - 설명:

                  - 요청 처리 중 예외가 발생하면 호출된다. 예외 상황에서도 새로운 User-Agent와 Proxy를 적용한 후 요청을 재시도한다.

               - 반환값: 

                  - 새로운 요청 객체(new_request)로 재요청

               - 파라미터:

                  - request: 예외 발생 당시의 Request 객체

                  - exception: 발생한 Exception 객체

                  - spider: 현재 실행 중인 Spider 객체

 

               - 간단한 사용 예제 (RotateUserAgentAndProxyMiddleware)

# settings.py 예시
USER_AGENT_LIST = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
]
PROXY_LIST = [
    "http://123.123.123.123:8080",
    "http://234.234.234.234:3128"
]

DOWNLOADER_MIDDLEWARES = {
    'myproject.middlewares.RotateUserAgentAndProxyMiddleware': 543,
}

 

               - 실무 시나리오 예제 1

                   - 사이트 차단 우회를 위해 요청마다 다른 User-Agent와 Proxy를 적용하고, 응답이 실패한 경우 재시도한다.

# middlewares.py 내에 정의된 RotateUserAgentAndProxyMiddleware를 사용
import random
from scrapy import signals

class RotateUserAgentAndProxyMiddleware:
    def __init__(self, user_agents, proxies):
        # 초기화: User-Agent와 Proxy 리스트를 인스턴스 변수로 저장
        self.user_agents = user_agents
        self.proxies = proxies

    @classmethod
    def from_crawler(cls, crawler):
        # 크롤러 설정에서 리스트를 가져와 미들웨어 인스턴스 생성
        user_agents = crawler.settings.getlist('USER_AGENT_LIST')
        proxies = crawler.settings.getlist('PROXY_LIST')
        return cls(user_agents, proxies)

    def process_request(self, request, spider):
        # 각 요청에 대해 랜덤한 User-Agent와 Proxy 적용
        user_agent = random.choice(self.user_agents)
        proxy = random.choice(self.proxies)
        request.headers['User-Agent'] = user_agent
        request.meta['proxy'] = proxy
        spider.logger.info(f'Using user-agent: {user_agent}, proxy: {proxy}')

    def process_response(self, request, response, spider):
        # 응답 상태가 200이 아닐 경우, 새로운 User-Agent와 Proxy로 재시도
        if response.status != 200:
            user_agent = random.choice(self.user_agents)
            proxy = random.choice(self.proxies)
            spider.logger.info(f'Non-200 response ({response.status}). Retrying with user-agent: {user_agent}, proxy: {proxy}')
            new_request = request.copy()
            new_request.headers['User-Agent'] = user_agent
            new_request.meta['proxy'] = proxy
            new_request.dont_filter = True  # 중복 요청 필터링 방지
            return new_request
        return response

    def process_exception(self, request, exception, spider):
        # 예외 발생 시, 다른 User-Agent와 Proxy로 재시도
        user_agent = random.choice(self.user_agents)
        proxy = random.choice(self.proxies)
        spider.logger.info(f'Exception {exception}. Retrying with user-agent: {user_agent}, proxy: {proxy}')
        new_request = request.copy()
        new_request.headers['User-Agent'] = user_agent
        new_request.meta['proxy'] = proxy
        new_request.dont_filter = True
        return new_request

# 사용 예: settings.py에 미들웨어 등록 후 실행하면,
# 사이트 접근 시 차단을 우회하여 정상적인 응답을 얻을 확률을 높일 수 있습니다.

 

               - 실무 시나리오 예제 2

                   - 설명:
                      - 추가 기능:
                          - 최대 재시도 횟수(max_retries)를 설정하여 응답 실패나 예외 발생 시 재시도 횟수를 제한한다.
                          - 각 요청에 retry_times를 메타에 기록하여 재시도 횟수를 추적한다.

import random
from scrapy import signals

class AdvancedRotateUserAgentAndProxyMiddleware:
    def __init__(self, user_agents, proxies, max_retries=3):
        self.user_agents = user_agents
        self.proxies = proxies
        self.max_retries = max_retries

    @classmethod
    def from_crawler(cls, crawler):
        # 크롤러 설정에서 필요한 리스트와 추가 설정값을 가져옴
        user_agents = crawler.settings.getlist('USER_AGENT_LIST')
        proxies = crawler.settings.getlist('PROXY_LIST')
        max_retries = crawler.settings.getint('ROTATE_MAX_RETRIES', 3)
        return cls(user_agents, proxies, max_retries)

    def process_request(self, request, spider):
        # 요청 전: 무작위 User-Agent와 Proxy를 설정
        user_agent = random.choice(self.user_agents)
        proxy = random.choice(self.proxies)
        request.headers['User-Agent'] = user_agent
        request.meta['proxy'] = proxy
        spider.logger.debug(f'[Request] Using UA: {user_agent} | Proxy: {proxy}')

    def process_response(self, request, response, spider):
        # 응답 후: 상태 코드가 200이 아니면 재시도 로직 적용
        if response.status != 200:
            retries = request.meta.get('retry_times', 0)
            if retries < self.max_retries:
                user_agent = random.choice(self.user_agents)
                proxy = random.choice(self.proxies)
                spider.logger.warning(
                    f"[Response] Status {response.status}. Retry {retries+1}/{self.max_retries} with UA: {user_agent}, Proxy: {proxy}"
                )
                new_request = request.copy()
                new_request.headers['User-Agent'] = user_agent
                new_request.meta['proxy'] = proxy
                new_request.meta['retry_times'] = retries + 1
                new_request.dont_filter = True  # 중복 요청 필터링 해제
                return new_request
            else:
                spider.logger.error(f"[Response] Max retries exceeded for {request.url}")
        # 정상 응답 또는 최대 재시도 초과 시 원본 응답 반환
        return response

    def process_exception(self, request, exception, spider):
        # 예외 발생 시, 최대 재시도 횟수 내에 재시도하도록 처리
        retries = request.meta.get('retry_times', 0)
        if retries < self.max_retries:
            user_agent = random.choice(self.user_agents)
            proxy = random.choice(self.proxies)
            spider.logger.warning(
                f"[Exception] {exception}. Retry {retries+1}/{self.max_retries} with UA: {user_agent}, Proxy: {proxy}"
            )
            new_request = request.copy()
            new_request.headers['User-Agent'] = user_agent
            new_request.meta['proxy'] = proxy
            new_request.meta['retry_times'] = retries + 1
            new_request.dont_filter = True
            return new_request
        else:
            spider.logger.error(f"[Exception] Max retries exceeded for {request.url}. Exception: {exception}")
            # None을 반환하면 예외 체인이 다음 미들웨어로 넘어감
            return None

    def spider_opened(self, spider):
        spider.logger.info(f"Advanced middleware enabled for spider: {spider.name}")

 

      8.2.2. RotateUserAgentMiddleware

         - 이 미들웨어는 요청마다 랜덤하게 User-Agent만을 선택하여 적용한다. Proxy 설정은 하지 않는다.

 

         - 주요 메소드 및 파라미터

            - __init__(self, user_agents)

               - 설명: 

                  - User-Agent 리스트를 받아 인스턴스를 초기화한다.
               - 파라미터:

                  - user_agents: 문자열로 구성된 User-Agent 리스트

 

            - @classmethod from_crawler(cls, crawler)

                - 설명: 

                   - 크롤러 설정에서 USER_AGENT_LIST를 가져와 인스턴스를 생성한다.

                - 파라미터:

                   - crawler: Scrapy 크롤러 객체


            - process_request(self, request, spider)

                - 설명: 

                   - 각 요청마다 랜덤하게 선택한 User-Agent를 요청 헤더에 설정한다.
                - 파라미터:

                   - request: 현재 처리 중인 Request 객체

                   - spider: 현재 실행 중인 Spider 객체

 

         - 간단한 사용 예제 (RotateUserAgentMiddleware)

# settings.py
USER_AGENT_LIST = [
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64)...',
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...',
]
PROXY_LIST = [
    'http://proxy1.example.com:8000',
    'http://proxy2.example.com:8000',
]
ROTATE_MAX_RETRIES = 3
# settings.py 예시
USER_AGENT_LIST = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
]

DOWNLOADER_MIDDLEWARES = {
    'myproject.middlewares.RotateUserAgentMiddleware': 544,
}

 

         - 실무 시나리오 예제

             - 기본적인 User-Agent 회전을 통해 스크래핑 중 차단 위험을 낮추는 경우이다.

# middlewares.py 내에 정의된 RotateUserAgentMiddleware 사용 예제
import random

class RotateUserAgentMiddleware:
    def __init__(self, user_agents):
        # 초기화: User-Agent 리스트 저장
        self.user_agents = user_agents

    @classmethod
    def from_crawler(cls, crawler):
        # 설정에서 USER_AGENT_LIST를 가져와 인스턴스 생성
        user_agents = crawler.settings.getlist('USER_AGENT_LIST')
        return cls(user_agents)

    def process_request(self, request, spider):
        # 요청마다 랜덤한 User-Agent 적용
        user_agent = random.choice(self.user_agents)
        request.headers['User-Agent'] = user_agent
        spider.logger.info(f'Using user-agent: {user_agent}')
        
# 실무 사용 시나리오:
# 특정 웹사이트가 User-Agent 기반 차단을 시도할 때,
# 다양한 User-Agent로 변경하여 접근하면 IP 차단 등의 위험을 줄일 수 있습니다.

 

      8.2.3. BasicsSpiderMiddleware

         - 이 미들웨어는 스파이더 미들웨어의 기본 골격을 제공한다. 모든 메소드가 반드시 구현될 필요는 없으며, 정의하지 않은 메소드는 Scrapy가 별도의 처리 없이 통과시킨다.

 

         8.2.3.1. 주요 메소드 및 파라미터

            - @classmethod from_crawler(cls, crawler)

               - 설명: 

                  - 크롤러로부터 미들웨어 인스턴스를 생성하며, 스파이더 열림 신호(spider_opened)에 연결한다.
               - 파라미터:

                  - crawler: 크롤러 객체

 

            - process_spider_input(self, response, spider)

               - 설명: 

                  - 스파이더로 전달되기 전에 각 응답(response)에 대해 호출된다.

                  - Spider에 전달되기 전, 응답 데이터를 전처리하거나 검증할 수 있다.
                  - 기본적으로 None을 반환하며, 예외 발생 시 에러 핸들링이 가능하다.

               - 파라미터:

                  - response: 다운로더로부터 전달받은 Spider로 전달되는 Response 객체
                  - spider: 응답을 처리할 현재 실행 중인 Spider 객체

               - 반환값: 반드시 None을 반환하거나 예외를 발생시킨다.

 

             - process_spider_output(self, response, result, spider)
                - 설명: 

                   - 스파이더가 응답을 처리한 후 반환한 결과(result)를 순회(iterable)하면서 추가 처리를 할 수 있다.
                - 파라미터:

                   - response: Spider가 처리한 Response 원본 응답 객체
                   - result: Spider가 반환한 결과(아이템 또는 Request)의 iterable
                   - spider: 현재 실행 중인 Spider 객체

                - 반환값: 

                   - 처리된 결과의 iterable (아이템 또는 Request)

 

             - process_spider_exception(self, response, exception, spider)

                - 설명: 

                   - 스파이더 또는 process_spider_input()에서 예외가 발생할 때 호출된다.
                - 파라미터:

                   - response: 예외 발생 시 관련된 Response 객체

                   - exception: 발생한 예외 객체

                   - spider: 현재 실행 중인 Spider 객체

 

                - 반환값: 

                   - None 또는 Request/아이템 객체들의 iterable

 

             - process_start_requests(self, start_requests, spider)

                - 설명: 

                   - 스파이더의 시작 요청(start_requests)에 대해 호출되며, 응답이 없는 요청들을 처리한다.

                   - Spider가 시작할 때 요청 목록을 미들웨어에서 가공하거나 필터링할 수 있다.
                - 파라미터:

                   - start_requests: 스파이더가 시작할 때 생성한 요청들의 iterable
                   - spider: 현재 실행 중인 Spider 객체

                - 반환값:

                   - Request 객체들의 iterable

 

             - spider_opened(self, spider)

                - 설명: 

                   - 스파이더가 열릴 때 호출되며, 보통 초기 로깅이나 설정 작업에 사용된다.
                - 파라미터:

                   - spider: 시작된 Spider 객체

 

            - 간단한 사용 예제 (BasicsSpiderMiddleware)

# settings.py 예시
SPIDER_MIDDLEWARES = {
    'myproject.middlewares.BasicsSpiderMiddleware': 543,
}

 

         - 실무 시나리오 예제

            - 스파이더 미들웨어를 확장하여 응답 전처리 및 오류 로깅을 추가한다.

# middlewares.py 내에 BasicsSpiderMiddleware 확장 예제
from scrapy import signals

class BasicsSpiderMiddleware:
    @classmethod
    def from_crawler(cls, crawler):
        # 크롤러로부터 미들웨어 인스턴스 생성 및 spider_opened 신호 연결
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    def process_spider_input(self, response, spider):
        # 응답을 스파이더에 전달하기 전 전처리 작업 (예: 특정 HTML 요소 제거)
        spider.logger.debug("Processing response before spider: %s" % response.url)
        return None

    def process_spider_output(self, response, result, spider):
        # 스파이더의 출력 결과를 추가 가공하거나 로깅하는 작업
        for item in result:
            spider.logger.debug("Spider yielded: %s" % item)
            yield item

    def process_spider_exception(self, response, exception, spider):
        # 스파이더 처리 중 발생한 예외를 로깅하거나 별도 처리
        spider.logger.error("Exception caught in spider processing: %s" % exception)
        # 별도의 처리를 하지 않고, None 반환 시 다음 미들웨어로 넘어감

    def process_start_requests(self, start_requests, spider):
        # 시작 요청에 대해 추가 전처리 작업 가능
        for request in start_requests:
            spider.logger.debug("Start request: %s" % request.url)
            yield request

    def spider_opened(self, spider):
        # 스파이더가 열릴 때 초기 설정 및 로깅
        spider.logger.info("Spider opened: %s" % spider.name)

# 실무에서는 이 미들웨어를 확장하여 요청/응답을 분석하거나, 동적 컨텐츠 처리를 위한 사전 작업을 할 수 있습니다.

 

      8.2.4. BasicsDownloaderMiddleware

         - 이 미들웨어는 다운로더 미들웨어의 기본 골격을 제공한다. 다운로더 미들웨어는 HTTP 요청 전송, 응답 수신, 그리고 예외 발생 시의 처리를 담당한다.

 

         - 주요 메소드 및 파라미터

            - @classmethod from_crawler(cls, crawler)
               - 설명: 

                  - 크롤러에서 미들웨어 인스턴스를 생성하고, 스파이더 열림 신호에 연결한다.
               - 파라미터:

                  - crawler: Scrapy의 크롤러 객체

 

            - process_request(self, request, spider)
               - 설명:

                  - 요청을 전처리할 수 있으며, None을 반환하면 이후 미들웨어로 진행하여 Response 객체를 반환하면 바로 해당 응답으로 대체, Request 객체를 반환하면 재요청 처리, 예외를 발생시켜 에러 체인을 실행할 수 있다.

               - 파라미터:

                  - request: Downloader로 전달되기 전의 Request 객체
                  - spider: 현재 실행 중인 Spider 객체

               - 반환값:

                  - None (요청 계속 처리),

                  - Response (요청 중단 후 응답 반환)

                  - Request (새로운 요청 반환)
                   - 예외 발생 시 IgnoreRequest

 

            - process_response(self, request, response, spider)

                - 설명:

                   - 응답 데이터를 후처리하며, 최종적으로 Response 또는 새로운 Request를 반환한다.

                - 파라미터:

                   - request: 원본 Request 객체
                   - response: Downloader에서 반환한 Response 객체
                   - spider: 현재 실행 중인 Spider 객체

                - 반환값:

                   - Response (정상 응답)
                   - Request (재요청)
                   - 예외 발생 시 IgnoreRequest

 

            - process_exception(self, request, exception, spider)
                - 설명:

                    - 요청 처리 도중 예외가 발생할 경우 호출되며, None을 반환하면 이후 미들웨어에 예외 처리를 위임한다.
                    - Response나 Request를 반환하면 체인을 중단하고 해당 값을 사용한다.

                - 파라미터:
                    - request: 예외 발생 당시의 Request 객체
                    - exception: 발생한 Exception 객체
                    - spider: 현재 실행 중인 Spider 객체

                - 반환값:

                    - None (예외 처리 계속)
                    - Response 또는 Request (예외 체인 중단)

 

            - spider_opened(self, spider)
               - 설명:

                  - Spider가 시작될 때 호출되어 초기화 작업이나 로깅 작업을 수행한다.
               - 파라미터:

                   - spider: 시작된 Spider 객체


            - 간단한 사용 예제 (BasicsDownloaderMiddleware)

# settings.py 예시
DOWNLOADER_MIDDLEWARES = {
    'myproject.middlewares.BasicsDownloaderMiddleware': 543,
}

 

            - 실무 시나리오 예제

               - 다운로드 중 발생할 수 있는 예외를 로깅하고, 비정상적인 응답에 대해 추가 처리를 한다.

# middlewares.py 내 BasicsDownloaderMiddleware 확장 예제
from scrapy import signals

class BasicsDownloaderMiddleware:
    @classmethod
    def from_crawler(cls, crawler):
        # 크롤러에서 미들웨어 인스턴스 생성 및 spider_opened 신호 연결
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    def process_request(self, request, spider):
        # 요청 전처리: 예를 들어, 요청 URL 로깅
        spider.logger.debug("Processing request: %s" % request.url)
        return None

    def process_response(self, request, response, spider):
        # 응답 후처리: 상태 코드 체크 후 추가 로깅
        if response.status != 200:
            spider.logger.warning("Received non-200 response: %s" % response.status)
        return response

    def process_exception(self, request, exception, spider):
        # 예외 발생 시 로깅 및 대체 응답 생성 (필요 시)
        spider.logger.error("Exception in request %s: %s" % (request.url, exception))
        # 예외 발생 시 None을 반환하면 다음 미들웨어로 넘어갑니다.
        return None

    def spider_opened(self, spider):
        # 스파이더 시작 시 로깅
        spider.logger.info("Downloader middleware activated for spider: %s" % spider.name)

# 실무 시나리오:
# 만약 사이트에서 간헐적인 네트워크 장애가 발생한다면, process_exception에서 이를 감지하고 추가 로깅 또는 재시도 로직을 삽입할 수 있습니다.

 

   8.3. 미들웨어 사용 시 기본 설정 및 등록 방법

      - Scrapy 설정 파일(settings.py)에서 미들웨어를 등록할 때, 순서(priority)에 따라 실행 순서가 결정된다.

DOWNLOADER_MIDDLEWARES = {
    'myproject.middlewares.RotateUserAgentAndProxyMiddleware': 543,
    'myproject.middlewares.RotateUserAgentMiddleware': 544,
    'myproject.middlewares.BasicsDownloaderMiddleware': 545,
}
SPIDER_MIDDLEWARES = {
    'myproject.middlewares.BasicsSpiderMiddleware': 543,
}

 

      - 숫자가 작을수록 먼저 실행된다.

      - 또한, 미들웨어 내에서 크롤러의 설정 값을 읽어오기 위해 from_crawler 메소드를 활용하며, 스파이더나 다운로더 신호에 연결하여 추가 초기화를 진행할 수 있다.

 

- reference : 
https://docs.scrapy.org/en/latest/intro/tutorial.html#

https://docs.scrapy.org/en/latest/topics/shell.html

https://docs.scrapy.org/en/latest/topics/spider-middleware.html

 

댓글