네이버 영화 리뷰 키워드분석 (3) 리뷰 크롤링

글 쓰는게 여간 귀찮은게 아니다..

암튼 다음으로 넘어가보면 이제 전에 만들었던 baseurl에 접속해서 리뷰들을 크롤링하는 단계다.

 

#import한 패키지 목록

import re
import pandas as pd
import requests
from tqdm import tqdm
from bs4 import BeautifulSoup
import time

 

 

일단 편의를 위해 영화는 주토피아로 정해놓았다.

베이스주소로 이동해보면 

주토피아의 경우, 17921개의 리뷰들이 한 페이지당 10개씩 올라와있다.

따라서 총 페이지개수를 정할 수 있는데, 계산하기 귀찮으니 10개가 안되는 마지막 페이지는 버리고

총 리뷰수 % 10으로 정하자.

그럼 총 리뷰수를 가져오는 selector를 구하자

 

평점 수 부분은 score_total 클래스에서 strong, em을 거치면 접근할 수 있을 것 같다.

여기서 총 리뷰수를 구하고 10으로 나눠 페이지수를 구해보자

#영화는 주토피아로 고정
url = 'https://movie.naver.com/movie/bi/mi/pointWriteFormList.nhn?code=130850&type=after&onlyActualPointYn=N&onlySpoilerPointYn=N&order=sympathyScore&page={}'

res = requests.get(url)
index = 1
user_dic = {}
if res.status_code == 200:
    soup=BeautifulSoup(res.text,'lxml')
    total = soup.select('div.score_total > strong > em')[0].text
    pages = int(total.replace(',','')[:-1]) #17,921 > 17921로 변환 후 캐스팅

 

잘 나온 것 같다. baseurl 뒷편에 페이지번호를 붙여서 url을 만든 후 접속해서 크롤링 해오면 되겠다.

이전 포스팅 설명에서는 베이스주소에 {}이 없었는데, 사실 하단의 전체코드를 보면 마지막에 '{}'이 추가되었다.

이는 지금 페이지를 포매팅하기 위함이다.

 

아...이제부터가 진짜 귀찮은데 하나하나 차근차근 정리해보겠다.

 

일단 내가 크롤링하고 싶은 데이터는 각각의 리뷰와 평점이다.

 

잘 보면, score_result 클래스 > ul 아래에 li가 여러개 있고 하나의 li는 하나의 리뷰칸을 차지한다.

li 하나를 열어보면 위 사진처럼 3개의 div로 되어있는데 첫번째가 평점쪽, 두번째가 리뷰쪽이다.

마지막 아래는 추천/비추천 버튼 공간이니 관심대상이 아니다.

 

먼저 평점부터 추출해보자. 위 사진을 보면 star_score클래스 아래의 em에 있다. 

리뷰는 score_reple클래스 아래에 p를 거쳐 span에 있다. 여러개의 li가 다 이런 식으로 구성되어있다.

근데 주의해야할 점이 있다.

평점은 그대로 크롤링해도 괜찮은데, 

이런 관람객 표시도 리뷰와 같은 위치에 있기 때문에 크롤링하면 '관람객'도 같이 추가된다.

그리고 스포일러댓글의 경우 자동으로 필터링이 되는데 이때, '스포일러가 포함된 감상평입니다. 감상평 보기'도 함께 리뷰로 처리된다.

나는 각 리스트별로 평점 하나, 리뷰 하나를 뽑고 싶은건데, 위의 경우 심하면 평점 하나, 리뷰 셋이 뽑히게 된다.

따라서 리뷰의 경우' 관람객'이거나 저런 스포일러 안내문구가 아닌 경우만 크롤링해야한다.

 

일단 되는지 확인을 위해 한 페이지를 크롤링하는 코드를 먼저 구현해보자.

#####이 코드 말고 아래 코드 사용#######

comments = [] #리뷰
stars = [] #평점
res = requests.get(url)
if res.status_code == 200:
    soup=BeautifulSoup(res.text,'lxml')
    star =  soup.select('div.score_result > ul > li > div.star_score > em')
    tds = soup.select('div.score_result > ul > li > div.score_reple > p > span')
    for st in star:
        stars.append(int(st.text))
    for cmt in tds:
        if cmt.text != '관람객' and cmt.text !='스포일러가 포함된 감상평입니다. 감상평 보기':
            comments.append(cmt.text)
    if(len(comments) != len(stars)): #한 페이지에서 리뷰개수와, 별점개수가 같은지 확인
        print(url)

 

 

그런데 이렇게 리뷰를 출력하면 다음과 같은 현상이 벌어진다.

태그가 span이기 때문인가..? 앞뒤로 공백이 엄청나게 붙는다. (코알못임 이유모름 ㅠㅠ)

따라서 각 리뷰를 append하기 전에 정규표현식을 통해 공백들을 제거하였다.

# 리뷰 앞뒤로 쓸데없는 공백을 제거하는 함수
def no_space(text):
    text1 = re.sub(' | |\n|\t|\r', '', text)
    text2 = re.sub('\n\n','', text1)
    return text2

위 함수를 이용하여 comments에 추가하기 전에 정제해주자

comments = []
stars = []
res = requests.get(url)
if res.status_code == 200:
    soup=BeautifulSoup(res.text,'lxml')
    star =  soup.select('div.score_result > ul > li > div.star_score > em')
    tds = soup.select('div.score_result > ul > li > div.score_reple > p > span')
    for st in star:
        stars.append(int(st.text))
    for cmt in tds:
        if cmt.text != '관람객' and cmt.text !='스포일러가 포함된 감상평입니다. 감상평 보기':
            comments.append(no_space(cmt.text))  #####여기에 추가됨
    if(len(comments) != len(stars)):
        print(url)

결과를 확인해볼까?

 

보이는 대로 제대로 가져온 것을 확인할 수 있다 (다행 ㅠㅠ)

 

이제 위 코드를 활용하여 여러 페이지를 크롤링 해보자.

딱히 어려운 건 없다. 페이지 번호만 바꿔주면서 반복문을 돌리면 된다.

이때, 그냥 돌리다보면 언제끝날지 모르기에 tqdm을 사용하여 진행상황을 출력하도록 했다.

base_url = url
comments = []
stars = []
for page in tqdm(range(1,pages+1)):
    url = base_url.format(page)
    if res.status_code == 200:
        soup=BeautifulSoup(res.text,'lxml')
        star =  soup.select('div.score_result > ul > li > div.star_score > em')
        tds = soup.select('div.score_result > ul > li > div.score_reple > p > span')
        for st in star:
            stars.append(int(st.text))
        for cmt in tds:
            if cmt.text != '관람객' and cmt.text !='스포일러가 포함된 감상평입니다. 감상평 보기':
                comments.append(no_space(cmt.text))
        if(len(comments) != len(stars)):
            print(url)
            break

전체 페이지를 크롤링 하는 데 33초 정도 걸렸다.

개수도 틀리지 않고 잘 가져왔다. 애초에 마지막 페이지의 열개미만의 리뷰들은 버리기로 했으니.

이제 이 평점과 데이터들의 리스트를 데이터프레임으로 변환하여 csv로 저장하면 끝이다!

 

df = pd.DataFrame({"Review":comments, "Rank":stars})
df.to_csv('data/주토피아review.csv' , index= False) #파일경로

지정한 경로에 가서 파일이 제대로 쓰여졌는지 확인해보자

제대로 된 것 같다.

사실 csv파일을 저장할 필요 없이 만들어놓은 데이터프레임으로 진행하면 된다. 

근데 해보니까 미묘하게 다르고 파일로 하는게 코드 수정하기에도 편했다.

잘못해서 데이터프레임 잘못건들면 또 크롤링해야됨 ㅠㅠ 

다음에는 크롤링한 데이터로 모델링/키워드 추출 하기 전에 전처리하는 과정을 다룰 것이다.

전체코드는 여기 Click!!


 

def crawl_review(base_url):
    def no_space(text):
        text1 = re.sub(' | |\n|\t|\r', '', text)
        text2 = re.sub('\n\n','', text1)
        return text2
    res = requests.get(base_url)
    if res.status_code == 200:
        soup=BeautifulSoup(res.text,'lxml')
        total = soup.select('div.score_total > strong > em')[0].text
        pages = int(total.replace(',','')[:-1]) #17,921 > 17921로 변환 후 캐스팅
        print(f"{pages}개의 페이지에서 리뷰를 모으고 있습니다.")
        time.sleep(1)
    comments = []
    stars = []
    for page in tqdm(range(1,pages+1)):
        url = base_url.format(page)
        if res.status_code == 200:
            soup=BeautifulSoup(res.text,'lxml')
            star =  soup.select('div.score_result > ul > li > div.star_score > em')
            tds = soup.select('div.score_result > ul > li > div.score_reple > p > span')
            for st in star:
                stars.append(int(st.text))
            for cmt in tds:
                if cmt.text != '관람객' and cmt.text !='스포일러가 포함된 감상평입니다. 감상평 보기':
                    comments.append(no_space(cmt.text))
            if(len(comments) != len(stars)):
                print(url)
                break 
    assert len(comments) == len(stars)
    df = pd.DataFrame({"Review":comments, "Rank":stars})
    return df
 def save_and_load(dataframe):
    basepath = 'data/'
    dataframe.to_csv(basepath+'주토피아review.csv' , index= False)
    df = pdr.read_csv(basepath+'주토피아.csv')
    return df