4. 정적 웹페이지 웹크롤링 -> 웹 스크래핑 하기(scrapy)
정적 웹페이지를 크롤링하는 것은 scrapy만 사용할 것이다. py27 환경에서 pip install jupyter로 쥬피터 노트북을 설치해놓자.
데이터를 수집할 사이트는 https://www.rottentomatoes.com/top/bestofrt/?year=2015 이다.
영화리스트에서 하나를 선택하게 되면, 영화 상세페이지가 나오게 된다. 그 상세 페이지에서 제목, 평점 등의 정보를 가져오도록 해보자.
정적 웹페이지 Scray로 크롤링 하기
- Prompt에서 [scrapy 프로젝트]를 생성해줘야한다.
scrapy startproject rt_crawler
rt_crawler 라는 폴더가 생성되, 다시 똑같은 제목의 폴더가 있고, scrapy구조가 완성 된 것을 알 수 있다.
확인은 jupyter해서 하면된다.
우리는 이 *.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
...
import scrapy
#스크래파이의 Item클래스를 상속받는 클래스 생성
class RTItem(scrapy.Item):
#클래스의 객체에다가 정보를 저장할 필드(멤버변수)를 생성해주는데, title은 Field()형태로 저장할 것이다.
title = scrapy.Field()
score = scrapy.Field()
genres = scrapy.Field()
consensus = scrapy.Field()
#기본적으로 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로 얻어진 웹사이트의 어떤부분을 스크래핑할지 명시해줘야한다
어느 요소를 처리할지를 알기 위한 도구인 [ 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')
(5) 이제 해당xpath에서 필요없는 경로를 삭제하면서 다시 최종적으로 타고 들어가야된다. 개발자 도구상 마우스로 살펴보면, table클래스 밑에 있는 <thead>와 <tbody>는 개별 item들과는 상관이 없는 요소였다. 그래서 xpath상의 tbody를 지운체, 다시 자식을 그대로 타고 가보았더니 최종적으로 원하는 데이터가 나왔다.
(a= 하이퍼링크요소, 까지 다시 타고 들어가야한다!)
response.xpath('//*[@id="top_movies_main"]/div/table/tr[1]/td[3]/a')
(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()
이 때, 맨마지막에 .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) - 이제 하나의 item에 대한 a요소로 찾은 상세페이지url이 아니라 전체 item들에 대한 url들이 모두 필요하다.
html구조를 거슬러 올라가서 찾아보자.
a요소 > td > tr까지 올라가보니까, tr이 반복되면서 , 리스트의 하나의item들이 각각 반복되는 것을 확인할 수 있다.
즉, <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)해준다. - 이제 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를 알아야한다
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)
리스트의 100개의 item에 대해, 하이퍼링크 url을 얻었다.
얻은 100개의 url을 가지고, 각 웹페이지를 요청하기 위해서, scrapy에서 제공하는 request함수를 사용할 것이다.
<하이퍼링크를 타고들어가서, 상세페이지의 item의 xpath를 통해, 필요한 요소들 뽑아오는 과정>
- (1)상세 페이지의 제목을 클릭후 xpath를 카피해, response.xpath()를 통해, selector를 얻어냈다면,
(2) /text()를 xpath에 더해서 제목을 가지는 셀렉터인지 확인하고,
(3) [0].extract()를 통해 텍스트만 뽑아내보자.
response.xpath('//*[@id="heroImageContainer"]/a/h1/text()')[0].extract()
(4) 이제 추출한 텍스트 앞에 있는 <\n과 공백>을 제거하기 위해서, 문자열에 대해서, .strip()함수를 적용시키면된다.
response.xpath('//*[@id="heroImageContainer"]/a/h1/text()')[0].extract().strip() - 비슷한 방법으로 영화 평점도 뽑아오자.
<예외처리2>
- 1개의 평점을 클릭한 뒤, xpath를 통해, 셀렉터를 뽑았는데 2개의 셀렉터가 나왔다.(아마도 크롬의 xpath뽑는 기준이 완벽치않음)
이 때는, 우리가 원하는 text()만 뽑은 다음, 결과물에 인덱싱[0]을 통해서, 첫번째것만 가져오는 식으로 한다. 그리고 extract()한다.
(기존에 항상 [0].extract()한 것과 동일함)
response.xpath('//*[@id="tomato_meter_link"]/span[2]/span/text()')[0].extract()
이렇게 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( 리스트)함수로 리스트를 합치면서,spider클래스의 parse()함수에서 나온 리스트형data는, join없이 그냥 리스트를 list변수에 담아두고, 변수에 map()으로 줄바꿈+공백만 삭제
각각에 strip()이 적용시킨다.
객체의 필드 = ''.join(genres_list).strip()
-> 뒤 pipelines에서 join해줄 것이다. - 총평(census)는 일단 생략하자. 그리고 마지막은 yield를 통해 채워진 객체 item을 반환해주자.
- 이제 우리가 제작한 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개 다 스크래핑 못해오더라.
결과물을 프로젝트 폴더에서 확인해보면, RTItem에는 있는 필드지만, 스크래핑을 생략한 필드에 대해서는 , , 으로 비어서 나온다.
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을 종료시키자.
<예외처리 4>
한글로 주석달아서, 파이썬이 한글을 못읽는 문제 :
SyntaxError: Non-ASCII Character 관련된 에러라고 부릅니다. 코드 내에 한글을 파이썬이 읽어들이지 못해서 발생되는 에러
해당 *.py의 첫번째 줄or 두번째 줄에 : # -*- coding: utf-8 -*-
주석을 추가해준다.
<예외처리 5>
나같은 경우, 장르 리스트의 2번째 성분 앞에 있는 < \n 공백 >이 안사라졌다.
구글링 결과, list에 \n이라는 유니코드를 먼저 없어줘야한다.
genres_list = map(unicode.strip, genres_list) 를 통해서, \n을 먼저 사라지게 한 뒤,item["genres"] = ', '.join(genres_list).strip() 를 통해, 구분자로서 콤마(,)를 넣고, 공백도 제거하자.
--> join은,, 나중에 pipelinse에서 할 것이다.
웹스크래핑을 통해 얻어진 데이터를, 어떻게 가공하고, 외부파일로 저장할지 구체적으로 명시하기 위한 item pipelines
- 프로젝트 폴더> 프로젝트명 폴더 > [pipelines.py]를 열자.
(1)기존 클래스를 제거하고, csv모듈을 import한다.
import csv
(2) [RTPipeline]클래스를 정의한다.(object를 상속한다)
(3) 생성자함수( 자바에서는 클래스의 객체생성시, 필드들 초기화)를 정의해주는데, 생성자안에
import한 csv모듈을 이용하여, 매 행을 csv파일을 쓰도록 정의해줄 것이다. - 이제 새로운 함수 process_item()함수를 정의할 것인데, 인자로서는 self외에 item(객체)과 spider가 들어간다.
spider를 이용해 수집한 각 item들을 어떻게 처리할지를 명시하게 된다.
파이썬 리스트를 하나 생성한 뒤에, 각 item객체의 필드들을 append할 것이다.
genres는 리스트이므로 |문자를 구분자로 하여 .join으로 각각을 연결해준다.
다음에는 self.csvwriter.writerow를 통해 순서대로 쌓아둔 리스트(row)를 각 행에 밀어넣는다.
이제 객체를 사용한 item은 returnd을 해줘야한다. - 이제 정의한 item pipelines 를 크롤링시 사용하도록, settings에서 설정을 해줘야한다.
(1) [settings.py]를 열고, # ITEM_PIPELINES 부분을 찾아 주석을 해제한 뒤,
pipelines.py에 우리가 정의한 클래스(RTPipelines)를 명시해준 뒤 저장한다. - setting에서 크롤링시 자동으로 csv파일을 생성하여 쓰기 때문에,
scrapy crawl RottenTomatoes 뒤에 –o rt.csv를 적을 필요가 없다.
***나는 미리, 리스트를 join해버려서,,, 다시 join시키는 꼴이므로,, 각 글자마자.. |가 찍힌다.
*** pipelines에서 join함수를 사용할거면,
rt_spider.py에서, 각 필드에 담을 때는,, join없이, 그냥 리스트를 유지하고, \n등 유니코드만 제거해준다.
my) spider에서 객체에 xpath를 통해 추출해서 담을 때는, 리스트는 유지하고 join쓰지말고, map()으로 유니코드만 제거 + yield로 item리턴
pipelines에서 객체의 필드를 꺼내 쓸때는, 리스트의 경우 구분자와 join을 써서 각 항목 보기좋게 구분 + return으로 item리턴
#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"] )
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
최종 결과물
'빅데이터 관련 프로그래밍 > 웹 크롤링 - scrapy & selenium' 카테고리의 다른 글
참고 : 2가지 크롤링 요약 (0) | 2018.03.11 |
---|---|
5. 동적 웹페이지 웹 스크래핑하기(scrapy+selenium) + 파이썬3.6에 설치 (8) | 2018.03.11 |
3. Scrapy 기본구조와 간단한 웹 스크래핑 (2) | 2018.03.08 |
2. scrapy 및 selenium 설치하기(Windows용) (0) | 2018.03.07 |
1. 웹 크롤링(스크래핑)의개념과 Scrapy & Selenium (1) | 2018.03.06 |