정적 웹페이지를 크롤링하는 것은 scrapy만 사용할 것이다. py27 환경에서 pip install jupyter로 쥬피터 노트북을 설치해놓자.
데이터를 수집할 사이트는 https://www.rottentomatoes.com/top/bestofrt/?year=2015 이다.
영화리스트에서 하나를 선택하게 되면, 영화 상세페이지가 나오게 된다. 그 상세 페이지에서 제목, 평점 등의 정보를 가져오도록 해보자.

정적 웹페이지 Scray로 크롤링 하기

  • Prompt에서  [scrapy 프로젝트]를  생성해줘야한다.
    scrapy startproject rt_crawler
    image
    rt_crawler 라는 폴더가 생성되, 다시 똑같은 제목의 폴더가 있고, scrapy구조가 완성 된 것을 알 수 있다.
    확인은 jupyter해서 하면된다.
    image
    우리는 이 *.py파일들을 수정한 뒤, spider를 실행하여, 최종적으로 스크래핑 및 크롤링을 할 것이다.
  • rt_crawler/
        scrapy.cfg            
        rt_crawler/           # 해당 프로젝트의 Python 모듈
            __init__.py
            items.py          # 프로젝트 Item 파일
            pipelines.py      # 프로젝트 Item pipeline 파일
            settings.py       # 프로젝트 Settings 파일
            spiders/          # Spider 저장 폴더
                __init__.py
                ...
  • items.py파일을 수정하여 [상세페이지]에서 수집할 정보를 담을 [새로운 item클래스]를 정의해준다. 기존에 자동으로 생성되어있는 클래스는 삭제하자.
    import scrapy
    #스크래파이의 Item클래스를 상속받는 클래스 생성
    class RTItem(scrapy.Item):
        #클래스의 객체에다가 정보를 저장할 필드(멤버변수)를 생성해주는데, title은 Field()형태로 저장할 것이다.
        title = scrapy.Field()
        score = scrapy.Field()
        genres = scrapy.Field()
        consensus = scrapy.Field()

  • spiders폴더에 들어가서, text파일을 하나 생성한 뒤 이름을 [rt_spider.py]로 바꾸고, [ spider] 클래스를 새롭게 정의하여, 각종 규칙을 명시하자.
    #기본적으로 scrapy 임폴트
    import scrapy
  • #우리프로젝트의 items.py에 만들어준 [RTItem 클래스] 도 import
    from rt_crawler.items import RTItem

    #scrapy에서 제공하는 Spider클래스를 상속한다.
    class RTSpider(scrapy.Spider):
       
        # 해당 Spider의 이름. 웹 크롤링 및 스크래핑을 실행할 때 여기서 지정한 이름을 사용함.
        name = "RottenTomatoes"
       
        #Spider로 하여금 크롤링하도록 허가한 웹 사이트의 도메인 네임.
        allowed_domains= ["rottentomatoes.com"]
       
        #웹 크롤링의 시작점이 되는 웹 페이지 URL. 해당 웹 페이지에서 출발하여 이어지는 웹 페이지들을 크롤링함
        start_urls=[
            "https://www.rottentomatoes.com/top/bestofrt/?year=2015"
        ]
       
        #start_urls의 웹페이지를, spider가 서버에 요청하게 되고, 이에 대한 응답을 reponse라는 변수에 받는데, 이 reponse를 받아 처리하는 parse()함수를 정의
        def parse(self, response):
            #response로 얻어진 웹사이트의 어떤부분을 스크래핑할지 명시해줘야한다

  • rt_spider.py의 RTSpider클래스에서 parse()함수에 <상세페이지로 가기위한 하이퍼링크url >을 얻기위하여,
    어느 요소를 처리할지를 알기 위한 도구인 [ scrapy shell ][ 크롬 개발자도구 ]를 사용해서 찾아내야한다.
    (1) 쥬피터를 띄우고있는 것 외에 새로운 prompt를 켜서, (py27)환경에서  scrapy shell을 실행시키자.
    activate py27
    scrapy shell “https://www.rottentomatoes.com/top/bestofrt/?year=2015
    (2) 다른 한켠에는, 해당 사이트에서 크롬 개발자도구를 켜서, 하이퍼링크를 담은 제목요소를 가리키는 xpath를 얻는다.
    (3) 복사된 xpath를 scrapy shell에서 response.xpath(‘//*[@id="top_movies_main"]/div/table/tbody/tr[1]/td[3]/a’)를 입력해도 아무것도 안나온다.

<예외처리1>

  • ***크롬 개발자도구의 xpath 추출기능이 만능이 아니라는 것이다.
    *** 이 때, seletor가 얻어지는 xpath를 찾기 위해서, 뒤에서부터 한단계씩 지워서 거슬러 올라가면 된다.
    (4) xpath의 마지막 뒤부분을 table까지 남기니까 selector가 리턴되었다!!!
    response.xpath('//*[@id="top_movies_main"]/div/table')
    image
    (5) 이제 해당xpath에서 필요없는 경로를 삭제하면서 다시 최종적으로 타고 들어가야된다. 개발자 도구상 마우스로 살펴보면, table클래스 밑에 있는 <thead>와 <tbody>는 개별 item들과는 상관이 없는 요소였다. 그래서 xpath상의 tbody를 지운체, 다시 자식을 그대로 타고 가보았더니 최종적으로 원하는 데이터가 나왔다.
    (a= 하이퍼링크요소, 까지 다시 타고 들어가야한다!)
    response.xpath('//*[@id="top_movies_main"]/div/table/tr[1]/td[3]/a')
    image
    (6)이제, 크롬개발자 도구상에서 보면, 하이퍼링크요소 <a ~ > </a> href라는 속성값하이퍼링크 url이다.
    저 주소를 가져오기 위해서 xpath 마지막에[  /@href  ]를 붙혀 주면, a요소의 자식속성 href를 Selector에 가져오게 된다.
    바로 뽑아내기 위해서, href가 만약 리스트라면 [0]첫번째를 가져오고, extract()로 selector가 아닌 실제값을 추출하게 한다.
    response.xpath('//*[@id="top_movies_main"]/div/table/tr[1]/td[3]/a/@href')[0].extract()
    image
    이 때, 맨마지막에 .encode(‘utf-8’)로 인코딩해주면, 앞에 달린 u가 사라진다.
    response.xpath('//*[@id="top_movies_main"]/div/table/tr[1]/td[3]/a/@href')[0].extract().encode('utf-8')
    print에 인코딩 없이 출력해도, 마찬가지로 u가 사라진다.(print에는 인코딩 기능까지)
    실제 데이터를 추출해보면, u는 사라지므로 일단 남겨두자
  • 이제 한 item의 상세 페이지의 서브url이 아니라, 전체 도메인까지 들어간 Full url이 필요하다.
    서브url을 url변수에 저장하고, response.urljoin()함수를 이용해서, 자동으로 전체도메인에 서브도메인을 붙히자.
    url = response.xpath('//*[@id="top_movies_main"]/div/table/tr[1]/td[3]/a/@href')[0].extract()
    response.urljoin(url)
    image
  • 이제 하나의 item에 대한 a요소로 찾은 상세페이지url이 아니라 전체 item들에 대한 url들이 모두 필요하다.
    html구조를 거슬러 올라가서 찾아보자.
    a요소 > td > tr까지 올라가보니까,  tr이 반복되면서 , 리스트의 하나의item들이 각각 반복되는 것을 확인할 수 있다.
    image
    즉, <tr> : 리스트의 각 하나의 item,   3번째<td> :  <a>요소에 제목과, 하이퍼링크속 Suburl을 포함한다.
  •  모든 tr요소들을 얻도록 xpath를 고쳐야한다.
    즉, item을 의미하는 <tr>요소의 인덱싱을 제거한 뒤, response.xpath(‘’)에다가 넣어서 셀렉터리스트로 반환받는다.
    -> for문인자에 넣어서 각 item을 의미하는 tr의 셀렉터하나씩 꺼내도록한다.
    –> 각각의 성분마다 .xpath(‘./’)을 이용해서 /@href 속성을 얻는 xpath경로를 셀렉터에 추가한 뒤 href변수(한 item의 href까지의 셀럭터)에 받는다.
    ->  href변수에 담긴 href속성 중 첫번째(서브url)을 [0].extract()한 것을  response.urljoin()을 통해  전체도메인에 붙혀서 url변수에 저장한다.
    -> 확인을 위해 print(url)해준다.

  • In [33]: for tr in response.xpath('//*[@id="top_movies_main"]/div/table/tr'):
         ...:     href = tr.xpath('./td[3]/a/@href')
         ...:     url = response.urljoin(href[0].extract())
         ...:     print(url)
    image
    리스트의 100개의 item에 대해, 하이퍼링크 url을 얻었다.
    얻은 100개의 url을 가지고, 각 웹페이지를 요청하기 위해서, scrapy에서 제공하는 request함수를 사용할 것이다.

  • 이제 100개의 리스트를 얻는 for문 부분을 복사해서 [rt_spider.py]의 response를 처리할 수 있는 parse()함수로 가져온다.
    마지막의 print만 삭제 한 뒤,  yield라는 키워드를 사용하여 scrapy의 함수 scrapy.Request함수를 사용하게 된다.
    scrapy.Request(,)의 인자로는, 각 item의 하이퍼링크 full url을 넣어주고,  또다른인자 callback=에다가는,
    url을 통해 불러온 페이지의 html소스코드를 어떻게 스크래핑할지 정해줄, 사용자 정의 함수를 넣어준다.
    def parse(self, response):
         #response로 얻어진 웹사이트의 어떤부분을 스크래핑할지 명시해줘야한다.
        
         for tr in response.xpath('//*[@id="top_movies_main"]/div/table/tr'):
             href = tr.xpath('./td[3]/a/@href')
             url = response.urljoin(href[0].extract())
             yield scrapy.Request(url, callback = self.parse_page_contents)

  • 이제 scrapy.Request()함수의 callback인자에 넣어준 parse_page_contents()함수를 정의해보자.
    리스트의 각 url을 통해 불러온 페이지의 html소스코드를 어떻게 스크래핑할지 정해줄 것이다.
    이 때, 인자로 들어가는 response는 scrapy.Request()의 각 상세페이지 하이퍼링크url을 통해 불러온 응답(html소스코드)을 의미한다.
    parse()가 start_urls에서 얻은 웹사이트의 response를, xpath를 통해 어떻게 스크래핑할지 정의하였는데,
    parse()속의 parse_page_contents()는 parse()함수의 마지막 Request(url, callback=)의 url에서 얻은 response를 어떻게스크래핑할지 정의하는 함수다. 그러므로, 파싱할 상세페이지를 shell과 크롬개발자도구로 먼저 살펴본 뒤, parse함수를 정의해준다.

    (1) 현재 실행된 scrapy shell을 [ctrl+d]를 눌러서 종료해준다.
    (2) 하나의 item 상세페이지url을 복사한 다음, scrapy shell을 다시 작동시키자.
    scrapy shell https://www.rottentomatoes.com/m/mad_max_fury_road
    (3) 이 상세페이지에서는 영화제목/평점/장르/총평의 정보를 가져올 것이므로, 크롬개발자도구를 이용해서 각각의 xpath를 알아야한다

<하이퍼링크를 타고들어가서, 상세페이지의 item의 xpath를 통해, 필요한 요소들 뽑아오는 과정>

  • (1)상세 페이지의 제목을 클릭xpath를 카피해, response.xpath()를 통해, selector를 얻어냈다면,
    (2) /text()를 xpath에 더해서  제목을 가지는 셀렉터인지 확인하고,
    (3) [0].extract()를 통해 텍스트만 뽑아내보자.
    response.xpath('//*[@id="heroImageContainer"]/a/h1/text()')[0].extract()
    image
    (4) 이제 추출한 텍스트 앞에 있는 <\n과 공백>을 제거하기 위해서, 문자열에 대해서, .strip()함수를 적용시키면된다.
    response.xpath('//*[@id="heroImageContainer"]/a/h1/text()')[0].extract().strip()
    image
  • 비슷한 방법으로 영화 평점도 뽑아오자.

<예외처리2>

  • 1개의 평점을 클릭한 뒤, xpath를 통해, 셀렉터를 뽑았는데 2개의 셀렉터가 나왔다.(아마도 크롬의 xpath뽑는 기준이 완벽치않음)
    이 때는, 우리가 원하는 text()만 뽑은 다음, 결과물에 인덱싱[0]을 통해서,  첫번째것만 가져오는 식으로 한다. 그리고 extract()한다.
    (기존에 항상 [0].extract()한 것과 동일함)
    response.xpath('//*[@id="tomato_meter_link"]/span[2]/span/text()')[0].extract()
    image
    이렇게 scrapy shell과 크롬개발자도구로 [ 상세페이지의 원하는 정보를 스크래핑하는 코드 ]를 다 찾아낼 수 있다.
    --> 여기까지 찾아냈으면, 앞서 정의했던 parse_page_contents()함수에다가 넣어줘야한다.
  • 이제 상세페이지의 각 필요한 정보들을 스크래핑하는 코드를
    for문을 돌면서 각 웹페이지를 불러오는 곳(Request)의 callback인자를 통해 호출되는 parse_page_contents()함수에 넣는데,
    처음 [items.py]에 정의한 양식대로, 객체를 만들어서, 거기다 담아야한다.(처음에 items.py의 RTItem클래스를 import했었다)

<예외처리 3> 장르 같은 경우, 리스트로 나온다.

  • 장르 같은 경우, xpath로 셀렉터를 뽑아보면, < 공백가져서 각각 strip()필요한 2개 리스트 > 가 나와, 리스트형식으로 가져와야한다.
    (1) xpath를 통해 셀렉터 뽑고, /text()를 통해 확인한다.
    (2) 줄바꿈과 공백을 포함하더라도, 리스트로 나오기 때문에, [0].extract.strip()을 하면 안된다. (리스트는 .strip()도 안된다)
    (3) extract()를 먼저하여 셀렉터 리스트 –> data리스트만 따로 변수에 담는다.
    genres_list = response.xpath('//*[@id="mainColumn"]/section[3]/div/div[2]/ul/li[2]/div[2]/a/text()').extract()
    (4) 결국 parse_page_contents()함수에서 객체 속 하나의 필드에 저장해야하므로, 리스트를   ‘’구분자.join( 리스트)함수로 리스트를 합치면서,
        각각에 strip()이 적용시킨다.
    객체의 필드 = ''.join(genres_list).strip()
    spider클래스의 parse()함수에서 나온 리스트형data는, join없이 그냥 리스트를 list변수에 담아두고, 변수에 map()으로 줄바꿈+공백만 삭제
    ->  뒤 pipelines에서 join해줄 것이다.

    image

  • 총평(census)는 일단 생략하자. 그리고 마지막은 yield를 통해 채워진 객체 item을 반환해주자.

  • def parse_page_contents(self, response):
            item = RTItem() #임폴트한 RTItem의 객체 생성
            #RTItem클래스에 정의한, 객체에서 쓸, 필드(멤버변수)들은 열 인덱싱처럼 뽑아낸다.
            item["title"] = response.xpath('//*[@id="heroImageContainer"]/a/h1/text()')[0].extract().strip()
            item["score"] = response.xpath('//*[@id="tomato_meter_link"]/span[2]/span/text()')[0].extract()

                genres_list = response.xpath('//*[@id="mainColumn"]/section[3]/div/div[2]/ul/li[2]/div[2]/a/text()').extract()
                item["genres"] = genres_list = map(unicode.strip, genres_list)

            yield item


    여기까지가 [items.py] 와 [rt_spider.py] 정의가 끝났다.
    scrapy shell을 종료시키자.

  • 이제 우리가 제작한 spider실행시켜야한다.
    (1) (py27)환경에서, 우리가 만든 프로젝트이름인 rt_crawler 폴더로 먼저 간다.
    cd rt_crawler
    (2) (py27) C:\Users\cho\Web_da\rt_crawler> 상태에서 크롤러를 실행시키는 명령어에 정의한 크롤러이름을 넣어 실행시킨다.
    ( 크롤러이름은,, 프로젝트명폴더/프로젝트명폴더/spiders에 만들어준 [rt_spider.py]name = “RottenTomatoes” 를 넣어준다)
       scrapy crawl RottenTomatoes
    (3) 만약 csv파일로 저장하고 싶다면 scrapy crawl RottenTomatoes -o rt.csv
    각종 indexError가 나서, 100개 다 스크래핑 못해오더라.
    image
    결과물을
    프로젝트 폴더에서 확인해보면, RTItem에는 있는 필드지만, 스크래핑을 생략한 필드에 대해서는 , , 으로 비어서 나온다.
    image

<예외처리 4>
한글로 주석달아서, 파이썬이 한글을 못읽는 문제 :

SyntaxError: Non-ASCII Character 관련된 에러라고 부릅니다. 코드 내에 한글을 파이썬이 읽어들이지 못해서 발생되는 에러
해당 *.py의 첫번째 줄or 두번째 줄에 : # -*- coding: utf-8 -*-
주석을 추가해준다.
image
<예외처리 5>
나같은 경우, 장르 리스트의 2번째 성분 앞에 있는 < \n    공백  >이 안사라졌다.
구글링 결과, list에 \n이라는 유니코드를 먼저 없어줘야한다.
genres_list = map(unicode.strip, genres_list) 를 통해서, \n을 먼저 사라지게 한 뒤,
item["genres"] = ', '.join(genres_list).strip()  를 통해, 구분자로서 콤마(,)를 넣고, 공백도 제거하자.
--> join은,, 나중에 pipelinse에서 할 것이다.


  • 스크래핑한 csv결과물을 보니,  스크래핑 순서가 역순으로 되어있고, 리스트인 genres가 2개 이상인 경우 “”쌍따옴표를 가져
    식별하기 어려운 형태로 저장되었다.
    image


웹스크래핑을 통해 얻어진 데이터를, 어떻게 가공하고, 외부파일로 저장할지 구체적으로 명시하기 위한 item pipelines

  • 프로젝트 폴더> 프로젝트명 폴더 > [pipelines.py]를 열자.
    (1)기존 클래스를 제거하고, csv모듈을 import한다.
    import csv
    (2) [RTPipeline]클래스를 정의한다.(object를 상속한다)
    (3) 생성자함수( 자바에서는 클래스의 객체생성시, 필드들 초기화)를 정의해주는데, 생성자안에
        import한 csv모듈을 이용하여, 매 행을 csv파일을 쓰도록 정의해줄 것이다.

  • #csv모듈 임폴트
    import csv

    #object를 상속하는 RT크롤러의 pipelines클래스 정의
    class RTPipelines(object):
        
         def __init__(self):
             #생성자에는 self를 이용해 변수를 만들고, csv모듈의 writer함수를 이용해서, csv파일을 열고 쓸 것이다.(없다면 생성)
             self.csvwriter = csv.writer(open("rt_movies_new.csv", "w"))
            
             #이 순서대로 csv파일의 열들을 채워넣는겠다는 의미로서, 열을 직접 입력해준다.
             self.csvwriter.writerrow( ["title", "score", "genres"] )

    image

  • 이제 새로운 함수 process_item()함수를 정의할 것인데, 인자로서는 self외에 item(객체)spider가 들어간다.
    spider를 이용해 수집한 item들을 어떻게 처리할지를 명시하게 된다.
    파이썬 리스트를 하나 생성한 뒤에, 각 item객체의 필드들 append할 것이다.
    genres는 리스트이므로 |문자를 구분자로 하여 .join으로 각각을 연결해준다.
    다음에는 self.csvwriter.writerow를 통해 순서대로 쌓아둔 리스트(row)를 각 행에 밀어넣는다.
    이제 객체를 사용한 item은 returnd을 해줘야한다.

  • def process_item(self, item, spider):
             row = []
             row.append(item["title"])
             row.append(item["score"])
             row.append('|'.join(item["genres"]))
             self.csvwriter.writerow( row )
            return item

    image

  • 이제 정의한 item pipelines크롤링시 사용하도록, settings에서 설정을 해줘야한다.
    (1) [settings.py]를 열고,  # ITEM_PIPELINES 부분을 찾아 주석을 해제한 뒤,
        pipelines.py에 우리가 정의한 클래스(RTPipelines)를 명시해준 뒤 저장한다.

    image

  • setting에서 크롤링시 자동으로 csv파일을 생성하여 쓰기 때문에,
    scrapy crawl RottenTomatoes 뒤에 –o rt.csv를 적을 필요가 없다.
    image
    ***나는 미리, 리스트를 join해버려서,,, 다시 join시키는 꼴이므로,, 각 글자마자.. |가 찍힌다.
    *** pipelines에서 join함수를 사용할거면, 
         rt_spider.py에서, 각 필드에 담을 때는,, join없이, 그냥 리스트를 유지하고, \n등 유니코드만 제거해준다.
    image

    image
    my)  spider에서 객체에 xpath를 통해 추출해서 담을 때는, 리스트는 유지하고 join쓰지말고, map()으로 유니코드만 제거 + yield로 item리턴
           pipelines에서 객체의 필드를 꺼내 쓸때는, 리스트의 경우 구분자와 join을 써서 각 항목 보기좋게 구분 + return으로 item리턴

최종 결과물

image

  1. 김학건 2018.04.16 09:15

    genres 가져올 때, 공백 어떻게 제거하셨나요?
    for문으러 strip 하고 리스트에 넣었는데 아예 뜨지를 않네요

    • 김학건 2018.04.16 09:17

      아 map(unicode.strip, genres_list)로 해주셨었네요. ㅎ

  2. irongaea 2018.07.31 21:03

    좋은 정보 잘 보고 갑니다. 2018년 7월 1위는 black panther군요.
    python3.7 에서
    item['genres'] = list(map(str.strip, genres_list) 입니다. 한참 찾았습니다.

+ Recent posts