Warning) 아래 코드들은 Google Colab 환경에서 작성된 것이기에 거의 그럴 일은 없지만 다른 환경에서는 정상적으로 작동되지 않을 수도 있음을 알려드립니다. 또한, 형편없는 저의 실력으로 코드들이 다소 비효율적일 수 있음을 미리 말씀드립니다. 우연히 이 글을 보게 되신 분들은 참고해주시기 바랍니다. 


STEP 4. Lexicon 사전을 통해 Critic리뷰 감성 분석

STEP 4에서는 STEP 3에서 만든 2가지 함수를 통해 본격적으로 영화 '탑건'의 Critic 리뷰를 분석해보도록 하겠습니다. STEP 2에서 웹스크래핑한 결과인 movie_review_df에 각각 함수를 적용한 결과를 새로운 열로 추가하도록 하겠습니다. SentiWordNet 분석 결과는 ['Senti_swn']이라는 컬럼 이름으로 VADER 분석 결과는 ['Senti_vader']이라는 컬럼으로 생성하였습니다. 그리고 상위 10개만 출력해보도록 하겠습니다. (출력은 10개로 했지만 캡쳐가 잘려서 8개 결과 밖에 보이지 않습니다.)

# swn_polarity()를 이용하여 리뷰에 대한 감정분석 결과를 새로운 열로 추가
movie_review_df['Senti_swn'] = movie_review_df['Review'].apply(lambda x : swn_polarity(x))
# vader_polarity()를 이용하여 리뷰에 대한 감정분석 결과를 새로운 열로 추가
movie_review_df['Senti_vader'] = movie_review_df['Review'].apply(lambda x : vader_polarity(x))
movie_review_df[:10]

분석 결과를 새로운 열로 추가한 movie_review_df

분석 결과를 보면 swn기반과 vader 기반의 결과가 서로 다른 경우도 적지 않아 보입니다. 두 분석의 결과가 다른 것이 총 몇 개 정도 되는지 한번 알아보도록 하겠습니다. True는 두 분석 결과가 같은 경우이고 False는 분석 결과가 서로 다른 것을 나타냅니다. 총 409개의 리뷰 중 118개의 리뷰는 서로 다른 분석 결과를 보이고 있습니다. 

(movie_review_df['Senti_swn'] == movie_review_df['Senti_vader']).value_counts()

두 분석 결과가 다른 경우 확인

통상적으로 VADER 분석이 SentiWordNet 분석에 비해 정확도가 더 높은 것으로 알려져 있습니다. 실제로 몇몇 리뷰를 직접 확인해본 결과 VADER 분석이 더 우수한 분석 결과를 보인 것으로 확인 되었습니다. 따라서 VADER 분석 결과를 기준으로 하여 Good/Bad를 나누도록 하겠습니다. 분석 결과가 1이면 Good 출력, 0이면 Bad를 출력하는 good_bad_dc() 함수를 만들어서 활용하겠습니다. 

# 각 감정 분석 함수를 통해 구한 값을 비교하여 최종 Good/Bad 출력하는 함수
def good_bad_dc(review):
  if review['Senti_vader'] == 1:
    return 'Good'
  else:
    return 'Bad'

good_bad_dc()을 적용한 결과를 새로운 컬럼 ['Good/Bad']으로 하여 추가하도록 하겠습니다. 그리고 movie_review_df를 새로 출력해보겠습니다. 

good_bad_list = []
for i in range(movie_review_df.shape[0]):
  good_bad_list.append(good_bad_dc(movie_review_df.loc[i,:]))
movie_review_df['Good/Bad'] = good_bad_list
movie_review_df

Good/Bad 컬럼을 추가한 movie_review_df

여기서 잠깐 캡쳐한 사진 마지막 8번 리뷰를 한 번 살펴보겠습니다. 'The Jacobin'의 Eileen Jones씨가 남긴 리뷰인데 swn, vader 둘 다 해당 리뷰에 대해 Positive 평가를 내렸습니다. 그러나 실제로 리뷰 내용을 살펴보면 매우 부정적인 리뷰임을 알 수 있습니다. 로튼 토마토 사이트에서 직접 확인해본 결과 아래와 같이 'rotten(섞은)'이라는 마크를 남기며 혹평을 한 것으로 확인되었습니다. 아마도 맥락을 확인하지 않고 단어의 긍부정 여부만 가지고 판단하다보니 생긴 문제인 것 같습니다. 

총 409개의 리뷰 중 몇 개가 Good으로 분류되고 Bad로 분류되는지 확인해보도록 하겠습니다. 우선 개수를 파악해보고 비율로서의 결과도 출력해보겠습니다. 

movie_review_df['Good/Bad'].value_counts()

# 영화에 대한 Good/Bad 비율
movie_review_df['Good/Bad'].value_counts(normalize=True)*100

총 409개의 리뷰 중 74%인 305개가 긍정 리뷰로 평가되었고 25%인 104개의 리뷰가 부정 리뷰로 평가되었습니다. 로튼 토마토 사이트는 영화에 대한 평점을 전문가와 대중 평점으로 나누어 점수를 토마토의 신선도에 비유해 표현합니다. 신선도가 높을수록 좋은 평가를 받은 것이며 전문가와 대중은 각각 어떤 평가를 내렸는지 서로 비교해볼 수 있는 장점이 있습니다. 이번 프로젝트에서는 아쉽게도 대중평가는 살펴보지 못하고 전문가 리뷰만 살펴보았습니다. 그럼에도 감성 분석 알고리즘이 정확히 분석을 했는지 알아보기 위해 로튼 토마토 평점 지수를 가져와 비교해보도록 하겠습니다. 

영화 '탑건-메버릭'의 전문가 평점 지수는 무려 97%(프로젝트를 수행했던 당시)입니다. 정확히 어떤 방식으로 점수를 산정하는지는 모르겠지만 대략 97%의 전문가가 좋은 평가를 한 것이라고 유추해볼 수는 있습니다. 하지만 앞서의 분석 결과를 살펴보면 긍정 리뷰는 74%로 실제 평점 지수와 상당한 차이가 나타납니다. 차이가 발생한 이유에는 여러가지가 있겠지만 context를 고려한 분석이 된다면 더 좋은 결과를 산출할 수 있을 것으로 예상됩니다. 재미로 시작한 프로젝트이지만 정확도가 낮고 날카로운 분석이 되지 못한 점이 조금은 아쉬움이 남습니다. 다음에는 더 향상된 성능을 가진 분석 모델을 가져와 실험해보도록 하겠습니다. 이상으로 [로튼 토마토 리뷰 감성 분석] 미니 프로젝트를 마치도록 하겠습니다. 

Warning) 아래 코드들은 Google Colab 환경에서 작성된 것이기에 거의 그럴 일은 없지만 다른 환경에서는 정상적으로 작동되지 않을 수도 있음을 알려드립니다. 또한, 형편없는 저의 실력으로 코드들이 다소 비효율적일 수 있음을 미리 말씀드립니다. 우연히 이 글을 보게 되신 분들은 참고해주시기 바랍니다. 


STEP 3. 로튼 토마토 사이트에 있는 Critic리뷰 감성 분석

STEP 2에서 영화 '탑건'의 리뷰 데이터를 스크래핑하는 작업을 진행하였습니다. 지금부터는 리뷰 데이터를 토대로 감성 분석을 진행하여 영화 '탑건'이 Critic(비평가)들로부터 전반적으로 어떤 평가를 받고 있는지 살펴보도록 하겠습니다. 이번 리뷰 분석에서는 알고리즘 모델에 학습시킬 output변수가 따로 없기 때문에 비지도학습 기반의 감성 분석(Lexicon based approach)을 사용하도록 하겠습니다. 

앞으로 진행될 과정에 대해 간략히 설명 드리면, 2가지 감성 사전(SentiWordNet, VADER)을 이용해서 리뷰 분석을 진행하고 2가지 분석 결과를 적절히 혼합하여 최종적으로 리뷰의 평가를 GOOD/BAD로 분류하게 됩니다. SentiWordNet, VADER 이 2가지 사전을 이용하는 큰 이유는 없습니다. SentiWordNet이 VADER에 비해 예측 정확도가 현저히 떨어진다고 알고 있는데 단지 이를 한번 확인해보고자 해서 다음과 같이 진행하게 되는 것입니다.

STEP 3에서는 SentiWordNet, VADER 각각의 사전을 통해 리뷰를 분석할 수 있는 함수를 만드는 것으로 마무리될 것입니다. 함수를 이용한 분석은 STEP 4에서 진행하겠습니다. 지금부터 감성 분석에 사용된 방법들은 [파이썬 머신러닝 완벽 가이드, 권철민 저]를 기반으로 하고 있습니다. 


(1) SentiWordNet Lexicon을 이용한 리뷰 감성 분석 함수 생성

SentiWordNet을 이용하기 위해 우선 nltk의 모든 데이터와 패키지를 다운로드하겠습니다. 다 설치하려다 보니 시간이 조금 걸립니다.

import nltk 
nltk.download('all')

nltk 설치가 완료되었으면 필요한 모듈들을 import 하겠습니다. (1) 토큰화된 단어들의 원형을 가져오기 위해  nltk 패키지에 있는 WordNetLemmatizer를 import하고 (2) SentiWordNet기반의 Lexicon사전을 import하고 (3) 문장 토큰화, 단어 토큰화, 그리고 품사 태깅을 위해 각각 필요한 모듈을 가져오겠습니다. 

from nltk.stem import WordNetLemmatizer #(1)
from nltk.corpus import sentiwordnet as swn #(2)
from nltk import sent_tokenize, word_tokenize, pos_tag #(3)

본격적으로 SentiWordNet 기반의 분석 함수를 만들기 전에 penn_to_wn() 함수를 만들겠습니다. 이 함수를 만드는 이유는 WordNet에 사용할 수 있는 품사형태로 만들어주기 위해서입니다. 품사를 구분하는 방식은 언어나 학자마다 다른데 NLTK에서는 PennTreeBank형식의 태그를 가져오게 됩니다. 따라서, NLTK의 품사 태깅된 결과를 WordNet의 품사 태깅으로 바꾸어 주어 이후에 Lemmatize와 Synset 객체를 만들 수 있도록 해야 합니다.

[파이썬 머신러닝 완벽 가이드]의 저자는 형용사, 명사, 부사, 동사 이렇게 4가지 품사만 변환하도록 하였습니다. 이 과정에서 인칭 대명사, 한정사, 'to' 등 일부 단어들이 손실됩니다. 아마 형용사, 명사, 부사, 동사들이 감성에 많은 영향을 주는 단어이기 때문에 이 4가지 품사만을 한정하여 분석에 활용하고자 한 것으로 생각됩니다. 

def penn_to_wn(tag):
    if tag.startswith('J'):
        return wn.ADJ
    elif tag.startswith('N'):
        return wn.NOUN
    elif tag.startswith('R'):
        return wn.ADV
    elif tag.startswith('V'):
        return wn.VERB
    return

이제 대망의 swn_polarity() 함수를 살펴보겠습니다. 정말 복잡한 함수입니다. 해당 함수의 로직을 중간 중간 주석을 통해 설명하였습니다. 그 과정이 상당히 길고 복잡하기에 한 번에 이해가 안 될 수도 있습니다. 가장 중요한 부분이라고 할 수 있는 두 번째 for문에 대해 정리하면 다음과 같습니다. 주석 처리된 각 단계마다 어떤 결과가 도출되는지 예시를 통해 설명하도록 하겠습니다. 문장 토큰화가 완료된 상태라 가정하고 아래 한 문장을 가지고 각 단계의 출력을 보여드리겠습니다.

ex) She wishes he came to visit every week because he tells great stories and makes her favorite dinner.

(1) 단어 토큰화 후 (단어, NTLK 기반의 품사 태그)의 튜플 추출

[('She', 'PRP'), ('wishes', 'VBZ'), ('he', 'PRP'), ('came', 'VBD'), ('to', 'TO'), ('visit', 'VB'), ('every', 'DT'), ('week', 'NN'), ('because', 'IN'), ('he', 'PRP'), ('tells', 'VBZ'), ('great', 'JJ'), ('stories', 'NNS'), ('and', 'CC'), ('makes', 'VBZ'), ('her', 'PRP$'), ('favorite', 'JJ'), ('dinner', 'NN'), ('.', '.')]

(2) NTLK 품사 태그를 WordNet 품사 태그로 변환

[('She', None), ('wishes', 'v'), ('he', None), ('came', 'v'), ('to', None), ('visit', 'v'), ('every', None), ('week', 'n'), ('because', None), ('he', None), ('tells', 'v'), ('great', 'a'), ('stories', 'n'), ('and', None), ('makes', 'v'), ('her', None), ('favorite', 'a'), ('dinner', 'n'), ('.', None)]

(3) 명사, 형용사, 부사, 동사 아닌 것은 패스하고 lemmatizer로 원형 단어 추출

['wish', 'come', 'visit', 'week', 'tell', 'great', 'story', 'make', 'favorite', 'dinner']


(4) 추출된 원형 단어와 품사 태그를 입력하여 Synset 객체 생성 (output 일부 생략)

[[Synset('wish.v.01'), Synset('wish.v.02'), Synset('wish.v.03'), Synset('wish.v.04'), Synset('wish.v.05'), Synset('wish.v.06')],  [Synset('come.v.01'), Synset('arrive.v.01'), Synset('come.v.03'), Synset('come.v.04'), Synset('come.v.05'), Synset('come.v.06'), ....(중략)...[Synset('favorite.s.01'), Synset('favored.s.01')], [Synset('dinner.n.01'), Synset('dinner.n.02')]]


(5) sentiwordnet의 감성 분석으로 SentiSynset 추출, 이때 synset 리스 중 첫 번째 값을 사용

[SentiSynset('wish.v.01'), SentiSynset('come.v.01'), SentiSynset('visit.v.01'), SentiSynset('week.n.01'), SentiSynset('state.v.01'), SentiSynset('great.s.01'), SentiSynset('narrative.n.01'), SentiSynset('make.v.01'), SentiSynset('favorite.s.01'), SentiSynset('dinner.n.01')]


(6) 감성 분석 결과는 (긍정 - 부정)을 더해가며 계산

[wish.v.01]에 대한 pos점수: 0.125, neg점수: 0.375 , [come.v.01]에 대한 pos점수: 0.0, neg점수: 0.0, [visit.v.01]에 대한 pos점수: 0.0, neg점수: 0.0,[week.n.01]에 대한 pos점수: 0.0, neg점수: 0.0, [state.v.01]에 대한 pos점수: 0.0, neg점수: 0.0, [great.s.01]에 대한 pos점수: 0.0, neg점수: 0.0, [narrative.n.01]에 대한 pos점수: 0.0, neg점수: 0.0, [make.v.01]에 대한 pos점수: 0.0, neg점수: 0.0, [favorite.s.01]에 대한 pos점수: 0.125, neg점수: 0.0 , [dinner.n.01]에 대한 pos점수: 0.0, neg점수: 0.0

def swn_polarity(text):
    # 감성 지수 초기값 
    sentiment = 0.0
    tokens_count = 0

    lemmatizer = WordNetLemmatizer() # WordNetLemmatizer 객체 생성
    raw_sentences = sent_tokenize(text) # 문장 토큰화
    
    for raw_sentence in raw_sentences:
        # (1)단어 토큰화 후 (단어, NTLK 기반의 품사 태그) 추출 
        tagged_sentence = pos_tag(word_tokenize(raw_sentence))

        for word , tag in tagged_sentence:
            # (2)NTLK 품사 태그를 WordNet 품사 태그로 변환
            wn_tag = penn_to_wn(tag) 

            # (3)명사, 형용사, 부사, 동사 아닌 것은 패스하고 lemmatizer로 원형 단어 추출
            if wn_tag not in (wn.NOUN , wn.ADJ, wn.ADV, wn.VERB): 
                continue                  
            lemma = lemmatizer.lemmatize(word, pos=wn_tag)
            if not lemma:
                continue

            # (4)추출된 원형 단어와 품사 태그를 입력하여 Synset 객체 생성 
            synsets = wn.synsets(lemma , pos=wn_tag)
            if not synsets:
                continue

            # (5)sentiwordnet의 감성 분석으로 감성 synset 추출, 이때 synset 리스 중 첫번째 값을 사용
            synset = synsets[0]
            swn_synset = swn.senti_synset(synset.name())
            
            # (6)감성 분석 결과는 (긍정 - 부정)을 더해가며 계산
            sentiment += (swn_synset.pos_score() - swn_synset.neg_score())        
            tokens_count += 1
    
    if not tokens_count:
        return 0
    
    # 총 score가 0 이상일 경우 긍정(Positive) 1, 그렇지 않을 경우 부정(Negative) 0 반환
    if sentiment >= 0 :
        return 1
    
    return 0

(2) VADER Lexicon을 이용한 리뷰 감성 분석 함수 생성

vader_polarity() 함수는 간단합니다. SentimentIntensityAnalyzer() 객체만 생성해주면 polarity_scores() 메서드를 통해 알아서 감성 점수 결과값을 가져오게 됩니다. 쉽게 말하면 앞에서 SentiWordNet 기반의 복잡한 함수 과정을 한 번에 메서드 하나로 해결이 가능하다는 것입니다. 이상으로 STEP 3를 마치고 다음 STEP 4에서는 앞서 생성한 2개의 함수를 통해 본격적으로 감성분석에 들어가도록 하겠습니다. 

from nltk.sentiment.vader import SentimentIntensityAnalyzer

def vader_polarity(review,threshold=0.1):
    analyzer = SentimentIntensityAnalyzer()
    scores = analyzer.polarity_scores(review)
    
    # compound 값에 기반하여 threshold 입력값보다 크면 1, 그렇지 않으면 0을 반환 
    agg_score = scores['compound']
    final_sentiment = 1 if agg_score >= threshold else 0
    return final_sentiment

Warning) 아래 코드들은 Google Colab을 기반으로 작성된 것이기에 다른 환경에서는 정상적으로 작동이 안 될 수 있음을 알려드립니다. 또한, 형편없는 저의 실력으로 코드들이 다소 비효율적일 수 있음을 미리 말씀드립니다. 우연히 이 글을 보게 되신 분들은 참고해주시기 바랍니다. 


STEP 2. 로튼 토마토 사이트에 있는 Critic리뷰 스크래핑 

(1) Selenium 및 webdriver 설치

감성분석을 수행하기 하기 전 필요한 데이터(리뷰)들을 수집하는 스크래핑 과정을 보여드리겠습니다. 간략히 로직에 대해 설명드리면, 로튼 토마토 사이트 내 특정 영화 리뷰에 대한 URL을 입력하면 해당 영화 Critic 리뷰를 모두 긁어오는 것입니다. 보통 웹 스크래핑에서 BeautifulSoup과 Selenium이 양대산맥으로서 많이 사용되는 패키지들인데 지금은 Selenium을 사용하도록 하겠습니다. 앞서 STEP 1에서 설명드린 바와 같이 로튼 토마토에서는 모든 리뷰를 가져오기 위해 next 버튼을 누르며 20개 간격으로 리뷰를 스크래핑해야 하기 때문에 웹브라우저 조작이 가능한 Selenium이 더 편리하다고 할 수 있습니다.

우선 웹스크래핑을 위해 Selenium을 설치하도록 하겠습니다. (1) Selenium 설치 후, (2) 우분투 환경을 업데이트해주고 (3) chromium의 chromedriver을 설치해줍니다. (4) usr 디렉터리 안에 있는 chromedriver 폴더를 usr/bin 디렉터리로 복사해주고 나서 (6) '/usr/lib/chromium-browser/chromedriver' 폴더를 환경변수 지정해줍니다. 그러면 이 폴더 안에 있는 파이썬 파일들을 쉽게 import 할 수 있습니다.

!pip install Selenium #(1)
!apt-get update #(2)
!apt install chromium-chromedriver #(3)
!cp /usr/lib/chromium-browser/chromedriver /usr/bin #(4)

import sys #(5)
sys.path.insert(0, '/usr/lib/chromium-browser/chromedriver') #(6)

(2) 필요한 라이브러리 import 및 chorme_options 설정

Selenium 설치와 환경설정을 완료했으면 필요한 라이브러리를 import하고 chrome_options를 미리 설정하도록 하겠습니다. chrome_options를 설정하는 이유는 웹 스크래핑을 수행할 때 웹 브라우저를 띄우지 않으면서 스크래핑을 진행하기 위해서입니다. 웹 브라우저 창을 띄우면서 스크래핑을 진행하면 스크래핑하는 창과 코드가 실행되는 창을 왔다 갔다 하도록 코드를 짜줘야 하기 때문에 더 복잡하고 비효율적이게 됩니다. 다소 설명이 이상한 것 같긴 한데... 여하튼 '--headless', '--no-sandbox' 등 옵션 변수들을 추가해주도록 하겠습니다. 

import time
import pandas as pd
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException, ElementNotInteractableException
from selenium.webdriver.common.by import By

chrome_options = webdriver.ChromeOptions()

chrome_options.add_argument('--headless')
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')​

(3) page_click() 함수 만들기

다음으로는 page_click(), review_scraping() 함수를 만들도록 하겠습니다. 전자는 웹페이지 안에서 next 버튼을 눌러 다음 페이지로 이동하도록 하는 함수이고 후자는 리뷰를 긁어모으는 과정을 함수로 만든 것입니다. review_scraping()함수 안에서 page_click() 함수가 실행된다고 보시면 됩니다.

(※ page_click() 함수는 굳이 만들지 않고 review_scraping() 함수 안에서 직접 실행시키셔도 상관없습니다.)  

page_click()에서 주의하실 점은 find_element를 통해 next 버튼에 해당하는 element를 찾아내어 click한 후에는 꼭 time.sleep()을 1,2초 정도 해주시기 바랍니다. 다음 페이지로 이동하는데 보통 1초 내외의 시간이 소요되는데 이 속도보다 더 빠르게 스크래핑을 시작하게 되면 NoSuchElementException 등의 에러가 발생할 수 있기 때문입니다.

def page_click(wd):
  next_bt = wd.find_element(By.XPATH, '//*[@id="content"]/div/div/div/nav[1]/button[2]/span')
  next_bt.click()
  time.sleep(1)

(4) review_scraping() 함수 만들기

본격적으로 reveiw_scraping()함수를 만들어보도록 하겠습니다. webdriver 객체를 chrome으로 설정하여 생성합니다. 그리고 함수 인자로 받을 url값을 통해 로튼토마토 리뷰 페이지로 이동합니다. 스크래핑으로 통해 받을 내용은 'reviewer(리뷰 작성자)', 'publication(작성자가 속한 매체)', 'review(리뷰)' 총 3가지이며 최종 반환 값으로서 3가지 항목에 대한 내용을 pandas의 dataframe 형식으로 만들겠습니다. 스크래핑하는 과정과 dataframe으로 변환하는 과정은 아래 코드의 주석들을 통해 설명해놓았으니 참고하시기 바랍니다.

첫 페이지에서 20개의 리뷰를 모두 추출하여 dataframe으로 변환하였으면 앞서 만든 page_click() 함수를 호출하여 다음 페이지로 넘어갑니다. 그리고 while문 반복을 통해 20개의 리뷰를 추출하고 페이지를 넘어가는 과정을 반복합니다. 더 이상 페이지를 넘어갈 수 없다면(next 버튼이 존재하지 않는다면) 예외처리를 통해 반복을 break 하겠습니다. 리뷰가 정상적으로 스크래핑이 되고 있는지 육안으로 확인하기 위해 스크래핑 시작, 페이지 이동을 알리는 메시지들과 최종적으로 스크래핑을 진행한 영화의 리뷰가 총 몇 개 나왔는지 메시지를 출력하겠습니다.

def review_scraping(url):
  # webdriver 객체를 chrome으로 하여 설정
  wd = webdriver.Chrome('chromedriver', options = chrome_options) 
  wd.implicitly_wait(3) 

  # 해당 url로 이동
  wd.get(url)

  movie_review_df = pd.DataFrame() # 빈 move_review_df 생성
  page_no = 1

  print("[스크래핑 시작]")
  
  # 매 페이지마다 반복적으로 리뷰를 스크래핑하기 위한 while문
  while True:
    try:
      # 스크래핑할 3가지 항목에 대한 빈 리스트 생성 
      reviewer_list = []
      publication_list = []
      review_list = []
	  
      # find_elements를 통해 추출한 것들 중 text만 추출하여 빈 리스트에 추가
      reviewers = wd.find_elements(By.CSS_SELECTOR, 'div.col-xs-8.critic-info > div.col-sm-17.col-xs-32.critic_name > a.unstyled.bold.articleLink')
      reviewer_list += [reviewer.text for reviewer in reviewers]
      #print(reviewer_list)

      publications = wd.find_elements(By.CSS_SELECTOR, 'div.col-sm-17.col-xs-32.critic_name > a:nth-child(3) > em')
      publication_list += [publication.text for publication in publications]
      #print(publication_list)

      reviews = wd.find_elements(By.CLASS_NAME, 'the_review')
      review_list += [review.text for review in reviews]
      #print(review_list)
	  
      # 20개의 리뷰를 스크래핑하여 만든 리스트들을 dataframe 형식으로 변환
      new_movie_review_df = pd.DataFrame({'Reviewer': reviewer_list,
                                          'Publication': publication_list,
                                          'Review' : review_list})
      movie_review_df = pd.concat([movie_review_df, new_movie_review_df], ignore_index = True)

      # 해당 페이지의 리뷰 스크래핑이 완료되면 다음 페이지로 이동 
      page_click(wd)
      page_no += 1
      print('[다음 {}페이지로 이동]'.format(page_no)) # 페이지 이동할 때마다 메시지 출력
	
    # 예외처리
    except ElementNotInteractableException as ex:
      print("[모든 스크래핑이 완료되었습니다.]")
      break

    except NoSuchElementException as ex:
      print("[모든 스크래핑이 완료되었습니다.]")
      break
  
  # 더이상 넘어갈 페이지가 없어 스크래핑이 완료되면 아래 메시지 출력 
  mv_title = url.split('/')[-2].replace('_', ' ').title()
  review_cnt = movie_review_df.shape[0]
  print("\n영화 [{}]에 대한 {}개의 리뷰 스크래핑 완료".format(mv_title, review_cnt))
  
  # 스크래핑한 결과물인 movie_review_df를 반환
  return movie_review_df

(5) review_scraping() 함수를 통해 스크래핑 진행

리뷰 스크래핑을 위한 모든 준비가 되었으니 본격적으로 리뷰를 가져와 보도록 하겠습니다. 최근 '탑건-메버릭'이라는 영화가 개봉되었는데 영화 '탑건'을 예시로 하여 스크래핑을 진행해보도록 하겠습니다. (리뷰 작성일 기준 총 409개의 리뷰가 있었습니다.)

url = 'https://www.rottentomatoes.com/m/top_gun_maverick/reviews'
movie_review_df = review_scraping(url)

review_scraping() 메시지 출력 결과
movie_review_df 출력 결과 (11행까지)

Warning) 아래 코드들은 Google Colab을 기반으로 작성된 것이기에 다른 환경에서는 정상적으로 작동이 안 될 수 있음을 알려드립니다. 또한, 형편없는 저의 실력으로 코드들이 다소 비효율적일 수 있음을 미리 말씀드립니다. 우연히 이 글을 보게 되신 분들은 참고해주시기 바랍니다.


얼마전 미국에서 30년 만에 영화 탑건의 속편이 개봉되어 화제가 되었습니다. 미국에서는 5월 27일에 개봉하여 이미 1달이 지났지만 우리나라에서는 한참 뒤인 6월 22일 개봉 예정되어 있어 아직 대중들에게 크게 주목 받고 있지는 못한듯 싶습니다. 하지만 많은 사람들에게 80년대 개봉되었던 '탑건'의 1편은 많은 추억으로 자리잡고 있을 것입니다. 물론 저는 90년대생이기 때문에 한참이 지나서야 OTT를 통해 영화를 접하게 되었는데 아직도 정말 매력적인 영화로 기억되고 있습니다. 특히 주연배우인 톰크루즈의 전성기 시절 모습과 영화의 OST로 사용된  Berlin의 'Take my breath away'는 이 영화의 아이코닉한 장면이라 해도 과언이 아닌 듯 합니다.

어쩌다 보니 영화에 대한 수다로 빠질 뻔했는데 잠시 영화에 대한 이야기는 여기서 접어두고 본론으로 들어가 보겠습니다. 최근 텍스트 분석을 공부하면서 감성분석(Sentimental Analysis)에 대해 살펴본 적이 있습니다. 감성분석은 텍스트에 담겨 있는 의견이나 감성, 평가, 태도 등을 머신러닝, 딥러닝을 통해 분석하는 방법인데. 자연어 처리 분야에서 꽤 오랜 기간 연구된 분야라고 합니다. 저는 항상 새로운 모델이나 알고리즘을 공부하게 되면 실무에 적용해볼 수 있는 방안들을 고민해보는 편입니다. 그래서 감성 분석을 공부하면서 어디서 감성분석을 활용해볼 수 있을지 생각을 해보았고 앞서 말씀 드렸던 영화 '탑건'이 떠오르게 되었습니다. 아마 이미 많은 현장에서 감성 분석을 이용해 리뷰를 분석하는 모델들이 많이 상용화되고 있을 것입니다. 그래도 이를 한번 직접 제 스스로 구현해보고 두 눈으로 확인해보면 좋지 않을까 해서 영화 리뷰를 분석하는 토이 프로젝트를 진행해보았습니다. 아직 머신러닝, 텍스트 분석 분야에 이제 막 걸음마를 떼고 있는 수준이라 프로젝트의 퀄리티는 다소 미흡한 부분이 많습니다. 하지만 누군가에게는 또 다른 영감이 될 수 있다는 생각을 가지고 프로젝트를 소개하고자 합니다. 이 프로젝트에서 뭔가 심오한 내용을 얻어가시기 힘들 것 같고 그냥 귀엽게 봐주셨으면 좋겠습니다.


STEP 1. 로튼 토마토 사이트 감성 분석

(1) 아이디어 구상

우선, 어떤 식으로 프로젝트를 진행할지 정리해보겠습니다. brief하게 말씀드리면 영화 사이트에서 해당 영화에 대한 리뷰를 가져와 감성분석을 진행하고 이를 Good/Bad 두가지로 출력합니다. 그리고 영화의 전반적인 평가와 선호도를 Good/Bad의 상대적 비율로서 계산하여 산출하는 것입니다. 리뷰를 가져올 영화 사이트는 로튼 토마토입니다. 로튼 토마토는 제가 영화를 검색할 때 많이 이용하는 사이트이기도 하고 영화의 평점을 토마토의 신선도에 비유해 표현하는 매우 재미있는 사이트입니다. reviewer는 서술식으로 리뷰를 남기고 'Fresh', 'Rotten' 이 두가지로 영화를 평가하게 됩니다. 그러면 전체적인 평가를 통계 내어 아래와 같은 표식(mark)가 만들어집니다. 여하튼, 로튼 토마토 사이트의 리뷰를 토대로 프로젝트를 진행하도록 하겠습니다. 다만, 아래 보시는 바와 같이 리뷰는 크게 전문가 리뷰(Tomatometer)와 관객 리뷰(Audience Score)로 구분되는데 저는 전문가 리뷰만 이용하겠습니다. 관객 리뷰는 분석하기에 데이터양이 너무 많기 때문입니다.

'신선한(Fresh)'과 '썩은(Rotten)'의 차이


(2) 적용할 기법 선택

사실 프로젝트를 진행할 때 사용할 모델, 기법 등은 저의 능력과 한계에 의해 정해지는 편입니다. 더 정확하고 최신의 기법들을 사용하고 싶지만 잘 알지 못하기 때문에 그냥 제가 할 수 있는 범위 내에서 프로젝트를 진행하는 편이긴 합니다. 이번 프로젝트도 어쩔 수 없는 제 능력의 한계로 제한적인 상황 속에서 진행하였습니다. 

우선 리뷰를 가져오는 것은 웹스크래핑을 통해 진행하겠습니다. 웹스크래핑은 Selenium을 적용할 텐데 Selenium은 웹브라우저를 조작해가며 스크래핑할 수 있는 장점을 가지고 있습니다. 아래 사진과 같이 400여 개의 리뷰 모두를 가져오기 위해서 계속 next 버튼을 누르며 다음 리뷰들을 가져와야 합니다. 따라서, BeautifulSoup을 사용하기에는 제한이 되며 Selenium의 webdriver 객체를 생성해 url을 불러와 웹사이트의 next 버튼을 누를 수 있도록 하겠습니다.

감성분석에 사용될 방법은 Lexicon 기반의 접근법(Lexicon-based)입니다. 그 중에서도 감성사전(Dictionary-based)을 이용해서 분석을 진행하겠습니다. 특별한 이유 때문에 해당 방법론을 적용하는 것은 아닙니다. 단지 제가 최근에 공부한 방법이기도 하고 아직 머신러닝 기반이나 Transformer, BERT와 같은 인공신경망 기법들을 적용하기에는 아직 실력이 부족하기 때문입니다. 아래 사진과 같이 감성분석(Sentiment Analysis)에는 정말 많은 방법론들이 있습니다. 상황에 따라 다른 방식들을 적용해볼 수 있기에 제 프로젝트는 수많은 방법 중 하나로만 참고 해주시면 감사하겠습니다.

출처:yngie-c.github.io


(3) 프로젝트 플로우 차트(Flow Chart)

대략적인 그림은 그려 보았으니 한번 큰 흐름을 정리해보도록 하겠습니다. 플로우 차트라고 하기 거창하지만 간단한 다이어그램으로 한눈에 살펴보겠습니다. 

+ Recent posts