SCRAPY 프레임워크의 사용 방법 정리 (1)
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('"', '"').replace('&', '&')
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
'Crawling' 카테고리의 다른 글
Scrapy + Playwright 매뉴얼 - 기본부터 실전까지 (1) | 2025.03.02 |
---|---|
SCRAPY 프레임워크의 사용 방법 정리 (2) (0) | 2025.02.28 |
[udemy] Web Scraping with BeautifulSoup, Selenium, Scrapy and Scrapy-Playwright. 4 Project-like Exercises + 4 Real Life Projects 학습 정리 (0) | 2025.02.26 |
[udemy] Web Scraping with Python: BeautifulSoup, Requests & Selenium 학습 정리 (0) | 2025.02.23 |
BeautifulSoup4 기본 사용방법 정리 (0) | 2025.02.22 |
댓글