working_helen

[데이터 수집] 마켓컬리 리뷰 데이터 크롤링 본문

deep daiv./NLP project

[데이터 수집] 마켓컬리 리뷰 데이터 크롤링

HaeWon_Seo 2024. 8. 29. 12:03

데이터 수집을 위해 마켓컬리 리뷰 데이터의 특성을 분석하고 리뷰를 크롤링한 과정을 정리해본다. 

 

 

1. 마켓컬리 리뷰 데이터

2. 크롤링 과정

 


1. 마켓컬리 리뷰 데이터 

 

1) 마켓컬리의 특성

 

마켓컬리를 분석 대상으로 선택한 이유 

 

① 마켓컬리 사용자들의 특성 

  • 상품위원회를 통한 고품질 상품 선별, 업계 최초로 신선제품 새벽배송 등 품질이 좋고 신선한 상품을 제공하기 위한 시스템을 구축 
  • 유명한 맛집의 시그니처 메뉴나 유명 셰프와의 콜라보 상품을 독점적으로 판매 
  • 이로 인해 좋은 품질의 프리미엄 식품을 구매할 수 있다는 브랜드 이미지가 확립 

=> 음식에 관심이 많고, 음식에 진심인 사용자들이 많음, 먹잘알들의 성지 
=> 다른 쇼핑몰에 비해 상품의 맛과 품질에 대한 구체적인 리뷰를 얻을 수 있을 것으로 기대 

 

 

② 마켓컬리 리뷰의 특성 

  • 타 쇼핑몰과 달리 '별점'이 없음, 사용자들은 리뷰의 개수, 내용, 사진 등을 통해 상품을 파악
    이는 별점이 아닌 리뷰 내용 자체를 사용자들이 신중히 읽어볼 수 있도록 유도하기도 하지만, 긴 글을 꺼려하고 바쁜 사용자들에게는 상품에 대해 판단할 수 있는 정량적 수치의 부재로 인해 불편함을 줄 수 있음
  • 또한 상단에 '베스트' 리뷰를 제시하는 것을 제외하곤 전체 리뷰를 최신순으로만 정렬할 수 있는데, 이 또한 사용자가 본인에게 더 필요하고 유용한 정보를 탐색하는데 시간을 소요하게 만들어 불편함을 줄 수 있음

=> '별점'과 같은 상품의 구매여부를 결정하는데 도움을 줄 수치적 지표의 부재

      + 리뷰 정렬 방법의 부족으로 무엇이 더 유용한 리뷰인지 판단할 수 있는 지표가 명확하지 않음

=> 다른 쇼핑몰에 비해 사용자가 직접 하나하나 리뷰를 읽어보면서 판단해야하는 불편함이 큼 

 

 

 

 

 

2) 마켓컬리 카테고리 

 

- 식품 내 '과일·견과 · 쌀',  '수산 · 해산 · 건어물',  '생수 · 음료' 등 다양한 카테고리 존재
- 그 중 '국 · 반찬 · 메인요리' 영역의 '국 ·· 찌개' 카테고리를 수집 대상으로 선정

 

 

'국 ·  · 찌개' 카테고리를 선정한 이유

 

- 분석적인 측면 

  • 상품 수 및 상품별 리뷰 수가 크롤링으로 수집하기에 적절한 양
  • 채소, 해산물과 같은 기본 식재료의 경우 본인의 레시피를 소개하는 내용의 후기가 많음
    →  우리는 제품 자체의 특성을 설명하는 후기가 더 유용하기 때문에 사용자가 바로 먹거나 간편하게 조리하여 사용하는 형태의 식품을 분석하는 것이 더 적절하다고 판단
    →  '국 · 탕 · 찌개' 의 경우 이미 조리된 국물에 약간의 재료만 추가하여 바로 섭취하기 때문에 리뷰 내용에서 레시피를 소개하는 내용보단 상품 자체에 대한 사람들의 평가가 대부분임 (맛, 유통기한, 포장상태 등등) 
  • 하나의 url에 여러 종류의 상품이 함께 존재하는 경우('맛 3종 중 선택'과 같은 경우) 전체 리뷰 수가 충분하더라도 각 종류별로 리뷰를 구분해야하기 때문에 리뷰 수가 감소할 수 있다는 문제 
    →  '국 · 탕 · 찌개' 의 경우 대부분 하나의 url에 하나의 상품으로만 구성되어 있어 분석에 편리함 

 

- 의미적인 측면

  • 물가가 점점 오르고 혼밥 문화가 확산되면서 외식보다 집에서 간편하게 조리하여 먹을 수 있는 간편식에 대한 수요가 지속적으로 늘고있음. 이에 따라 다양한 식품 회사에서 자체적으로 간편식을 개발하는 것은 물론, 유명 쉐프나 가게와 협업하여 식당을 가야만 먹을 수 있던 유명 맛집의 음식을 간편식으로 제공하는 사례도 증가함.
  • 특히 국물을 애용하는 한국인의 입맛에 따라 국물을 사용하는 간편식이 많이 출시되어 왔고, 2024년에는 냉동 국·탕·찌개 총매출 전년보다 23.4% 상승. 마켓컬리에서도  '국 · 탕 · 찌개' 의 '사미헌 갈비탕' 제품이 2022년 한 해동안 108만개 넘게 판매되며 가정간편식 카테고리 중 판매량 1위를 차지함. 
  • 이러한 식문화 흐름에 맞춰 최근 사람들이 많이 구매하고 있고 앞으로도 더 많은 유입자가 발생할 것으로 기대되는 간편식 종류의 상품을 분석 대상으로 선정함. 
  • 관련 자료 : https://www.foodbank.co.kr/news/articleView.html?idxno=65145
 

[창간 28주년]“외식비 겁난다”…고물가에 간편식 시장 성장 - 식품외식경제

외식 물가가 오르면서 식사비용 부담을 해소하고자 하는 소비자들의 간편식 구매가 늘고 있다. 외식비용 대비 비교적 저렴한 가격, 간단한 조리법, 새벽배송 등의 요인이 간편식 구매시장의 성

www.foodbank.co.kr

 

 

 

 

3) 크롤링 대상 

 

- 마켓컬리에선 카테고리마다 고유한 id가 포함된 url 사용 

  '국 ·  · 찌개' url : https://www.kurly.com/categories/911001

 

https://www.kurly.com/categories/911001

 

www.kurly.com

 

 

- 각 리뷰마다 '작성자 맴버스', '작성자', '상품 종류', '리뷰 내용', '작성 날짜', '도움돼요'로 구성

- 베스트 리뷰인 경우 '작성자 맴버스' 정보 앞에 '베스트' 태그를 붙히고 있으며 리뷰 가장 상단에 위치함 

- '도움돼요' 수는 해당 후기가 유용하다고 투표한 사용자 수  

 

- 마켓컬리에는 별점이 존재하지 않음

- 리뷰 목록에서 페이지 번호를 사용하지 않고 화살표 버튼만 사용 

 

 

 

 

 

 

 

 

 

2. 크롤링 과정 

1단계 : 특정 카테고리의 모든 상품 URL 수집하기

 

- CSS seletor를 이용해 html의 각 요소에 접근
- find_element : 선택자에 해당되는 가장 첫 원소 하나만 반환
- find_elements : 선택자에 해당되는 모든 요소 리스트로 반환

 

 

① 카테고리 내 전체 상품 개수 추출

 

※ re.search(찾을 문자열 str1, 대상 문자열 str2)
   - str2에서 str1과 그 위치를 찾아 저장

   - str2에 str1이 존재하면 re.Match 객체로 반환, 없으면 None으로 반환

#re.search 예시
source = "Total: 1234 items"
total = re.search("\d+", source)

if total:
    print(total.group())  #'1234', 찾은 문자열
    print(total.start())  # 7, 문자열의 시작 위치
    print(total.end())    # 11, 문자열의 끝 위치
    print(total.span())   # (7, 11), 문자열의 범위

 

def get_total_num(driver):
    time.sleep(0.2)
    
    #"총 n건"에 해당되는 html element = div.css-crqql1.eudxpx33
    source = driver.find_element(By.CSS_SELECTOR, 'div.css-crqql1.eudxpx33').text.replace(",", "")
    
    #정규표현식으로 "총 n건"에서 숫자 n만 추출하기
    total = re.search("\d+", source)
    
    if total:
        #찾은 문자열을 정수로 반환하여 출력
        return int(total.group())
    else: return None

 

 

② 상품 url list에 저장하기 

def get_product_urls(driver):
    time.sleep(0.5)
    
    #각 상품의 url에 해당되는 html element = a.css-8bebpy.e1c07x488
    sources = driver.find_elements(By.CSS_SELECTOR, 'a.css-8bebpy.e1c07x488')
    
    #현재 source는 <a> 요소 (html 태그)
    #각 <a>에서 href 속성의 값(url 값)을 추출
    urls = [source.get_attribute('href') for source in sources]
    return urls

 

 

 

③ 특정 카테고리에 대하여 전체 상품 url list 생성하기  

 

※ list.extend(iterable 객체)

   - 기존 리스트에 다른 리스트나 반복 가능한(iterable) 객체의 요소들을 하나씩 추가하여 확장

   - 기존 리스트에 다른 객체에 있는 모든 원소를 한꺼번에 추가 가능

chrome_options = Options()
driver = webdriver.Chrome(options=chrome_options)

item_per_page = 96
category_id = 911001 # '국, 탕, 찌개' 카테고리의 id
category_url = f"https://www.kurly.com/categories/{category_id}"

driver.get(category_url)
html = driver.page_source

# 전체 상품 개수 확인
num_items = get_total_num(driver)
num_pages = math.ceil(num_items / item_per_page)

#페이지 1에 있는 모든 상품 url 저장하기
product_urls = []
product_urls.extend(get_product_urls(driver))

#페이지 2부터 끝까지 모든 상품 url 저장하기
for i in range(2, num_pages + 1):
    driver.get(f"{category_url}/?page={i}")
    product_urls.extend(get_product_urls(driver))

 

 

 

 

 

 

2단계 : 상품별 리뷰 개수 확인하기

- 1단계에서 생성한 전체 상품 url list를 사용하여 각 상품마다 url로 접속해 리뷰 수를 크롤링 

def get_review_info(product_url):    
    driver.get(product_url)
    ID = int(product_url.split("/")[-1])
    name = driver.find_element(By.CSS_SELECTOR, 'h1.css-13lg2xu.ezpe9l11')
    num = driver.find_elements(By.CSS_SELECTOR, 'span.count')
    
    if num:
        return (ID, name.text, int(re.findall(r'\d+', num[0].text.replace(',', ''))[0]))
    else:
        return (ID, name.text, 0)

 

- 리뷰 수가 가장 많은 상품은 약 17만개이고, 두 번째로 많은 상품은 약 5만개 

- 287개의 상품 중 리뷰 수가 10000개 이하인 상품은 247개, 100개 이하인 상품은 18개 

 

- 본 프로젝트에선 요약 모델을 사용해야하기 때문에 리뷰 수가 너무 적어 분석이 어려운 리뷰 수 100개 이하의 상품 18개는 분석 대상에서 제외 

- 리뷰 수가 17만개인 상품의 경우 17만개의 리뷰를 모두 크롤링하는데 시간이 너무 소요되고, 오랜기간 판매된 제품이다보니 예전 리뷰의 경우 상품의 현재 특성을 잘 반영하지 못한다고 판단하여 두 번째로 많은 리뷰 수인 50291개까지만 수집하는 것으로 결정 

 

최종 분석 대상 =  '국 ·  · 찌개' 카테고리 내 리뷰 수가 100개가 넘는 269개의 상품 

 

 

 

 

 

 

3단계 : 각 상품의 리뷰 수집하기 

- 각 리뷰마다 [리뷰 내용, 상품 종류, 작성 날짜, '도움돼요' 수] 정보를 크롤링 

- 각 상품 url 접속 → 현재 페이지의 리뷰 10개 크롤링 → 다음 페이지로 넘어가는 버튼 클릭

# '도움돼요' 수를 추출하는 함수
def extract_help_num():
    help_num_list = driver.find_elements(By.CSS_SELECTOR, 'button.css-g3a39p.e198bwfo1 > span:nth-child(2)')
    help_num = []

    for num in help_num_list :
        match = re.search(r'\d+', num.text)
    
        if match:
            help_num.append(int(match.group()))
        else:
            help_num.append(0)   
    return help_num



# id에 해당하는 상품의 리뷰 정보를 크롤링하는 함수
def get_reviews(product_id):
    df = pd.DataFrame(columns=['product_id', 'product_type', 'date', 'help_num', 'review'])
    product_url = 'https://www.kurly.com/goods/'+ str(product_id)
    
    driver.get(product_url)
    clicks = 0
    
    try:
        while True:
        	# 리뷰 내용
            review = list(map(lambda x : x.text, driver.find_elements(By.CSS_SELECTOR, 'p.css-y49dcn.e36z05c13')))
            # 상품 종류
            product_type = list(map(lambda x : x.text.split(']')[-1].strip(), driver.find_elements(By.CSS_SELECTOR, 'h3.css-11q4ylb.e36z05c12')))
            # 작성 날짜
            date = list(map(lambda x : x.text, driver.find_elements(By.CSS_SELECTOR, 'span.css-14kcwq8.e36z05c0')))
            # '도움돼요' 수
            help_num = extract_help_num()
                
            
            df = df.append(pd.DataFrame({'product_id' : [product_id] * len(date),
                                        'product_type': product_type,
                                        'date' : date,
                                        'help_num' : help_num,
                                        'review' : review}))

            # 다음 페이지로 넘어가는 버튼 클릭하기 
            button = driver.find_elements(By.CSS_SELECTOR, 'button.css-1orps7k.ebs5rpx0')[0]
            if not button.get_attribute('disabled'):
            	# 다음 페이지가 있으면 넘어가고
                button.click()
                clicks+=1
            else:
            	# 다음 페이지가 없으면 종료하기
                break
            time.sleep(1.5)
        
        return df

   # 예외 발생 시 어떤 id의 상품이 몇 페이지까지 수집되었는지 출력
    except Exception as e:
        print("e :", e, "clicks :", clicks, 'product_id :', product_id)
        return df

 

 

- 2025-08-25를 기준으로 해당 날짜를 포함하여 이전 시점까지 작성된 리뷰만 남김 

- url을 올바르게 사용했음에도 해당 페이지에 접근할 수 없었던 2가지 상품은 제외 

# 기준 날짜 설정
threshold_date = '2024.08.25'
threshold_date = datetime.strptime(threshold_date, '%Y.%m.%d')

review_df['date'] = pd.to_datetime(review_df['date'], format='%Y.%m.%d')
review_df = review_df[review_df['date'] <= threshold_date]

review_df = review_df.reset_index(drop=True)
review_df

 

 

 최종 dataframe = '  · 탕 · 찌개' 카테고리 내 267개 상품의 리뷰 

 

 

 

 

 

 

 

 

Reference

https://www.openads.co.kr/content/contentDetail?contsId=10792
https://mygrowthlog.tistory.com/2
https://brunch.co.kr/@rainofflowers/29
https://www.foodbank.co.kr/news/articleView.html?idxno=65145