본문 바로가기

프로그래밍/python

스크래피 scrapy 이거 하나로 익숙해지기..2

반응형

이전 포스트를 안보신분은 아래 링크를 참고하세요


데이터 추출하기

스크래피를 이용해서 데이터를 추출해보는 연습중에 가장 좋은 방법은 Scrapy shell을 이용하는 것입니다.

아래와같이 터미널에 입력하여 실행 시킵니다.

scrapy shell "http://quotes.toscrape.com/page/1/"
커맨드라인에서 Scarpy shell을 이용할 때 무조건 작은따옴표(' ') 를 사용해서 만에하나 특수문자가 들어가 있는 url을 실행 시켰을 때 안되는 것을 방지 합니다. 그렇지만 만약 Window 환경일 경우에는 큰따옴표(" ")를 쓰도록 합니다.

 

크롤링이 된 후 바로 파이썬 인터프리터로 넘어가는걸 볼 수 있습니다.

 

이제 shell 안에서 CSS를 이용하여 element를 선택할 수있습니다.

>>> response.css('title')
[<Selector xpath='descendant-or-self::title' data='<title>Quotes to Scrape</title>'>]

response.css('title') 이 커맨드를 사용함으로써 얻느 결과값은 XML/HTML element를 감싸는 Selector 오브젝트의 리스트를 담고있는 SelectorList 형태입니다.  위의 결과값인 title 의 text를 추출할려면 다음과같이 하면 됩니다.

 

>>> response.css('title::text').getall()
['Quotes to Scrape']

두가지 특별한점이 있는데, 먼저 ::text 를 CSS query에 추가 된것입니다. 이것은 <title> element 안의 text요소만을 가져오기 위한 것이며, ::text를 추가하지 않을경우 tags를 포함한 전체 요소를 리턴하게 됩니다.

 

>>> response.css('title').getall()
['<title>Quotes to Scrape</title>']

나머지는 .getall() 을 이용하는 것입니다. 이것은 한개 이상의 selector를 리스트로 반환이 가능하며, 만약 첫번째 결과값만 추출하고 싶을 때는 아래와 같이 하면됩니다.

>>> response.css('title::text').get()
'Quotes to Scrape'

 

또다른 방법으로는 아래와 같습니다.

>>> response.css('title::text')[0].get()
'Quotes to Scrape'

SelectorList에서 .get()을 사용할 때 주목할점은 결과를 찾을 수 없을때 IndexError 가 아닌 None 을 리턴하는 것입니다.

 

 

.get()과 .getall() 말고 또다른 방법은 정규식을 쓰는 것입니다.

>>> response.css('title::text').re(r'Quotes.*')
['Quotes to Scrape']
>>> response.css('title::text').re(r'Q\w+')
['Quotes']
>>> response.css('title::text').re(r'(\w+) to (\w+)')
['Quotes', 'Scrape']

XPath 사용하기 

CSS 이외로 Scrapy는 XPath expression을 지원합니다.

>>> response.xpath('//title')
[<Selector xpath='//title' data='<title>Quotes to Scrape</title>'>]
>>> response.xpath('//title/text()').get()
'Quotes to Scrape'

XPath 익스프레션은 매우 강력하며, Scrapy Selectors의 근본입니다. 사실 CSS selectors의 메카니즘의 안쪽은 결국 XPath로 변환이 됩니다.

 

CSS만큼 유명하지는 않을지라도 XPath는 구조를 navigating 함에 있어서 그 속에 담긴 컨텐츠 또한 찾아낼 수 있으므로 가장 파워를 가집니다.  예를들면 어떠한 "Next Page" 라는 단어가 담긴 링크를 선택할 수 있다는 것입ㄴ다. 이것은 스크래핑 함에 있어서 딱 맞는 기능을 가지고 있는 것입니다. 따라서 CSS selectors에 익숙할지라도 XPath를 공부하는 것을 추천합니다. 이것이 스크랩하는데 더 쉽게 해줄 것이니깐요.

 


인용글과 작가 추출하기

이제 어느정도는 검색(selection) 과 추출(extraction)이 가능하므로, 인용글을 추출하는 코드를 스파이더에 넣어서 완성 해보도록 하겠습니다.

 

각각의 https://quotes.toscrape.com 인용글은 아래와같은 HTML elements 형식으로 표현됩니다.

<div class="quote">
    <span class="text">“The world as we have created it is a process of our
    thinking. It cannot be changed without changing our thinking.”</span>
    <span>
        by <small class="author">Albert Einstein</small>
        <a href="/author/Albert-Einstein">(about)</a>
    </span>
    <div class="tags">
        Tags:
        <a class="tag" href="/tag/change/page/1/">change</a>
        <a class="tag" href="/tag/deep-thoughts/page/1/">deep-thoughts</a>
        <a class="tag" href="/tag/thinking/page/1/">thinking</a>
        <a class="tag" href="/tag/world/page/1/">world</a>
    </div>
</div>

 

이제 다시 terminal 을 꺼내 우리가 원하는 데이터를 어떻게 꺼내는지 알아보겠습니다.

$ scrapy shell 'http://quotes.toscrape.com'

 

인용문을 가지고 있는 HTML 요소의 리스트를 가져옵니다.

>>> response.css("div.quote")
[<Selector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' quote ')]" data='<div class="quote" itemscope itemtype...'>,
 <Selector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' quote ')]" data='<div class="quote" itemscope itemtype...'>,
 ...]

쿼리에 의해 리턴된 selector들은 그 안의 element를 또 쿼리가 가능하도록 만들어줍니다. 이제 특정한 인용문을 가져오기 위해 첫번째 selector만 가져와봅니다.

>>> quote = response.css("div.quote")[0]

 

이제 인용문 quote 오브젝트에서 text, author 그리고 tags를 추출해봅니다. 

>>> text = quote.css("span.text::text").get()
>>> text
'“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”'
>>> author = quote.css("small.author::text").get()
>>> author
'Albert Einstein'

 

.getall() 메쏘드를 통해 스트링 태그들을 가져올 수 있습니다.

>>> tags = quote.css("div.tags a.tag::text").getall()
>>> tags
['change', 'deep-thoughts', 'thinking', 'world']

 

이제는 반복을 통해 찾았던 quote들의 요소들을 추출하고 파이썬 dictionary형식으로 저장을 시킬 수 있습니다.

>>> for quote in response.css("div.quote"):
...     text = quote.css("span.text::text").get()
...     author = quote.css("small.author::text").get()
...     tags = quote.css("div.tags a.tag::text").getall()
...     print(dict(text=text, author=author, tags=tags))
{'text': '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”', 'author': 'Albert Einstein', 'tags': ['change', 'deep-thoughts', 'thinking', 'world']}
{'text': '“It is our choices, Harry, that show what we truly are, far more than our abilities.”', 'author': 'J.K. Rowling', 'tags': ['abilities', 'choices']}
...

스파이더에서 데이터 추출하기

이제 다시 스파이더로 돌아와서, 지금까지는 특정 데이터를 추출하는 대신 HTML 페이지들을 저장하였는데 이제는 데이터를 추출하는 기능을 스파이더에 넣어보도록 하겠습니다.

전형적으로 스크래피 스파이더는 dictionary 형식으로 페이지에서 데이터를 추출하는데, 그렇게 하기 위해서는 yield 파이썬 키워드를 콜백 함수에 써야합니다.

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
        'http://quotes.toscrape.com/page/2/',
    ]

    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(),
            }

 

이제 스파이더를 실행하면 추출 데이터를 로그를 통해 아래와 같이 출력해줍니다.

2016-09-19 18:57:19 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/1/>
{'tags': ['life', 'love'], 'author': 'André Gide', 'text': '“It is better to be hated for what you are than to be loved for what you are not.”'}
2016-09-19 18:57:19 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/1/>
{'tags': ['edison', 'failure', 'inspirational', 'paraphrased'], 'author': 'Thomas A. Edison', 'text': "“I have not failed. I've just found 10,000 ways that won't work.”"}

 


스크랩 데이터를 저장하기

스크랩 데이터를 저장하는 간단한 방법은 아래의 커맨드를 이용하는 방법입니다.

scrapy crawl quotes -O quotes.json

이것은 JSON 파일로 serialized 스크랩데이터들을 저장합니다.

여기서 -O 는 이미 존재하는 파일에 덮어쓰기를 하는 것이며, -o는 이미 존재하는 파일에 내용을 추가하는 것입니다. 하지만 JSON 형식에 파일을 추가하면 JSON의 기본 형식이 깨지기 때문에 JSON Lines을 이용하면 됩니다.

scrapy crawl quotes -o quotes.jl

 

JSON Line 포맷은 스트림 형식에 유용한데, 두번 반복 실행을 해도 문제가 없습니다. 또한 각각의 record는 라인으로 분리되어 있기 때문에 메모리에 상관없이 큰 파일들을 참조가 가능합니다. JQ 라는 툴을 커맨드에 이용하면 됩니다.

이렇게 간단하고 작은형태일 경우 상관없지만 크고 복잡한 상황일 경우 Item Pipeline을 이용합니다. 기본적인 프로젝트를 생성시에 tutorial/pipelines.py 파일이 생성되어 있습니다.

 


링크 따르기

https://quotes.toscrape.com 의 첫번째와 두번째 페이지의 스크랩을 하는 것 대시 만약 그 웹사이트안에 있는 모든 quote를 추출하고 싶다고 가정합니다. 그러면 만약 페이지에서 데이터를 추출하는 패턴을 안 상태에서 어떻게 링크를 따라 추출하는지 알아봅시다.

 

첫번째는 따라갈 페이지 링크를 추출해야합니다. 현재의 페이지에서 next page 라는 링크를 찾을 수 있습니다.

<ul class="pager">
    <li class="next">
        <a href="/page/2/">Next <span aria-hidden="true">&rarr;</span></a>
    </li>
</ul>

 

셸에서 실행해 봅시다.

>>> response.css('li.next a').get()
'<a href="/page/2/">Next <span aria-hidden="true">→</span></a>'

anchor 요소가 있지만 우리는 href 어트리뷰트 값을 알고 싶어 합니다. 그렇게 하기 위해서 CSS를 이용하는 방법은 아래와 같습니다.

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

또한 비슷한 대안으로 attrib 라는 프로퍼티가 존재합니다.

>>> response.css('li.next a').attrib['href']
'/page/2/'

 

이제 재귀적으로 링크를 따라서 데이터를 추출하는 스파이더를 봅시다.

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    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:
            next_page = response.urljoin(next_page)
            yield scrapy.Request(next_page, callback=self.parse)

이제 데이터를 추출한 후 parse() 메쏘느는 next page 링크를 확인하고 urljoin() 메쏘드를 이용해 absolute URL을 만들고 새로운 request 를 그다음 페이지로 요청하게 됩니다. 그렇게 재귀적으로 크롤링을 하게 되는 것입니다.

 

여기서 확인할 수 있는 스크래피의 기본 메커니즘은 링크를 따라가는 것입니다. 스크래피는 request 를 요청하고 콜백함수를 실행하는 스케쥴링을 끝날때까지 계속하게 됩니다. 

 

이러한 점을 이용해 링크를 따라가는 복잡한 크롤러를 만들 수가 있습니다.

 

 

반응형