티스토리 뷰

개발/python

웹크롤러 scrapy를 소개합니다

Jaeyeon Baek 2021. 7. 15. 16:43

scrapy는 웹사이트에서 필요한 데이터를 추출하는 오픈소스 프레임워크입니다. 네, 많고 많은 crawler 중에 하나입니다. 혹시 듣보잡 아니냐고요? 네, 뭐 구글 트렌드로 다른 크롤러와 비교해보면 크게 뒤떨어지는 건 사실입니다. (지난 5년간 대한민국 기준 트렌드 자료입니다. 파란색이 selenium, 노란색이 beautifulsoup, 빨간색이 scrapy)

구글 트렌드

GitHub에 Star를 인기의 척도라고 본다면 scrapy는 상당히 인기 있는 프레임워크로 볼 수 있습니다. 이 말인즉, 개발자에게는 꽤나 인기 있는 크롤러라는 겁니다. 아래를 보세요. 스타 수가 무려 41.1k 개고 아직도 활발히 개발되고 있습니다. 

https://github.com/scrapy/scrapy

scrapy는 가볍고, 빠르고, 확장성이 좋습니다. 개발자는 파이썬 기반으로 spider라고 하는 코드를 작성하면 되거든요. 

 

# 주요 특징 

scrapy의 특징은 아래와 같이 정리됩니다.

  • 비동기 네트워킹 라이브러리(asynchronous networking library)인 Twisted를 기반으로 하기 때문에 매우 우수한 성능을 발휘합니다. 또한 셀레니움과 마찬가지로 XPath, CSS 표현식으로 HTML 소스에서 데이터 추출이 가능합니다. 한편, 셀레니움과 다르게 webdriver를 사용하지 않습니다.
  • 셀레니움은 페이지를 렌더링 하기 위해 필요한 js, css 그리고 image 파일까지 불러오며 난리가 나죠. 하지만 scrapy는 지정된 url만 조회합니다. 그렇기 때문에 scrapy가 셀레니움보다 가볍고 빠른 퍼포먼스를 낼 수 있는 겁니다. curl 커맨드, 혹은 python에서 requests 라이브러리를 사용해서 URL에 get 메소드를 날리는 걸 상상해보세요. 간결한 구성으로 빠르게 응답받을 수 있겠죠!

 

# 예제 코드 

다음으로 예제 코드를 살펴보겠습니다. ( 공식 코드에 main만 추가한 코드입니다 )

# quotes_spider.py
import scrapy
from scrapy.crawler import CrawlerProcess


class QuotesSpider(scrapy.Spider):
    name = "quotes"

    def start_requests(self):
        urls = [
            'http://quotes.toscrape.com/page/1/',
            'http://quotes.toscrape.com/page/2/',
        ]
        for url in urls:
            yield scrapy.Request(url=url, callback=self.parse)

    def parse(self, response):
        page = response.url.split("/")[-2]
        filename = f'quotes-{page}.html'
        with open(filename, 'wb') as f:
            f.write(response.body)
        self.log(f'Saved file {filename}')

if __name__ == '__main__':
	process = CrawlerProcess(setting)
    crawler = process.create_crawler(QuotesSpider)
    process.crawl(crawler)
    process.start()

코드를 보면 대충 감이 오시나요? main 부분은 신경 쓰지 않으셔도 됩니다. 크롤러를 초기화하는 과정으로 사실 이 부분 없이도 터미널에서 아래처럼 QuotesSpider를 직접 실행시키는 것도 가능하니까요.

$ scrapy crawl quotes

코드를 잠시 살펴보면, 주어진 URL 두 개에 대해서 request(get)를 보내고 그 결과를 callback으로 처리하는 로직입니다. parse 이외에도 콜백은 계속 연결할 수 있습니다. 예를 들어 "로그인 → 페이지 이동 → 데이터 조회 → 데이터 다운로드" 이런 파이프라인을 생각해 볼 수 있겠네요. 위 예제는 quotes.toscrape.com에서 1, 2 페이지를 방문하고 있는데 이처럼 여러 가지 액션이 동시에 이루어져야 하는 경우 막강하게 활용할 수 있겠습니다. 한편, callback처럼 에러 핸들링을 위한 errback 도 지원합니다. callback처럼 errback도 붙여서 사용하시면 됩니다. 아래는 공식 예제의 일부입니다.

import scrapy

from scrapy.spidermiddlewares.httperror import HttpError
from twisted.internet.error import DNSLookupError
from twisted.internet.error import TimeoutError, TCPTimedOutError

class ErrbackSpider(scrapy.Spider):
    name = "errback_example"
    start_urls = [
        "http://www.httpbin.org/",              # HTTP 200 expected
        "http://www.httpbin.org/status/404",    # Not found error
        "http://www.httpbin.org/status/500",    # server issue
        "http://www.httpbin.org:12345/",        # non-responding host, timeout expected
        "http://www.httphttpbinbin.org/",       # DNS error expected
    ]

    def start_requests(self):
        for u in self.start_urls:
            yield scrapy.Request(u, callback=self.parse_httpbin,
                                    errback=self.errback_httpbin,
                                    dont_filter=True)

    def parse_httpbin(self, response):
        self.logger.info('Got successful response from {}'.format(response.url))
        # do something useful here...

    def errback_httpbin(self, failure):
        # log all failures
        self.logger.error(repr(failure))

        # in case you want to do something special for some errors,
        # you may need the failure's type:

        if failure.check(HttpError):
            # these exceptions come from HttpError spider middleware
            # you can get the non-200 response
            response = failure.value.response
            self.logger.error('HttpError on %s', response.url)

        elif failure.check(DNSLookupError):
            # this is the original request
            request = failure.request
            self.logger.error('DNSLookupError on %s', request.url)

        elif failure.check(TimeoutError, TCPTimedOutError):
            request = failure.request
            self.logger.error('TimeoutError on %s', request.url)

호출에 성공하면 callback으로 붙어있는 parse_httpbin이 실행되고, 어떤 이유로든 실패하게 되면 errback으로 선언되어 있는  errback_httpbin이 호출되는 구조입니다. 

예제 코드까지 살펴봤는데 사실 여기까지만 보면 curl이나 python의 requests 등의 라이브러리를 사용하는 것과 차이를 느끼지 못할 수도 있습니다. 하지만 조금 더 심화로 들어가게 되면 scrapy에서 다양한 configuration이 있습니다. 다른 라이브러리로 개발한다면 밑바닥부터 신경 써야 하는 모든 것을 하나의 프레임워크로 제공받게 된다는 걸 알게 됩니다. 이를테면 데이터 다운로드 타임아웃을 설정한다던가, 각 request 간에 random 한 텀(사람의 실제 액션처럼 보이기 위한)을 둔다던지 말이죠. 상세한 설정은 아래 링크에서 확인 가능합니다.

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

 

# 결론

위에서 확장성이 좋다는 이야기도 했는데 맞습니다. 미들웨어를 새로 개발한다거나 파이프라인을 연결하는 게 아주 쉽기 때문이에요. 예를 들면 proxy 처리를 위한 미들웨어, 데이터 파이프라인처럼요. 하지만 scrapy에는 큰 단점이 있습니다. javascript 지원이 힘들다는 건데요, ajax/pjax로 데이터가 갱신되는 웹페이지라면 원하는 데이터를 추출하는 게 쉽지 않습니다. scrapy+selenium으로 극복도 가능하지만 그럴 거면 그냥 셀레니움 쓰는 게 낫지 않냐며... 아무튼, 한 개 사이트 안에서 여러 페이지를 돌아다니며 핸들링해야 하는 데이터가 많을 때. 그리고 그 페이지가 javascript의 영향이 적을 때 scrapy는 selenium보다 훨씬 더 좋은 선택지가 될 수 있겠습니다. 데이터 수집의 여정에 scrapy가 도움이 될 수 있기를 바랍니다!

댓글
댓글쓰기 폼