데이터사이언스/추천시스템

맥주 추천시스템 구현 - 1. 데이터 크롤링

ghtis1798 2021. 2. 2. 11:34

🍺 리뷰 데이터 크롤링

💡 어떤 맥주를 수집할 것인가?

추천시스템 구현을 위한 리뷰 데이터를 먼저 수집하려고 합니다.

맥주 데이터를 크롤링 할 곳은 RateBeer라는 전 세계 맥주 리뷰 사이트입니다.

BeerAdvocate와 함께 가장 큰 맥주 리뷰 사이트로 유명합니다.

https://www.ratebeer.com

우선 필요한 라이브러리들을 가져오고, 수집할 맥주 목록을 정하도록 하겠습니다.

import pandas as pd
import numpy as np
import time

import re

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

수집할 맥주는 국내 편의점에서 구매 가능한 맥주로 한정하겠습니다.

# 수집할 맥주 목록
beer_list = ['kloud', 'fitz super clear', 'Asahi super dry', 'Tsingtao', 'Heineken',
 'Kirin ichiban', 'Sapporo Premium Beer / Draft Beer', 'stella artois', 'guinness braught',
 '1664 Blanc', 'pilsner urquell', 'San Miguel', 'OB premier pilsner', 'cass fresh',
 'stout', 'dry finish', 'max hite', 'hite extra cold', 'victoria bitter', 'BINTANG pilsner',
 'krombacher weizen', 'Miller Genuine Draft', 'Hoegaarden Cherry', 'TIGER REDLER',
 "Suntory The Premium Malt's", 'REEPER B Weissbier', 'PEEPER B IPA', 'TIGER BEER',
 'TSINGTAO WHEAT BEER', 'Erdinger Weissbier', 'Carlsberg', 'Budweiser ', 'Hoegarden',
 'YEBISU', 'Paulaner Hefe-weissbier', 'Desperados', 'Peroni Nastro Azzurro',
 'Edelweiss Snowfresh', 'Heineken Dark Lager', 'Kozel Dark 10', 'Guinness original',
 'Filite', 'SEOULITE ALE', 'JEJU WIT ALE', 'Stephans Brau Philsner', 'Stephans Brau Larger',
 'Stephans Bräu Hefe-Weizen Naturtrüb', 'Bali Hai Premium Larger', 'Apostel Brau',
 'Egger Zwickl', 'Egger Marzenbier', 'Holsten Premium Beer', 'Franzisaner Hefe-Weissbier',
 'Egger Radler Grapefruit', 'Barvaria Premium', 'Barvaria 8.6', 'Lapin Kulta IV A',
 'Grolsch Premium Larger', 'Gambrinus Original', 'XXXX Gold', 'Leffe Brown',
 'Lowenbrau Original', 'Asahi Super dray Black ', 'Harbin Beer', "beck's",
 'Hoegaarden Rosee', 'Platinum White Ale', 'Platinum Pale Ale', 'Ambar Especial Larger',
 'Schöfferhofer Grapefruit', 'Volfas Engelman Grünberger Hefeweizen', 'Berliner Kindl Pilsener ',
 'BURGE MEESTER', 'Red Rock', 'Erdinger Dunkel', 'Warsteiner Premium Verum', "Queen's Ale Blonde Type"]

 # 데이터프레임으로 저장
beer_list = pd.DataFrame(data=beer_list, columns=['검색이름'])

💡 수집 방법은?

Selenium을 사용하기 위해서는 미리 chormedriver를 설치해야 합니다.

https://chromedriver.chromium.org/downloads

해당 사이트에서 자신의 크롬 버전에 맞는 Chromedriver를 설치합니다.

저는 주피터노트북을 사용하고 있으므로, 주피터노트북과 같은 경로에 Chromedriver.exe 파일을 두겠습니다.

여기서 window_size를 지정해주는 이유는 반응형 웹의 경우 element들의 위치가 바뀌는 경우가 있기 때문입니다.

그럼 실행을 해보겠습니다.

# 데이터 프레임 생성
data = pd.DataFrame(data=[], columns=['맥주정보', '검색이름', '맥주이름'])

# chromedriver.exe 파일 경로 설정
chromedriver = 'chromedriver.exe'
# 크롤링 할 경로 설정
url = 'https://www.ratebeer.com/search?tab=beer'

# 셀레니움으로 웹브라우저를 오픈합니다.
driver = webdriver.Chrome(chromedriver)
driver.get(url)
driver.set_window_size(900, 900)
time.sleep(1)

자동화된 웹 브라우저가 잘 실행됩니다.

이제 F12 개발자 도구를 이용해 각 element들로부터 데이터를 수집하는 코드를 작성하겠습니다.

전체 코드를 먼저 제공한 뒤, 어떤 과정으로 셀레니움이 동작하는 지 살펴보겠습니다.

def crawl(driver, beer, data, k):
    # 데이터 프레임 생성
    data = pd.DataFrame(data=[], columns=['맥주정보', '검색이름', '맥주이름'])

    # url open
    print('url_open... {0} 맥주 데이터를 수집합니다..'.format(beer))
    driver = webdriver.Chrome(chromedriver)
    driver.get(url)
    driver.set_window_size(900, 900)

    # 1번 사진에 해당 : 맥주 검색
    time.sleep(2)
    element = driver.find_element_by_xpath('//*[@id="root"]/div[2]/header/div[2]/div[1]/div[2]/div/div/input')
    time.sleep(2)
    element.click()
    time.sleep(2)
    element.send_keys(beer)
    time.sleep(3)

    # 2번 사진에 해당 : 상품 선택
    driver.find_element_by_xpath('//*[@id="root"]/div[2]/header/div[2]/div[1]/div[2]/div/div[2]/a[1]/div/div[2]').click()

    # 3번 사진에 해당 : 상품 이름 수집
    time.sleep(3)
    beer_name = driver.find_element_by_css_selector('.MuiTypography-root.Text___StyledTypographyTypeless-bukSfn.pzIrn.text-500.colorized__WrappedComponent-hrwcZr.hwjOn.mt-3.MuiTypography-h4').text

    error_cnt = 0

    while 1:
        try :
            # 4번 사진에 해당 : 전체 리뷰 개수 수집
            time.sleep(3)
            string = driver.find_element_by_class_name('MuiTypography-root.Text___StyledTypographyTypeless-bukSfn.pzIrn.text-500.colorized__WrappedComponent-hrwcZr.hwjOn.MuiTypography-h6').text

            # ,가 포함되어 있는지에 대한 로직
            extract = re.compile('[0-9]*,*[0-9]+')
            str_num = extract.findall(string)
            str_num = str_num[0]

            print('성공... while문을 탈출합니다.')
            break
        except :
            print('오류 발생.. 다시 시작합니다.')

            error_cnt += 1

            if error_cnt == 5:
                print('연속된 오류로 다음 맥주로 넘어갑니다...')
                return

    if ',' in str_num:
        str_num = str_num.split(',')
        str_num = int(str_num[0]+str_num[1])
        num = str_num
    else:
        num = int(str_num)

    # 5번 사진에 해당 : Score breakdown 클릭
    time.sleep(3)
    element = driver.find_element_by_xpath('//*[@id="root"]/div[2]/div[2]/div/div/div/div[2]/div[4]/div/div[2]/div[1]/div[2]')
    time.sleep(3)
    # 해당 element로 이동하는 코드입니다. 반드시 적어주세요.
    driver.execute_script("arguments[0].click();", element)

    # 수집할 Page 수를 계산합니다.
    page_num = num // 15 + 1


    for i in range(page_num):
        print(i+1, '번째 페이지입니다.')

        # 6번 사진에 해당 : 전체 맥주 정보를 통째로 수집
        time.sleep(3)
        beer_info = driver.find_elements_by_css_selector('.px-4.fj-s.f-wrap')

        tmp = []

        # 수집한 것을 데이터프레임에 저장
        for i in range(len(beer_info)):
            tmp.append(beer_info[i].text)

        tmp = pd.DataFrame(data=tmp, columns=['맥주정보'])
        tmp['맥주이름'] = beer_name
        tmp['검색이름'] = beer
        data = pd.concat([data, tmp])

        # 다음 페이지로 넘어가기 : 7번 사진에 해당합니다.
        # div, span, title 태그 후 속성은 class 외에도 사용 가능
        try :
            element = driver.find_element_by_xpath('//button[@title="Next page"]/span[@class="MuiIconButton-label"]')
            time.sleep(3)
            driver.execute_script("arguments[0].click();", element)
        except:
            print('마지막 페이지입니다.')

    # 데이터가 중복 수집될 경우 리뷰 수 만큼만 Cut
    if num != len(data):
        data = data[:num]

    print('리뷰수 : ', num, '수집된 리뷰수 : ', len(data))

    # 데이터를 csv, excel 파일로 저장합니다.
    result = pd.merge(data, beer_list, on='검색이름', how='left')
    result.to_csv("beer_n_"+str(k)+".csv", encoding='utf-8')
    result.to_excel("beer_n_"+str(k)+".xlsx")

    driver.quit()

    return result

셀레니움으로 가져올 element들을 순서대로 확인하며 작동과정을 보겠습니다.

우선은 맥주 검색부분에 해당하는 element의 xpath를 가져옵니다.

이 과정은 F12를 누른 후, 해당 element에 해당하는 html 소스를 우클릭 한 뒤

Copy > Copy xpath를 누르면 그대로 경로를 가져올 수 있습니다.

# 1번 사진에 해당 : 맥주 검색
time.sleep(2)
element = driver.find_element_by_xpath('//*[@id="root"]/div[2]/header/div[2]/div[1]/div[2]/div/div/input')
time.sleep(2)
element.click()
time.sleep(2)
element.send_keys(beer)
time.sleep(3)

중간 중간 time.sleep(2)이 들어가는 이유는 셀레니움이 화면을 인식할 시간을 주기 위함입니다.

맥주에 이름을 입력하고 결과가 나오는 데 시간이 걸리기 때문이죠.

그리고 맥주 검색부분의 element에 맥주 이름을 선택한 뒤, 첫 번째 맥주를 클릭하겠습니다.

# 2번 사진에 해당 : 상품 선택
driver.find_element_by_xpath('//*[@id="root"]/div[2]/header/div[2]/div[1]/div[2]/div/div[2]/a[1]/div/div[2]').click()

다음 화면에서는 이름, 전체 리뷰 개수를 먼저 수집합니다.

    error_cnt = 0

    while 1:
        try :
            # 4번 사진에 해당 : 전체 리뷰 개수 수집
            time.sleep(3)
            string = driver.find_element_by_class_name('MuiTypography-root.Text___StyledTypographyTypeless-bukSfn.pzIrn.text-500.colorized__WrappedComponent-hrwcZr.hwjOn.MuiTypography-h6').text

            # ,가 포함되어 있는지에 대한 로직
            extract = re.compile('[0-9]*,*[0-9]+')
            str_num = extract.findall(string)
            str_num = str_num[0]

            print('성공... while문을 탈출합니다.')
            break
        except :
            print('오류 발생.. 다시 시작합니다.')

            error_cnt += 1

            if error_cnt == 5:
                print('연속된 오류로 다음 맥주로 넘어갑니다...')
                return

리뷰 개수를 수집하는 로직을 처리할 때는 콤마(,)가 있는 case가 있기 때문에 정규표현식으로 처리했습니다.

이후 str.split(',')함수를 사용하여 콤마(,)를 기준으로 분리한 뒤 다시 합쳤습니다.

전체 리뷰 개수를 수집하는 이유는 수집할 페이지 수를 계산하기 위해서입니다.

그런데 리뷰 개수가 없는 경우 오류가 발생하는 경우가 있습니다. 🤦‍♂️

따라서 While문을 으로 5회까지 시도한 뒤 또 오류가 발생하면 다음 맥주로 넘어가도록 구현했습니다.

이제 본격적으로 맥주 리뷰를 수집할 것인데, Score breakdown이라는 버튼을 눌러야 자세한 정보가 나옵니다.

    # 5번 사진에 해당 : Score breakdown 클릭
    time.sleep(3)
    element = driver.find_element_by_xpath('//*[@id="root"]/div[2]/div[2]/div/div/div/div[2]/div[4]/div/div[2]/div[1]/div[2]')
    time.sleep(3)
    # 해당 element로 이동하는 코드입니다. 반드시 적어주세요.
    driver.execute_script("arguments[0].click();", element)

    # 수집할 Page 수를 계산합니다.
    page_num = num // 15 + 1

Score breakdown 버튼 클릭 후 모습입니다.

어떤 element를 클릭 시 해당 위치로 이동하는 코드가 필요합니다.

코드 : driver.execute_script("arguments[0].click();", element)

셀레니움은 사람처럼 동작하기 때문에 해당 element로 이동한 뒤 클릭을 하던 입력을 하던 해야합니다.

여기서 6번에 해당하는 모든 정보를 한 번에 크롤링 하겠습니다.

그 이유는 닉네임 아래에 국적이 표시된 경우도 있고 안 된 경우도 있어 예외처리를 해주어야 합니다.

따라서 우선 몽땅 수집한 뒤 나중에 한 번에 전처리 해주도록 하겠습니다.

    for i in range(page_num):
        print(i+1, '번째 페이지입니다.')

        # 6번 사진에 해당 : 전체 맥주 정보를 통째로 수집
        time.sleep(3)
        beer_info = driver.find_elements_by_css_selector('.px-4.fj-s.f-wrap')

        tmp = []

        # 수집한 것을 데이터프레임에 저장
        for i in range(len(beer_info)):
            tmp.append(beer_info[i].text)

        # 한 페이지 동안 수집한 것을 기존 data 프레임에 합쳐줍니다.
        tmp = pd.DataFrame(data=tmp, columns=['맥주정보'])
        tmp['맥주이름'] = beer_name
        tmp['검색이름'] = beer
        data = pd.concat([data, tmp])

1 페이지를 다 수집했으니 다음 페이지로 넘어가겠습니다.

해당 element로 이동하기 위한 driver.execute_script("arguments[0].click();", element) 코드는 필수입니다.

# 다음 페이지로 넘어가기 : Next 버튼 클릭! 7번 사진에 해당합니다.

# div, span, title 태그 후 속성은 class 외에도 사용 가능
        try :
            element = driver.find_element_by_xpath('//button[@title="Next page"]/span[@class="MuiIconButton-label"]')
            time.sleep(3)
            driver.execute_script("arguments[0].click();", element)
        except:
            print('마지막 페이지입니다.')

💡 수집한 데이터 저장하기

    # 데이터가 중복 수집될 경우 리뷰 수 만큼만 Cut
    if num != len(data):
        data = data[:num]

    print('리뷰수 : ', num, '수집된 리뷰수 : ', len(data))

    # 데이터를 csv, excel 파일로 저장합니다.
    result = pd.merge(data, beer_list, on='검색이름', how='left')
    result.to_csv("beer_n_"+str(k)+".csv", encoding='utf-8')
    result.to_excel("beer_n_"+str(k)+".xlsx")

    driver.quit()

    return result

# 맥주 리스트 개수만큼 데이터 수집
for k in range(len(beer_list)):
    result = crawl(driver, beer_list['검색이름'].iloc[k], data, k)

리뷰 수보다 더 많이 수집된 경우 뒷 부분은 버렸습니다.

그리고 수집한 데이터를 csv, excel 파일로 저장한 뒤 드라이버도 종료시켰고

수집한 데이터를 result 데이터프레임에 담아 리턴했습니다.

결과적으로 맥주 리스트 개수만큼 beer_n_k.csv 파일이 만들어질 것입니다.

다음 포스팅에서는 이 데이터프레임들을 합쳐보도록 하겠습니다. ✨