Warning) 아래 코드들은 버전이 바뀌어 정상적으로 작동되지 않을 수도 있습니다. 따로 버전을 명시하지 않았기 때문에 에러가 발생되면 조금씩 수정해 나가면서 사용하시길 바랍니다. 또한, 형편없는 저의 실력으로 코드들이 다소 비효율적일 수 있음을 미리 말씀드립니다. 우연히 이 글을 보게 되신 분들은 참고해주시기 바랍니다. 

text-preprocessing-for-textanlytics.ipynb
0.31MB


STEP 4. 빈도 분석을 위한 텍스트 전처리

STEP3에서는 이전를 날리는 일이 없도록 하기 위해 제품별 url을 하나의 리스트로 묶어 반복 실행하지 않고 일부러 따로 따로 독립적으로 수집하였습니다.

(1) 라이브러리 import 및 파일 불러오기

import pandas as pd
import re

df = pd.read_csv('/content/drive/MyDrive/Github/review-topic-modeling-project/output_file/all_review.csv')
df['Review']

8개 제품에d은 같기 때문에 2개의 코드만 가져왔고 이와 같은 방식을 8번 진행했다고 보시면 됩니다. 수집 결과 아래 사진과 같이 총 8개의 csv 파일이 만들어지게 됩니다. 

(2) 특수문자 제거

df['Review'] = df['Review'].str.replace(pat=r'[^\w]', repl= r' ', regex=True)  # replace all special symbols to space
df['Review'] = df['Review'].str.replace(pat=r'[\s\s+]', repl= r' ', regex=True)  # replace multiple spaces with a single space

df

def extract_symbol(text):
    text = text.str.replace(pat=r'[^\w]', repl= r' ', regex=True)
    result = text.str.replace(pat=r'[\s\s+]', repl= r' ', regex=True)
    return result

(3) 한글 표현만 남기기

#한글 표현만 남기기 
def extract_word(text):
    hangul = re.compile('[^가-힣]') 
    result = hangul.sub(' ', text) 
    return result
print("Before Extraction : ",df['Review'][17480])
print("After Extraction : ", extract_word(df['Review'][17480]))

print("Before Extraction : ",df['Review'][1494])
print("After Extraction : ", extract_word(df['Review'][1494]))

df['Review'] = df['Review'].apply(lambda x:extract_word(x))

(4) 띄어쓰기 처리

pip install git+https://github.com/haven-jeon/PyKoSpacing.git
from pykospacing import Spacing

spacing = Spacing()
print("Before Fixing : ",df['Review'][324])
print("After Fixing : ", spacing(df['Review'][324]))
print("Before Fixing : ",df['Review'][14454])
print("After Fixing : ", spacing(df['Review'][14454]))

 

df['Review'] = df['Review'].apply(lambda x:spacing(x))

df['Review'][324]
#띄어쓰기 처리 
from pykospacing import Spacing
def extract_word(text):
    spacing = Spacing()
    result = text.apply(lambda x:spacing(x))

 

(5) 형태소 분석

!pip install konlpy

from konlpy.tag import Okt
okt = Okt()
# 리뷰 텍스트 정규화 처리
df['Review'] = df['Review'].apply(lambda x: okt.normalize(x))
df['Review']

df.to_csv('/content/drive/MyDrive/Github/review-topic-modeling-project/output_file/processed_all_review.csv', index = False)

지금까지 수집한 파일들은 output_file 폴더에 저장하였고 총 8개 파일이 있습니다. 파일들이 다 파편적으로 분리가 되어 있으면 텍스트 분석을 진행하는데 있어 번거로움이 발생합니다. 따라서, 파일들을 하나로 병합하도록 하겠습니다. 그리고 파일 병합을 진행하기 전에는 중복된 파일들이 있는지 확인하고 중복된 경우에는 제거하도록 하겠습니다.

words = " ".join(df['Review'].tolist())
words = okt.morphs(words, stem=True)
words
len(words)

실 정확하게 스크래핑을

(6) 한글자 처리 및 불용어 제거

path = '/content/drive/MyDrive/Github/review-topic-modeling-project/output_file'

one_word = [x for x in words if len(x) == 1]
one_word_set = set(one_word)
print(one_word_set)

#특정 한글자를 제외한 나머지 한글자는 drop
remove_one_word = [x for x in words if len(x) > 1 or x in ['맛', '술', '양', '짱']]
len(remove_one_word)

#각 단어별 빈도수 확인
from collections import Counter
frequent_words = Counter(remove_one_word).most_common()
frequent_words

with open('/content/drive/MyDrive/Github/review-topic-modeling-project/stopwords.txt', encoding='cp949') as f:
    stopwords_list = f.readlines()
stopwords = stopwords_list[0].split(",")
len(stopwords)

remove_stopwords = [x for x in remove_one_word if x not in stopwords]
len(remove_stopwords)

Counter(remove_stopwords).most_common()

words_frequency = dict(Counter(remove_stopwords))
words_frequency

(7) 빈출 키워드 워드 클라우드 생성

#!sudo apt-get install -y fonts-nanum
#!sudo fc-cache -fv
#!rm ~/.cache/matplotlib -rf

import matplotlib.pyplot as plt
from wordcloud import WordCloud
# jupyter notebook 내 그래프를 바로 그리기 위한 설정
%matplotlib inline

# unicode minus를 사용하지 않기 위한 설정 (minus 깨짐현상 방지)
plt.rcParams['axes.unicode_minus'] = False
import matplotlib.pyplot as plt

plt.rc('font', family='NanumGothic') 

df = pd.DataFrame(remove_stopwords, columns=['Word'])

wc = WordCloud(font_path='NanumGothic', colormap = 'summer', background_color = 'black', width=800, height=400, scale=2.0, max_font_size=250)
gen = wc.generate_from_frequencies(words_frequency)
plt.figure(figsize = (10, 8))
plt.style.use('seaborn-whitegrid')
plt.imshow(gen)
plt.grid(False)
plt.axis('off')
#wordcloud 이미지 저장
plt.savefig('/content/drive/MyDrive/Github/review-topic-modeling-project/output_file/wordcloud.png', dpi= 300)

후에는 중복된 리뷰 데이터들을 제거하겠습니다. 중복이 제거된 데이터프레임은 다시 새로운 파일로 저장하겠습니다. 

중복 제거까지 진행한 결과 최종 수집된 데이터는 19445개로 당초 예상했던 2만개에는 조금 못 미치는 정도입니다. 중복된 내용 때문에 계획한 샘플 수를 충족시키지는 못했지만 결과에는 큰 영향을 주지 않을 것 같아 그냥 진행하도록 하겠습니다. 

 

조금은 쓸데없는 과정이기는 하지만 쇼핑몰별로 수집된 리뷰 데이터의 수를 확인해보도록 하겠습니다. 앞서 프로젝트 계획 단계에서 편향되지 않은 분석 결과를 도출하기 위해 가능한 다양한 쇼핑몰에서의 리뷰를 가져오겠다고 말씀드린 적이 있습니다. 사실 리뷰의 출처가 되는 쇼핑몰이 다양할수록 리뷰 또한 다양하고 편향되지 않는다고 말할 수는 없습니다. 쇼핑몰과 리뷰 내용의 편향성은 상관관계가 높지 않을 것입니다. 하지만 한쪽 쇼핑몰의 리뷰만 가져올 경우에는 분명 해당 쇼핑몰만의 문제들이 분석 결과에 다소간 영향을 끼칠 수도 있는 있을 것입니다. 때문에 제 생각에는 쇼핑몰의 다양성을 유지하는 것이 그리 나쁜 선택은 아닌 것 같습니다. 

아래 결과를 보면 이건 쫌... 이라는 생각이 드실 것 같습니다. 생각보다 쇼핑몰간 편차가 커 보입니다. SSG닷컴이 거의 전체 샘플 데이터의 절반 이상을 차지하고 있고 11번가까지 정도만 천단위 샘플이 수집되었고 나머지는 거의 미미한 수준입니다. 한편, 쿠팡이나 마켓컬리와 같이 최근 많은 매출을 기록하고 있는 쇼핑 플랫폼의 데이터가 없는 것이 아쉬움이 남습니다. 쿠팡 같은 경우는 네이버 쇼핑과 연동이 되지 않아 따로 수집해야 되는 것으로 보입니다. 여튼 분포 결과가 샘플의 균형을 맞추기에는 다소 부족한 부분이 있지만 실제로 결과물을 가져와 보아야 정확히 알 수 있을 듯 싶습니다. 

 

다음 단계에서는 이번 프로젝트의 첫번째 핵심이라고 할 수 있는 텍스트 전처리를 진행하도록 하겠습니다. 전처리 수준에 따라 결과물의 내용이 달라질 수 있기 때문에 많은 심혈을 기울여야 될 것 입니다. 다음 스텝에서 뵙도록 하겠습니다.

 

 

 

Warning) 아래 코드들은 버전이 바뀌어 정상적으로 작동되지 않을 수도 있습니다. 따로 버전을 명시하지 않았기 때문에 에러가 발생되면 조금씩 수정해 나가면서 사용하시길 바랍니다. 또한, 형편없는 저의 실력으로 코드들이 다소 비효율적일 수 있음을 미리 말씀드립니다. 우연히 이 글을 보게 되신 분들은 참고해주시기 바랍니다. 

file-merge.ipynb
0.07MB


STEP 3. 데이터 수집하기

STEP3에서는 이전 단계에서 만든 스크래핑 함수를 사용해 데이터를 수집하도록 하겠습니다. 그리고 파일 형태로 출력한 데이터들을 일부 수정하고 파일들을 합쳐 하나의 데이터 파일로 만들어 보겠습니다. 이번 스텝에서는 인내를 가지고 오랜 시간을 기다리는 것이 관건입니다. 잘 기억은 나지 않지만 저의 경우 한 개 제품에 대해 스크래핑을 진행할 때 8분 정도 소요되었던 것으로 기억합니다. 그래서 8개 제품을 진행했을 때 대략 50분 정도 걸렸다고 할 수 있습니다. 수집하게 되는 샘플 데이터 수가 적은 편이라 일반적인 텍스트 분석 프로젝트에서 데이터를 수집하는 과정에서 소요되는 시간에 비해서는 세발이 피이지만 저한테는 쉽지 않았던 과정이었습니다. 런타임이 갱신되어 도중에 중단된 적도 몇번 있었습니다. 그래서 이후에도 다시 말씀드리겠지만 중간에 발생하는 오류로 데이터를 날리는 일이 없도록 하기 위해 제품별 url을 하나의 리스트로 묶어 반복 실행하지 않고 일부러 따로 따로 독립적으로 수집하였습니다.

(1) 제품별 리뷰 데이터 수집

8개 제품에 대한 네이버 쇼핑 url을 가져와 평점 리스트와 함께 shopping_review_scraping() 함수에 넣었습니다. 그리고 함수의 결과로 반환되는 데이터프레임을 csv 파일로 변환하여 저장하였습니다. 아래는 제품별로 스크래핑을 진행한 코드들입니다. url과 평점 리스트를 제외한 나머지 부분은 같기 때문에 2개의 코드만 가져왔고 이와 같은 방식을 8번 진행했다고 보시면 됩니다. 수집 결과 아래 사진과 같이 총 8개의 csv 파일이 만들어지게 됩니다.  

#풀무원 얇은피 꽉찬속 땡초만두
pulmuone_ddaengcho_url = 'https://search.shopping.naver.com/catalog/21554501906?NaPm=ct%3Dl77a62js%7Cci%3D5a5bdf33fddab8a692dc7dc7ad7b111851a8415a%7Ctr%3Dslsl%7Csn%3D95694%7Chk%3D3b59e90756e0c9de00247557f9d560f3908617a1'
pulmuone_ddaengcho_df = shopping_review_scraping(pulmuone_ddaengcho_url, [1,2,3,4,5])
pulmuone_ddaengcho_df.to_csv('pulmuone_ddaengcho_review.csv', index=False, encoding = 'utf-8-sig')​
#CJ제일제당 비비고 수제김치만두 400g×2
bibigo_url = 'https://search.shopping.naver.com/catalog/24088993894?query=%EB%B9%84%EB%B9%84%EA%B3%A0%20%EA%B9%80%EC%B9%98%EB%A7%8C%EB%91%90&NaPm=ct%3Dl75tunb4%7Cci%3D81e459f36f72ca8812ff811e28306c972e961483%7Ctr%3Dslsl%7Csn%3D95694%7Chk%3D923a154dbf1e412d62c5544e1e9435573a05215a'
bibigo_review_df = shopping_review_scraping(bibigo_url,[3,4,5])
bibigo_review_df.to_csv('bibigo_review.csv', index=False, encoding = 'utf-8-sig')


(2) 중복된 데이터 제거 후 파일 병합

지금까지 수집한 파일들은 output_file 폴더에 저장하였고 총 8개 파일이 있습니다. 파일들이 다 파편적으로 분리가 되어 있으면 텍스트 분석을 진행하는데 있어 번거로움이 발생합니다. 따라서, 파일들을 하나로 병합하도록 하겠습니다. 그리고 파일 병합을 진행하기 전에는 중복된 파일들이 있는지 확인하고 중복된 경우에는 제거하도록 하겠습니다.

사실 정확하게 스크래핑을 진행했다면 중복된 데이터가 없어야 하긴 합니다. 그러나 제가 만든 스크래핑 함수 중간에 페이지 이동이 원할하지 않은 부분이 있어서인지 csv 파일을 확인해본 결과 중복 데이터들이 존재하였습니다. 깔끔하게 수집이 되었으면 하는 아쉬움이 있었지만 여튼 중복을 제거하는 것은 간단한 일이니 함께 진행해보도록 하겠습니다. 

#output_file 폴더에 있는 리뷰 데이터 파일 리스트를 가져오기
path = './output_file'
file_list = os.listdir(path)
print ("file_list: {}".format(file_list))

#새로운 데이터 프레임 생성
df_all_review = pd.DataFrame()

#각 파일을 불러와 읽어낸 후 df_all_review에 결합
for file_name in file_list:
    df_new = pd.read_csv(path + '/' + file_name)
    df_all_review = pd.concat([df_all_review, df_new], ignore_index= True)

8개 파일에 있는 내용들을 새로운 데이터프레임 한 곳에 모은 후에는 중복된 리뷰 데이터들을 제거하겠습니다. 중복이 제거된 데이터프레임은 다시 새로운 파일로 저장하겠습니다. 

#중복된 리뷰 제거
df_all_review.drop_duplicates(subset=None, keep='first', inplace=True, ignore_index=True)
df_all_review = df_all_review.reset_index(drop=True)
df_all_review = df_all_review.rename(columns={'Reveiw':'Review'})

#병합한 데이터프레임을 새로운 파일로 저장
df_all_review.to_csv('./output_file/all_review.csv', index=False)

중복 제거까지 진행한 결과 최종 수집된 데이터는 19445개로 당초 예상했던 2만개에는 조금 못 미치는 정도입니다. 중복된 내용 때문에 계획한 샘플 수를 충족시키지는 못했지만 결과에는 큰 영향을 주지 않을 것 같아 그냥 진행하도록 하겠습니다. 


(3) 쇼핑몰별 리뷰 데이터 수 확인

조금은 쓸데없는 과정이기는 하지만 쇼핑몰별로 수집된 리뷰 데이터의 수를 확인해보도록 하겠습니다. 앞서 프로젝트 계획 단계에서 편향되지 않은 분석 결과를 도출하기 위해 가능한 다양한 쇼핑몰에서의 리뷰를 가져오겠다고 말씀드린 적이 있습니다. 사실 리뷰의 출처가 되는 쇼핑몰이 다양할수록 리뷰 또한 다양하고 편향되지 않는다고 말할 수는 없습니다. 쇼핑몰과 리뷰 내용의 편향성은 상관관계가 높지 않을 것입니다. 하지만 한쪽 쇼핑몰의 리뷰만 가져올 경우에는 분명 해당 쇼핑몰만의 문제들이 분석 결과에 다소간 영향을 끼칠 수도 있는 있을 것입니다. 때문에 제 생각에는 쇼핑몰의 다양성을 유지하는 것이 그리 나쁜 선택은 아닌 것 같습니다. 

#all_review 파일을 불러와 'Mall'에 대해 value_counts()를 적용
df_all_review.to_csv('./output_file/all_review.csv', index=False)
df_mall = pd.DataFrame(df_all_review['Mall'].value_counts())
print(df_mall)

#쇼핑몰별 리뷰 수에 대해 수평 막대 그래프 그리기
df = df_mall.sort_values(by=['Mall'], ascending=True)
df.plot.barh(y = 'Mall',
             figsize=(8, 8),
             xlabel="Mall Name",
             title="Reveiw Counts from Each Mall",
             width=0.8, 
             color=['#2a9d8f', '#e9c46a', '#f4a261', '#e76f51'], 
             legend=False)
plt.tight_layout()
plt.show()

아래 결과를 보면 이건 쫌... 이라는 생각이 드실 것 같습니다. 생각보다 쇼핑몰간 편차가 커 보입니다. SSG닷컴이 거의 전체 샘플 데이터의 절반 이상을 차지하고 있고 11번가까지 정도만 천단위 샘플이 수집되었고 나머지는 거의 미미한 수준입니다. 한편, 쿠팡이나 마켓컬리와 같이 최근 많은 매출을 기록하고 있는 쇼핑 플랫폼의 데이터가 없는 것이 아쉬움이 남습니다. 쿠팡 같은 경우는 네이버 쇼핑과 연동이 되지 않아 따로 수집해야 되는 것으로 보입니다. 여튼 분포 결과가 샘플의 균형을 맞추기에는 다소 부족한 부분이 있지만 실제로 결과물을 가져와 보아야 정확히 알 수 있을 듯 싶습니다. 

다음 단계에서는 이번 프로젝트의 첫번째 핵심이라고 할 수 있는 텍스트 전처리를 진행하도록 하겠습니다. 전처리 수준에 따라 결과물의 내용이 달라질 수 있기 때문에 많은 심혈을 기울여야 될 것 입니다. 다음 스텝에서 뵙도록 하겠습니다. 

Warning) 아래 코드들은 버전이 바뀌어 정상적으로 작동되지 않을 수도 있습니다. 따로 버전을 명시하지 않았기 때문에 에러가 발생되면 조금씩 수정해 나가면서 사용하시길 바랍니다. 또한, 형편없는 저의 실력으로 코드들이 다소 비효율적일 수 있음을 미리 말씀드립니다. 우연히 이 글을 보게 되신 분들은 참고해주시기 바랍니다. 

naver_shopping_review_scraping.ipynb
0.09MB

STEP 2. 네이버 쇼핑 리뷰 데이터 스크래핑 준비

STEP2에서는 분석 대상인 리뷰 데이터를 수집할 수 있도록 웹스크래핑을 준비하겠습니다. 풀무원, CJ 비비고, 동원 F&B, 노브랜드 4개 브랜드의 8개 제품에 대한 리뷰 데이터를 스크래핑하였으며 1점부터 5점까지의 리뷰를 골고루 가져올 수 있도록 하였습니다. 

(1) Selenium 및 웹 드라이버 설치

먼저, 동적 스크래핑을 위해 Selenium을 설치하고 크롬 웹브라우저를 통해 데이터를 수집할 수 있도록 web driver을 설치하겠습니다. 

!pip install Selenium
!apt-get update
!apt install chromium-chromedriver
!cp /usr/lib/chromium-browser/chromedriver /usr/bin

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

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

수집한 데이터를 데이터프레임 형식으로 정리하고 이를 csv파일로 저장하기 위해 pandas를 import하고 pandas 외에 추가적으로 필요한 라이브러리들을  import하겠습니다. 그리고 스크래핑 과정에서 불필요한 창 띄우기를 없애기 위해 웹브라우저에 대한 옵션들을 사전에 지정하겠습니다. 

#필요한 라이브러리 import
import time
import pandas as pd
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException, ElementNotInteractableException, NoAlertPresentException, UnexpectedAlertPresentException
from selenium.webdriver.common.by import By
import random

#chrome 웹드라이버 옵션 지정
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_review_scrap(): 해당 페이지 스크랩 함수 

본격적으로 스크래핑을 진행하는 함수를 정의하도록 하겠습니다. 총 2가지 함수를 정의하게 될텐데 첫 번째는 한 페이지에 대한 스크래핑을 진행하는 정적 스크래핑 함수입니다. page_review_scrap()는 한 페이지 내에 있는 20개의 리뷰를 수집하는 함수입니다. 네이버 쇼핑의 경우 아래 사진과 같이 1번, 2번, 3번 등 각 페이지에 20개씩의 리뷰가 있습니다. 따라서, 모든 리뷰를 계속 가져오기 위해서는 페이지를 넘겨가며 해당 페이지에 있는 리뷰를 긁어와야 합니다. 이를 위해 페이지 내 20개의 리뷰를 가져오는  page_review_scrap() 함수와 페이지를 넘겨가며 사전에 정의한 page_review_scrap()을 적용하는 shopping_review_scraping()함수를 각각 따로 정의해주었습니다. 

다시 위 사진을 보게 되면 네이버에서 제공하는 리뷰 데이터에는 평점, 쇼핑몰, 아이디, 날짜, 리뷰 내용, 사진 등의 정보가 담겨 있습니다. 이 중에서 쇼핑몰, 리뷰 내용, 평점 이렇게 3가지 정보만 수집해보도록 하겠습니다. 이후에 진행할 텍스트 분석에서는 리뷰 내용만을 사용하게 되기 때문에 쇼핑몰 정보와 평점 정보는 불필요한 정보이긴 하지만 혹시 모를 상황을 대비해 3가지 정보 모두 수집하였습니다. 

쇼핑몰, 리뷰 내용, 평점에 대한 각각의 XPATH를 확인하여 이에 대한 데이터를 각각의 리스트(mall_list, review_list, rating_list)에 넣어주고 이를 데이터프레임 형식으로 정리하여 최종 20개의 리뷰에 대한 스크래핑 결과를 가져오는 방식으로 진행하였습니다. 

def page_review_scrap(wd):

  try:
    alert = wd.switch_to.alert
    print(alert.text)
    alert.dismiss()
    time.sleep(random.uniform(1, 2))
  
  except:
    pass

  #수집할 데이터를 넣어줄 빈 데이터프레임과 리스트 지정
  review_df = pd.DataFrame()
  mall_list = []
  review_list = []
  rating_list = []
  
  #쇼핑몰, 리뷰 내용, 평점에 대한 각각의 XPATH를 통해 데이터를 수집
  malls = wd.find_elements(By.XPATH, '//*[@id="section_review"]/ul/li/div[1]/span[2]')
  mall_list += [mall.text for mall in malls]
  #print(mall_list)

  reviews = wd.find_elements(By.XPATH, '//*[@id="section_review"]/ul/li/div[2]/div/p')
  review_list += [review.text for review in reviews]
  #print(publication_list)

  ratings = wd.find_elements(By.XPATH, '//*[@id="section_review"]/ul/li/div[1]/span[1]')
  rating_list += [rating.text[-1] for rating in ratings]
  #print(review_list)
  
  #20개 리뷰에 대해 수집한 데이터를 데이터프레임 형식으로 변환
  new_review_df = pd.DataFrame({'Mall': mall_list,
                                'Reveiw': review_list,
                                'Rating' : rating_list})
  review_df = pd.concat([review_df, new_review_df], ignore_index = True)
  return review_df

(4) shopping_review_scraping: 쇼핑몰 리뷰 스크래핑 함수

앞서 정의한 page_review_scrap() 함수가 단일 페이지에 대한 정적 스크래핑 함수였다면 이제 정의하게 되는 shopping_review_scraping() 함수는 페이지를 이동하며 각 페이지에서 page_review_scrap()함수를 적용하는 동적 스크래핑 함수입니다. 이때, 페이지를 이동하기에 앞서 1점부터 5점까지의 리뷰를 골고루 가져오기 위해 각 평점에 대한 페이지로 먼저 이동하도록 하겠습니다. 만약, 각 평점 페이지로 이동하는 것을 따로 정의하지 않게 되면 5점 리뷰에 대해서만 데이터를 수집하게 되기 때문에 추후에 텍스트를 분석할 때 편향된 결과를 얻게 될 수 있습니다. 따라서, 긍정적인 리뷰, 부정적인 리뷰 모두를 분석할 수 있도록 다양한 평점 리뷰를 가져오는 프로세스를 적용해주어야 합니다. 

네이버 쇼핑에서 데이터를 수집할 때 유의해야 할 점 한 가지를 짚고 넘어가겠습니다. 네이버 쇼핑에서는 각 제품에 대한 리뷰를 2000개만 제공하고 있습니다. 따라서, 해당 제품에 대해 리뷰가 2000개 이상이더라도 소비자가 볼 수 있는 리뷰는 2000개로 한정되게 됩니다. 위 사진처럼 전체 리뷰 수가 44,477개이더라도 모든 리뷰를 다 확인할 수는 없다는 것입니다. 스크래핑 과정에서도 이러한 제약이 적용되어 마찬가지로 한 제품 당 2000개의 리뷰만 가져올 수 있게 됩니다. 아마 무분별한 웹크롤링으로 트래픽이 과부화되는 것을 막기 위해서 이러한 제약을 두는 것이 아닐까 생각됩니다. 저도 본격적으로 데이터 수집을 진행하기 전에는 이 부분에 대해 잘 인지하지 못했던 터라 계속 에러가 발생하는 것 때문에 고생하였습니다. 중간 중간 time.sleep()을 랜덤으로 주어가며 수정을 해보기도 했지만 해결이 되지 않다가 결국 수동으로 계속 페이지를 넘어가며 이와 같은 사실을 발견하였습니다. 여튼, 아쉽게도 모든 데이터를 가져오기는 어렵기 때문에 아래 코드에서도 페이지 이동을 100페이지로 제한을 두었습니다. 한 페이지에 20개의 리뷰가 있으니 100페이지면 우리가 가져올 수 있는 2000개 리뷰 제약에 딱 부합하게 됩니다.

def shopping_review_scraping(url, selectable_rate):
  wd = webdriver.Chrome('chromedriver', options = chrome_options)
  wd.implicitly_wait(3)

  # 해당 url로 이동
  wd.get(url)
  time.sleep(random.uniform(1, 2))

  total_review_df = pd.DataFrame()

  selectable_rate = [7-i for i in selectable_rate]
  for rating in selectable_rate:
    rating_xpath = f'//*[@id="section_review"]/div[2]/div[2]/ul/li[{rating}]'
    rating_bt = wd.find_element(By.XPATH, rating_xpath)
    rating_bt.click()
    print('\n****{}점 리뷰페이지로 이동****'.format(7-rating))
    time.sleep(random.uniform(1, 2))

    page_no = 0

    print("[스크래핑 시작]")
    while page_no <= 100:
      try:
        for num in range(1, 12):
          next_xpath = f'//*[@id="section_review"]/div[3]/a[{num}]'
          review_bt = wd.find_element(By.XPATH, next_xpath)
          review_bt.click()
          time.sleep(random.uniform(1, 2))
          page_no += 1
          print('[{}페이지로 이동]'.format(page_no)) #해당 페이지의 리뷰 스크래핑이 완료되면 다음 페이지로 이동
          review_df = page_review_scrap(wd)
          total_review_df = pd.concat([total_review_df,review_df], ignore_index=True)

        while page_no <= 100:
          page_no += 1
          for num in range(3, 13):
            next_xpath = f'//*[@id="section_review"]/div[3]/a[{num}]'
            review_bt = wd.find_element(By.XPATH, next_xpath)
            review_bt.click()
            time.sleep(random.uniform(1, 2))
            page_no += 1
            print('[{}페이지로 이동]'.format(page_no))
            review_df = page_review_scrap(wd)
            total_review_df = pd.concat([total_review_df,review_df], ignore_index=True)

      except ElementNotInteractableException as ex:
        print(ex)
        print("[모든 스크래핑이 완료되었습니다.]")
        break

      except NoSuchElementException as ex:
        review_df = page_review_scrap(wd)
        total_review_df = pd.concat([total_review_df,review_df], ignore_index=True)
        print("[모든 스크래핑이 완료되었습니다.]")
        break
  
  return total_review_df

이상으로 STEP2 리뷰 데이터를 수집하기 위한 사전 준비를 마치도록 하겠습니다. 다음 스텝에서는 지금까지 준비한 함수를 활용해서 4개 브랜드의 8개 제품에 대한 스크래핑을 본격적으로 진행하도록 하겠습니다. 

 

Warning) 아래 코드들은 버전이 바뀌어 정상적으로 작동되지 않을 수도 있습니다. 따로 버전을 명시하지 않았기 때문에 에러가 발생되면 조금씩 수정해 나가면서 사용하시길 바랍니다. 또한, 형편없는 저의 실력으로 코드들이 다소 비효율적일 수 있음을 미리 말씀드립니다. 우연히 이 글을 보게 되신 분들은 참고해주시기 바랍니다. 


STEP 1. 프로젝트 계획

(1) 분석 목적과 목표

평소에 텍스트 분석에 관심이 많아 유튜브 강의나 책을 통해 조금씩 공부를 하고 있습니다. 대학에서는 경영학을 전공하고 있고 특히 서비스, 마케팅과 관련된 수업을 많이 들었던 터라 지금 공부하고 있는 데이터 사이언스가 서비스 기획이나 마케팅에서 어떻게 활용될 수 있을지 항상 고민을 해왔습니다.

텍스트 마이닝(Text Mining) 또는 텍스트 분석(Text Analysis)이 경영과 접점을 이룰 수 있는 부분은 고객들의 데이터를 활용하는 것이지 않을까 생각합니다. 특히, 고객 데이터 중에서 회사 내부 DB에 저장되는 정형화된 데이터(구매 내역, 사이트 활동 시간 등) 외에 소셜이나 웹사이트에서 얻을 수 있는 비정형 텍스트 데이터에 대한 관심이 날로 커져가는 것 같습니다. 저 또한 고객들이 인터넷 상에 남기는 텍스트 데이터에서 어떻게 하면 인사이트를 발견할 수 있을지 많은 관심을 가지고 있습니다.

따라서, 이번 토이 프로젝트에서는 고객 리뷰 데이터를 가지고 텍스트 분석을 진행하여 상품 기획이나 마케팅 전략 수립에 대한 간단한 제언을 하는 연습을 해보았습니다. 이후 설명 과정에서도 말씀 드리겠지만 분석 결과가 그리 유의미하지는 않으며 상식적인 선에서도 충분히 알 수 있는 결과가 도출되었습니다. 때문에 프로젝트 결과물에 대한 기대는 하기 어렵다고 말씀드릴 수 있습니다. 다만, 빈도 분석과 LDA 토픽 모델링을 적용해보았다는 점, 그리고 한글 텍스트 전처리 과정을 직접 경험해보았다는 점에서 개인적인 의의를 두고 있습니다. 

이번에 활용한 텍스트 데이터는 냉동 만두 구매 고객들의 쇼핑몰 리뷰 데이터입니다. 냉동 만두 리뷰를 선택한 것은 별다른 이유는 없고 이 프로젝트를 기획하던 당시 풀무원 만두를 먹고 있었기 때문입니다. 물론 비비고, 풀무원 브랜드의 냉동 만두 사례가 학교 수업 시간에 포지셔닝, 브랜딩 파트에서 많이 활용되는 것을 익히 알고 있기 때문에 이러한 도메인 지식을 활용하면 좋은 분석 결과가 나오지 않을까 하는 기대가 있기도 하였습니다. 

결론적으로 이번 토이 프로젝트의 목표은 냉동 만두 구매 고객들의 리뷰 데이터를 분석하여 쇼핑몰에서 만두를 구매하여 소비하는 고객들의 행동과 상황을 확인하는 것입니다. 그리고 이를 활용해 향후 제품 개발과 마케팅 전략에 어떻게 활용할 수 있을지 살펴보는 것입니다.


(2) 데이터 수집 계획

국내에 정말 많은 온라인 쇼핑몰이 있긴 하지만 데이터 수집을 용이하게 하기 위해서는 몇 개의 쇼핑몰로 제한할 필요가 있습니다. 그리고 충분한 sample 수를 얻기 위해 가능한 큰 규모의 온라인몰에서 데이터를 수집하는 것이 좋습니다. 국내 대형 온라인몰 중에는 쿠팡, SSG, 롯데몰 등 대형 유통회사들이 꽤 많지만 저는 네이버쇼핑에서 리뷰 데이터를 수집하였습니다.  한 쇼핑몰의 데이터만 가져오기보다는 여러 쇼핑몰의 데이터를 수집하는 것이 더 객관적인 sampling이 될 수 있을 거라 생각되었기 때문입니다. 하지만, 막상 데이터를 수집한 결과 몇몇 온라인 몰에 집중되어 있어 네이버 쇼핑 데이터를 통해 수집한 의미가 퇴색되긴 하였습니다.  이에 대해서는 추후에 다시 말씀드리도록 하겠습니다.

텍스트 분석을 위해 어느정도의 데이터가 있으면 좋은 지에 대해서는 기준은 없습니다. 많은 데이터가 있으면 그만큼 좋은 결과를 얻을 수 있긴 하지만 수집과 전처리 과정에서 그만큼 많은 시간이 소요됩니다. 사실 제가 많이 고민되었던 부분은 얼만큼의 sample을 수집할지였습니다. 일반적으로 토픽 모델링에 많이 활용되는 뉴스 텍스트는 다양한 토픽들과 다채로운 단어들이 사용되기에 분석 결과의 객관성(?)을 확보하기에 유리합니다. 하지만 리뷰 데이터의 경우 그 길이가 매우 짧고 내용이 한정되어 있어 적은 데이터 sample로는 편향된 결과가 도출되지 않을까 하는 걱정이 되었습니다. 가령, 리뷰 데이터에는 '정말 좋습니다^^', '잘 쓸게요~' 등 단순한 표현들이 많고 리뷰들간 중복되는 것들도 정말 많습니다. 그래서 충분히 많은 데이터를 수집하고자 하는 욕심이 있었습니다. 실제로 리뷰에 대해 토픽 모델링을 시행한 몇몇 사례들을 살펴보니 보통 10만개 이상의 데이터를 수집하는 경우가 많았습니다. 그러나 너무 많은 데이터를 수집하기에는 중간 전처리에 대한 부담이 있어 2만개 데이터를 수집하는 것으로 타협하였습니다.


(3) 분석 방법 선정

3-1) 빈도 분석을 통한 WordCloud 생성

전처리 과정을 거친 텍스트 데이터에 대해서 단어별 빈도를 확인하고 WordCloud를 생성하도록 하겠습니다. WordCloud를 통해 전체 리뷰 데이터에서 많이 언급된 단어들을 확인하고 냉동 만두 구매 과정에서 생기는 다양한 생각과 경험들을 한눈에 살펴보도록 하겠습니다.

3-2) LDA 토픽 모델링으로 주요 토픽과 핵심 단어 추출

텍스트 데이터에 LDA 토픽 모델링을 적용하여 전체 리뷰 텍스트의 주요 토픽들을 도출하겠습니다. 그리고 각 토픽별 핵심 단어들을 추출하여 앞서 WordCloud를 통해 얻어낸 인사이트를 조금 더 구체화하여 보겠습니다. WordCloud도 마찬가지이지만 토픽 모델링의 경우 분석된 결과에 대해 추가적인 해석의 과정이 필요합니다. 이는 분석하는 사람의 몫이며 도메인 지식과 개인의 분석 역량에 따라 다양하고 주관적인 해석 결과가 나올 수 있습니다. 불행 중 다행인지는 모르겠지만 이번에 도출된 결과물의 경우에는 도메인 지식 없이도 누구나 충분히 해석이 가능한 것으로 보입니다. 


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

+ Recent posts