본문 바로가기
[업무 지식]/Crawling

[제품 리뷰 언어 분석]

by 에디터 윤슬 2025. 3. 12.

라이브러리 호출

from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager
import time
import pandas as pd
import numpy as np
from datetime import datetime, timedelta, timezone
import os
import re

import warnings # 경고창 무시
warnings.filterwarnings('ignore')

 

리뷰 데이터 추출

def get_product_reviews(url, max_pages=5):
    # ChromeDriver 경로 설정
    chrome_driver_path = 'C:/Users/...'
    service = ChromeService(chrome_driver_path)    
    
    # WebDriver 초기화
    options = webdriver.ChromeOptions()
    wd = webdriver.Chrome(service=service, options=options)

    # 빈 리스트 생성하기
    writer_list = []
    review_list = []
    date_list = []
    option_list = []

    try:
        wd.get(url)
        wait = WebDriverWait(wd, 10)  # 명시적 대기 설정 (최대 10초)

        # 첫 페이지 데이터 수집
        collect_page_data(wd, wait, writer_list, review_list, date_list, option_list)
        
        # 페이지 2부터 max_pages까지 순회
        for page_no in range(2, max_pages + 1):
            try:
                # # 페이지 버튼 찾기 및 클릭
                # page_link = wait.until(
                #     EC.element_to_be_clickable(
                #         (By.XPATH, f"//ul[@class='pagination']/li/a[text()='{page_no}']")
                #     )
                # )
                # page_link.click()
                
                # JavaScript 함수를 직접 실행하는 방식으로 변경
                js_code = f"SITE_SHOP_DETAIL.changeContentPCTab('review',{page_no},1,1,'N',0);"
                wd.execute_script(js_code)
                
                # 페이지 로딩 대기
                time.sleep(2)
                
                # 현재 페이지 데이터 수집
                collect_page_data(wd, wait, writer_list, review_list, date_list, option_list)
                
            except Exception as e:
                print(f"Error on page {page_no}: {e}")
                break

    finally:
        wd.quit()

    # Pandas DataFrame 생성
    product_review_df = pd.DataFrame({
        "Writer": writer_list,
        "Review": review_list,
        "Date": date_list,
        "Option": option_list
    })

    return product_review_df

def collect_page_data(wd, wait, writer_list, review_list, date_list, option_list):
    """현재 페이지의 데이터를 수집하는 함수"""
    # 페이지 로딩 대기
    review_items = wait.until(EC.presence_of_all_elements_located((By.CLASS_NAME, 'tabled.full-width')))
    
    for item in review_items:
        # 1. 제품 옵션 추출
        try:
            option = item.find_element(By.CLASS_NAME, 'prod_option').text
            option_list.append(option)
        except:
            option_list.append('')

        # 2. 리뷰 내용 추출
        try:
            # 먼저 tw-whitespace-pre-line 클래스를 찾아보고
            try:
                review_text_element = item.find_element(By.CLASS_NAME, 'tw-whitespace-pre-line')
                review_text = review_text_element.text.strip()
            # 없으면 XPath로 span 내부 텍스트 추출
            except:
                review_text_element = item.find_element(By.XPATH, ".//span[contains(@class, '_review_body')]/span[@class='tw-whitespace-pre-line'] | .//span[contains(@class, '_review_body')]")
                # dummy div 이후의 텍스트만 추출
                dummy = review_text_element.find_element(By.CLASS_NAME, "dummy._dummy") if review_text_element else None
                if dummy:
                    review_text = review_text_element.text.replace(dummy.text, '').strip()
                else:
                    review_text = review_text_element.text.strip()
            
            review_list.append(review_text)
        except Exception as e:
            print(f"Review extraction error: {e}")
            review_list.append('')

        
        # 3. 사용자 ID 추출
        try:
            user_info = item.find_element(By.XPATH, ".//div[@class='table-cell vertical-top width-5 text-13 use_summary max-lg:tw-block']")
            user_divs = user_info.find_elements(By.TAG_NAME, 'div')
            if len(user_divs) > 0:
                writer_list.append(user_divs[0].text)
            else:
                writer_list.append('')
        except Exception as e:
            print(f"Writer extraction error: {e}")
            writer_list.append('')

        # 4. 날짜 추출
        try:
            if len(user_divs) > 1:
                date_list.append(user_divs[1].text)
            else:
                date_list.append('')
        except Exception as e:
            print(f"Date extraction error: {e}")
            date_list.append('')
    
    print(f"Collected {len(review_items)} reviews from current page")

# 데이터 추출
url = "https://a-odd.co.kr/356/?idx=444#prod_detail_review"
reviews_df = get_product_reviews(url, max_pages=5)
reviews_df

 

날짜 변환

# 날짜/시간 변환 (UTC 기준)
df["Date"] = pd.to_datetime(df["Date"])

# 새로운 컬럼 추가: 게시 날짜와 시간
df["publish_date"] = df["Date"].dt.date
df["publish_hour"] = df["Date"].dt.hour
df['publish_day'] = pd.to_datetime(df['publish_date']).dt.day_name()

print("날짜/시간 변환 후 데이터프레임:")
print(df[["publish_date", "publish_hour"]].head())

리뷰 데이터 리스트화

comments = df['Review'].to_list()
comments[8]

'자석으로 붙는 형식이라 깔끔하고 색상 무게 다 맘에 듭니다.'

한글 데이터 전처리

import re

def text_cleaning(text):
    #이메일 주소 제거
    text = re.sub(r'[a-zA-Z0-9._%+-]*@*[a-zA-Z0-9-]+\.(?:com|co\.kr|net)', '', text)

    #날짜 형식 제거 (YYYY.MM.DD)
    text = re.sub(r'\d{4}\.\d{2}\.\d{2}', '', text)

    #대괄호와 내용 제거
    text = re.sub(r'\[.*?\]', '', text)

    #중괄호와 내용 제거
    text = re.sub(r'\{.*?\}', '', text)

    #소괄호와 내용 제거
    text = re.sub(r'\(.*?\)', '', text)

    #특수문자 제거
    # 예: #, $, &, *, ^, @, !, ? 등
    text = re.sub(r'[^\w\s\.,]', '', text)

    #여러 개의 공백을 하나로 변경
    text = re.sub(r'\s+', ' ', text)

    #앞뒤 공백 제거
    text = text.strip()

    return text
    
comments_cleaned = [text_cleaning(doc) for doc in comments]
['아주 좋아요 만족합니다',
 '예쁘긴해요 문제는 가죽이라 기스가 잘나고 어디에 쿡 찍히면 잘 남네요. 그리고 흰색쪽이 밖이라 만약 책상에 먼지같은게 있으면 붙긴해요.ㅜ',
 '고급지고 색이 너무 잘나왔어요 강추',
 '딸래미가 좋아 합니다.',
 '색감 이쁘고 마음에 듭니다',
 '심플하고 좋습니다 색상 배색이 고급지고 슬림해서 더 좋네여',
 '선물했는데 예쁘다고하네요 잘쓸게여',
 '다른 케이스에 비해 확실히 고급지고, 사용성도 좋네요',
 '자석으로 붙는 형식이라 깔끔하고 색상 무게 다 맘에 듭니다.',
 '디자인 예쁘고 고급스럽긴한대 자력이 생각보다 너무 약해서 조심히 들고 다녀야 할 거 같아요 ㅜ',
 '딸이 좋다고 합니다.',
 '디자인 깔끔하고 색감이 예뻐서 좋아요',
 '좋아요.........',
 '질리지도 않고 매우 만족하고 있습니다',
 '예빠요 색감 조화며 마감상태며',
 '비싼만큼 퀄 좋아요 다만 열리는 방향이 반대였으면.. 좀 직관성이 떨어져요',
 '퀄리티 좋아요 굿굿',
 '다른 곳에 비해 색감 매우 뛰어납니다. 가격 비싼거 빼고 너무 좋아요.',
 '파스텔톤에 배색이라 색은 이쁜데 리뷰에서 종종 본 것 처럼 자력이 약해요 붙여놓고 보려고 위치를 바꾸는데 떨어지더라고요 케이스 자체가 보호하고 편하게 쓸려고 한건데 너무 아쉬워요',
 '색도 이쁘고 생각보다 때도 안타고 좋네요',
 '너무 고급스러워용 만족합니당',
 '기대이상의 퀄리티와 질',
 '빠른배송감사합니다1',
 '예쁘긴하나 재질이 너무 약한듯합니다 산지 하루만에 겉에 스크래치며 파인 자국이 많이 생기네요',
 '재질좋고 편하고 가벼워요',
 '딸아이 아이패드 케이스 폭풍검색하다 겟함 넘나 이뻐용',
 '색깔도 이쁘고 외관적인 부분은 정말 좋아요. 안쪽스웨이드부분이 밝은색인데 거치하면 바닥에 닿게되어 오염 걱정이 좀 되네요ㅠ 자력도 좀만 더 쎄면 좋울것같아요. 팬슬케이스 끼우고쓰는데 살짝 안닫히네요ㅠㅠ그래도 이뻐서 쭉 쓸거같엉ㅅ',
 '사진에 담기지 못 했지만 실물이 진짜 깡패입니다 케이스 추천 영상보고 들어와서 구매했는데 가격이 비싼만큼 유니크한 케이스 디자인과 촉감, 감성까지 한꺼번에 다 잡은 아이패드 케이스라고 볼 수 있습니다 솔직히 아이패드 케이스가 다 거기서 거기 아닌가 했는데 사용하고 보니 그런 생각이 사라졌네요 ㅎㅎ 너무 예쁩니다 잘 쓰겠습니다',
 '제품 자체의 질은 좋아요. 주관적이지만 생각한 것보다 무거웠고, 오배송되어서 예상한 날짜보다 훨씬 나중에 받았네요.',
 '고급스럽게 보이네요. 오염도 덜 될듯',
 '칼라 배합은 이쁘나 자력이 너무 약해서 아쉬움',
 '색 다 예뻐서 고민하다 때 탈까봐 네이비로 했는데 배색 너무 예쁘네요 고급스러워요',
 '폴리오 케이스 디자인이 다 비슷하고 마음에 들지 않았는데 오드 케이스는 너무 이뻐요 재질도 좋고 색상의 질이나 내외부 색상 조화가 너무 잘 되어 있어서 만족스럽습니다 거치시 기울기도 여러 단계가 가능해서 좋아요 그리고 포장부터 꼼꼼하고 선물 받는 느낌으로 정성스럽게 되어 있어서 너무 좋았습니다. 다음에는 다름 제품도 이용해보고 싶네요',
 '우선 디자인이 이쁘고 카메라 보호 잘 돼요. 고급져요.',
 '아빠 사드렸어용 색 예쁘다고 조아하심',
 '배송빠르고 색상도 이쁘네요 카메라 펜슬 잡아줘서 좋은거같습니다',
 '배송도 빠르고 선물로 보내줬는데 엄청 맘에들어해요',
 '깔끔하고 이뻐서 주변에서 어디에서 샀냐구 자꾸 물어봐요 좋습니다 ㅎㅎ',
 '색상도 너무 이쁘구 자력이 강해 잘 붙어서 좋습니다',
 '너무고급지고예뻐요다른색도사고싶네요. .',
 '고급스럽고 아주 예뻐요',
 '가볍고 간단한 게 필요했는데, 맘에 드네요. 색상도 좋고.',
 '너무이뻐요 고급스러워보여서 조아요',
 '색감도 예쁘고 자석도 셈',
 '깔끔하고 이쁩니다. 펜도 안빠지고 디자인 굳',
 '아직 착용전인데 디자인 이뻐서 맘에 들어요. 오래 쓸 수 있으면 좋겠습니다',
 '깔끔하고 이뻐서 대만족입니다 진짜 강추']

 

KSS, ekonly 형태소 명사 추출

import kss
from ekonlpy.tag import Mecab as EMecab

def analyze_text(text):
    # 1. 문장 분리
    # sentences = kss.split_sentences(text)
    
    mecab = EMecab()
    result = []
    for sentence in text:
        try:
            # 문장별 형태소 분석
            nouns = mecab.nouns(sentence)
            result.append(nouns)
        except ValueError as e:
            # 오류 정보 출력
            print(f"Error: {sentence}")
            print(f"오류 메시지: {e}")
            
            # 빈 리스트 추가하고 계속 진행
            result.append([])
            
    return result
    
analysis = analyze_text(comments_cleaned)

# 결과 출력
for i, sentence_morphs in enumerate(analysis):
    print(f"문장 {i+1}: {sentence_morphs}")
문장 1: ['만족']
문장 2: ['문제', '기스', '흰색', '밖', '만약', '책상', '먼지']
문장 3: ['고급', '색', '강추']
문장 4: ['딸래미']
문장 5: ['색감', '마음']
문장 6: ['색상', '배색', '고급', '슬림']
문장 7: ['선물']
문장 8: ['고급', '사용']
문장 9: ['자석', '형식', '깔끔', '색상', '무게', '맘']
문장 10: ['디자인', '고급', '자력', '생각']
문장 11: ['딸']
문장 12: ['디자인', '깔끔', '색감']
문장 13: []
문장 14: ['만족']
문장 15: ['색감', '조화', '마감', '상태']
문장 16: ['방향', '반대', '직관']
문장 17: ['굿', '굿']
문장 18: ['곳', '색감', '가격']
문장 19: ['파스텔', '톤', '배색', '색', '자력', '위치', '보호']
문장 20: ['색', '생각', '때']
문장 21: ['고급', '만족', '합', '당']
문장 22: ['기대', '질']
문장 23: ['배송', '감사']
문장 24: ['재질', '산지', '겉', '스크래치', '파인', '자국']
문장 25: ['재질']
문장 26: ['딸아이', '폭풍', '검색', '용']
문장 27: ['색깔', '외관', '안쪽', '색', '거치', '바닥', '오염', '걱정', '자력', '팬', 'ㅅ']
문장 28: ['실물', '깡패', '추천', '구매', '가격', '디자인', '촉감', '감성', '사용', '생각']
문장 29: ['제품', '질', '생각', '배송', '예상', '날짜', '나중']
문장 30: ['고급', '오염']
문장 31: ['칼라', '배합', '자력', '아쉬움']
문장 32: ['색', '고민', '때', '네이비', '배색', '고급']
문장 33: ['디자인', '비슷', '마음', '오드', '재질', '색상', '질', '외부', '색상', '조화', '만족', '가능', '포장', '선물', '느낌', '정성', '제품', '이용']
문장 34: ['디자인', '보호', '고급']
문장 35: ['아빠', '어용', '색', '조아']
문장 36: ['배송', '색상', '펜슬']
문장 37: ['배송', '선물', '맘']
문장 38: ['깔끔', '주변']
문장 39: ['색상', '자력']
문장 40: ['고급', '색']
문장 41: ['고급']
문장 42: ['간단', '필요', '맘', '색상']
문장 43: ['고급', '조아']
문장 44: ['색감', '자석', '셈']
문장 45: ['깔끔', '펜', '디자인', '굳']
문장 46: ['착용', '전', '디자인', '맘']
문장 47: ['깔끔', '만족', '강추']

의미 없는 단어 제거

stopwords = ['굿', '곳', '조아', '굳', '합', '당', '감사', '생각']
comments_nouns = [[noun for noun in nouns if noun not in stopwords] for nouns in analysis]
comments_nouns
[['만족'],
 ['문제', '기스', '흰색', '밖', '만약', '책상', '먼지'],
 ['고급', '색', '강추'],
 ['딸래미'],
 ['색감', '마음'],
 ['색상', '배색', '고급', '슬림'],
 ['선물'],
 ['고급', '사용'],
 ['자석', '형식', '깔끔', '색상', '무게', '맘'],
 ['디자인', '고급', '자력'],
 ['딸'],
 ['디자인', '깔끔', '색감'],
 [],
 ['만족'],
 ['색감', '조화', '마감', '상태'],
 ['방향', '반대', '직관'],
 [],
 ['색감', '가격'],
 ['파스텔', '톤', '배색', '색', '자력', '위치', '보호'],
 ['색', '때'],
 ['고급', '만족'],
 ['기대', '질'],
 ['배송'],
 ['재질', '산지', '겉', '스크래치', '파인', '자국'],
 ['재질'],
 ['딸아이', '폭풍', '검색', '용'],
 ['색깔', '외관', '안쪽', '색', '거치', '바닥', '오염', '걱정', '자력', '팬', 'ㅅ'],
 ['실물', '깡패', '추천', '구매', '가격', '디자인', '촉감', '감성', '사용'],
 ['제품', '질', '배송', '예상', '날짜', '나중'],
 ['고급', '오염'],
 ['칼라', '배합', '자력', '아쉬움'],
 ['색', '고민', '때', '네이비', '배색', '고급'],
 ['디자인',
  '비슷',
  '마음',
  '오드',
  '재질',
  '색상',
  '질',
  '외부',
  '색상',
  '조화',
  '만족',
  '가능',
  '포장',
  '선물',
  '느낌',
  '정성',
  '제품',
  '이용'],
 ['디자인', '보호', '고급'],
 ['아빠', '어용', '색'],
 ['배송', '색상', '펜슬'],
 ['배송', '선물', '맘'],
 ['깔끔', '주변'],
 ['색상', '자력'],
 ['고급', '색'],
 ['고급'],
 ['간단', '필요', '맘', '색상'],
 ['고급'],
 ['색감', '자석', '셈'],
 ['깔끔', '펜', '디자인'],
 ['착용', '전', '디자인', '맘'],
 ['깔끔', '만족', '강추']]

명사 리스트화

noun_list = [item for sublist in comments_nouns for item in sublist]
noun_list

['만족',
 '문제',
 '기스',
 '흰색',
 '밖',
 '만약',
 '책상',
 '먼지',
 '고급',
 '색',
 '강추',
 '딸래미',
 '색감',
 '마음',
 '색상',
 '배색',
 '고급',
 '슬림',
 '선물',
 '고급',
 '사용',
 '자석',
 '형식',
 '깔끔',
 '색상',
 '무게',
 '맘',
 '디자인',
 '고급',
 '자력',
 '딸',
 '디자인',
 '깔끔',
 '색감',
 '만족',
 '색감',
 '조화',
 '마감',
 '상태',
 '방향',
 '반대',
 '직관',
 '색감',
 '가격',
 '파스텔',
 '톤',
 '배색',
 '색',
 '자력',
 '위치',
 '보호',
 '색',
 '때',
 '고급',
 '만족',
 '기대',
 '질',
 '배송',
 '재질',
 '산지',
 '겉',
 '스크래치',
 '파인',
 '자국',
 '재질',
 '딸아이',
 '폭풍',
 '검색',
 '용',
 '색깔',
 '외관',
 '안쪽',
 '색',
 '거치',
 '바닥',
 '오염',
 '걱정',
 '자력',
 '팬',
 'ㅅ',
 '실물',
 '깡패',
 '추천',
 '구매',
 '가격',
 '디자인',
 '촉감',
 '감성',
 '사용',
 '제품',
 '질',
 '배송',
 '예상',
 '날짜',
 '나중',
 '고급',
 '오염',
 '칼라',
 '배합',
 '자력',
 '아쉬움',
 '색',
 '고민',
 '때',
 '네이비',
 '배색',
 '고급',
 '디자인',
 '비슷',
 '마음',
 '오드',
 '재질',
 '색상',
 '질',
 '외부',
 '색상',
 '조화',
 '만족',
 '가능',
 '포장',
 '선물',
 '느낌',
 '정성',
 '제품',
 '이용',
 '디자인',
 '보호',
 '고급',
 '아빠',
 '어용',
 '색',
 '배송',
 '색상',
 '펜슬',
 '배송',
 '선물',
 '맘',
 '깔끔',
 '주변',
 '색상',
 '자력',
 '고급',
 '색',
 '고급',
 '간단',
 '필요',
 '맘',
 '색상',
 '고급',
 '색감',
 '자석',
 '셈',
 '깔끔',
 '펜',
 '디자인',
 '착용',
 '전',
 '디자인',
 '맘',
 '깔끔',
 '만족',
 '강추']

 

빈도 분석

from collections import Counter

# 빈도 분석
noun_counter = Counter(noun_list)

# 상위 15개 단어 출력
print("【 상위 출현 명사 】")
for word, count in noun_counter.most_common(15):
    print(f"{word}: {count}회")
    
    
【 상위 출현 명사 】
고급: 11회
색: 7회
색상: 7회
디자인: 7회
만족: 5회
색감: 5회
깔끔: 5회
자력: 5회
맘: 4회
배송: 4회
배색: 3회
선물: 3회
질: 3회
재질: 3회
강추: 2회

 

유의어 병합

from collections import Counter

color_synonyms = {
    '색': '색',
    '색상': '색', 
    '색감': '색',
    '빛깔': '색',
    '배색': '색',
    '컬러': '색',
    '칼라': '색',
    '맘': '마음',
    '마음': '마음',
    '재질': '질',
    '질': '질'
    }

def normalize_color_terms(word):
    # 단일 단어(문자열)에 대해 처리
    for synonym, standard in color_synonyms.items():
        if word == synonym:
            return standard
    return word

# 리스트의 각 요소에 함수 적용
normalized_noun_list = [normalize_color_terms(word) for word in noun_list]

# 빈도 분석
noun_counter = Counter(normalized_noun_list)

# 상위 15개 단어 출력
print("【 상위 출현 명사 】")
for word, count in noun_counter.most_common(15):
    print(f"{word}: {count}회")
【 상위 출현 명사 】
색: 23회
고급: 11회
디자인: 7회
마음: 6회
질: 6회
만족: 5회
깔끔: 5회
자력: 5회
배송: 4회
선물: 3회
강추: 2회
사용: 2회
자석: 2회
조화: 2회
가격: 2회

 

상위 10개 단어 시각화

import matplotlib.pyplot as plt
import numpy as np
import matplotlib

# 한글 폰트 설정
matplotlib.rc('font', family='Malgun Gothic')  # Windows: 'Malgun Gothic', macOS: 'AppleGothic'
plt.rcParams['axes.unicode_minus'] = False  # 마이너스 기호 깨짐 방지

# 데이터 준비
top10 = noun_counter.most_common(10)
words = [item[0] for item in top10]
counts = [item[1] for item in top10]

# 1등 찾기 (가장 높은 값의 인덱스)
max_index = counts.index(max(counts))

# 색상 설정 (기본: 파란색 계열 그라데이션)
colors = plt.cm.Blues(np.linspace(0.5, 0.9, len(words)))

# 그래프 생성
plt.figure(figsize=(12, 6))
bars = plt.bar(words, counts, color=colors, width=0.6)

# 1등 막대 강조 (빨간색으로 변경)
bars[max_index].set_color('crimson')
bars[max_index].set_edgecolor('black')
bars[max_index].set_linewidth(1.5)

# 제목 및 레이블 설정
plt.title('아이패드 케이스 리뷰 주요 키워드 (상위 10개)', fontsize=15)
plt.xlabel('키워드', fontsize=12)
plt.ylabel('언급 횟수', fontsize=12)
plt.xticks(fontsize=11)
plt.grid(axis='y', linestyle='--', alpha=0.4)

# 값 표시 (1등은 글자 크기와 색상 강조)
for i, bar in enumerate(bars):
    height = bar.get_height()
    if i == max_index:  # 1등인 경우
        plt.text(bar.get_x() + bar.get_width()/2, height + 0.3, 
                f'{height}', ha='center', fontsize=13, 
                color='crimson', fontweight='bold')
    else:  # 나머지
        plt.text(bar.get_x() + bar.get_width()/2, height + 0.3, 
                f'{height}', ha='center', fontsize=10)

# 여백 조정
plt.tight_layout()

# 그래프 표시
plt.show()