본문 바로가기
생각정리

WIL(Weekly I Learned) , 크롤링(스크래핑)을 짤때 고려해야하는 예외처리

by 물고기고기 2021. 4. 7.

github.com/silano08/TIL-Today_I_Learnd-/tree/main/scrapinggithub.com/silano08/TIL-Today_I_Learnd-/tree/main/scraping

 

silano08/TIL-Today_I_Learnd-

Contribute to silano08/TIL-Today_I_Learnd- development by creating an account on GitHub.

github.com

이번에 진행한 프로젝트는 스타벅스 클론코딩이었다. 혼자서 ejs엔진을 만지며 되도않는 프론트를 잡고있던 CRUD시기를 지나 프론트(리액트)와 처음 협업하는 프로젝트였기에 더더욱 기억에 남는다. 이번프로젝트에서 배운것이 정말 많지만 그 중 크롤링(원래는 스크래핑이라고 해야한다고..)에서 오랜만에 고생을했기에 이번 회고록은 스크래핑을 통해 직접 해결한 애로사항들을 공유하고자한다.

 

그리고 이건 프로젝트 notion 및 깃허브링크

 

직접보는 코드 전문

더보기
import csv
from genericpath import exists
import re
import time

import requests
from bs4 import BeautifulSoup
from pymongo import MongoClient
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from webdriver_manager.chrome import ChromeDriverManager
# import pandas as pd

client = MongoClient('localhost', 27017)  # mongoDB는 27017 포트로 돌아갑니다.
db = client.starbucks    # 'menu'라는 이름의 db를 사용합니다. 'menu' db가 없다면 새로 만듭니다.

# 보니까 메뉴끼리 URL에 통일성이 없음.. 큰틀에서 한번 URL만 싹 모아오고.. 그리고

# 포함관계 메뉴(음료) - 카테고리(콜드브루) - 상세정보(나이트로 바닐라크림)
# 작업순서
# 음료(메인페이지) > 콜드브루 / 브루드커피 / 에스프레소 URL을 가져온다
# 라고하려했느,ㄴ데 div > gnb_sub_inner 태그가 너무 많아서 저게 안불러와짐

# 이렇게 나눈거 굳이 필요없을듯..
url = ["https://www.starbucks.co.kr/menu/drink_list.do?CATE_CD=product_cold_brew","https://www.starbucks.co.kr/menu/drink_list.do?CATE_CD=product_brood","https://www.starbucks.co.kr/menu/drink_list.do?CATE_CD=product_espresso"
,"https://www.starbucks.co.kr/menu/drink_list.do?CATE_CD=product_frappuccino","https://www.starbucks.co.kr/menu/drink_list.do?CATE_CD=product_blended","https://www.starbucks.co.kr/menu/drink_list.do?CATE_CD=product_fizzo",
"https://www.starbucks.co.kr/menu/drink_list.do?CATE_CD=product_tea","https://www.starbucks.co.kr/menu/drink_list.do?CATE_CD=product_etc","https://www.starbucks.co.kr/menu/drink_list.do?CATE_CD=product_juice"]
url_name = ["콜드브루","브루드커피","에스프레소","프라푸치노","블렌디드","스타벅스피지오","티","기타제조음료","스타벅스주스(병음료)"]

#########################

driver = webdriver.Chrome(ChromeDriverManager().install())
driver.get(url[1])

html = driver.page_source
soup = BeautifulSoup(html, 'html.parser')
drinks = soup.findAll("li", {"class": re.compile("menuDataSet")})

# 상세페이지의 url을 얻는법
detail_url = []
for drink in drinks:
    a_tag = drink.find("a")
    prod = "https://www.starbucks.co.kr/menu/drink_view.do?product_cd="+a_tag['prod']
    detail_url.append(prod)

# 각 url당 상세정보를 for문을 돌리기
for url in detail_url:
    try:
            driver = webdriver.Chrome(ChromeDriverManager().install())
            driver.get(url)

            html = driver.page_source
            soup = BeautifulSoup(html, 'html.parser')
            drinks = soup.findAll("div", {"class": re.compile("content02")})

            # 데이터 삽입
            #큰카테고리
            menu = "음료"

            # 중간카테고리
            a = soup.findAll("div", {"class": re.compile("sub_tit_inner")})
            category = a[0].findAll('a', {"class": re.compile("cate")})[0].text

            if category == "웹사이트 비노출 메뉴(사이렌오더 영양정보 연동)":
                category= "추천"

            # 디테일
            eng_name = drinks[0].findAll("div", {"class": re.compile("myAssignZone")})[0].findAll("h4")[0].findAll('span')[0].text
            name = drinks[0].findAll("div", {"class": re.compile("myAssignZone")})[0].findAll("h4")[0].text.replace(eng_name,'')
            description = drinks[0].findAll("p", {"class": re.compile("t1")})[0].text
            
            # price
            def menu_price(idx):
                dict_menu = {
                        "2021 CherryBlossom":6100,
                        "추천":5600,
                        "콜드 브루":5800,
                        "에스프레소":5100,
                        "프라푸치노":6100,
                        "블렌디드":6300,
                        "스타벅스 피지오":5900,
                        "티":5800,
                        "기타 제조 음료":5900
                }
                return dict_menu[idx]
                
            price = menu_price(category)

            # eng_category 작업
            
            
            def switch_eng_menu(idx):
                dict_menu = {
                        "2021 CherryBlossom":"New",
                        "추천":"Recommend",
                        "콜드 브루":"Cold Brew",
                        "에스프레소":"Espresso",
                        "프라푸치노":"Frappuccino",
                        "블렌디드":"Blended",
                        "스타벅스 피지오":"Starvbucks Fizzio",
                        "티":"Teavana",
                        "기타 제조 음료":"Others"
                }
                return dict_menu[idx]

            eng_category = switch_eng_menu(category)


            # 영양소가 1차원 배열인데 텍스트로 수정..할지말지 추후 상의
            # 1차원 배열이었는데 ","를 기준으로 나뉘는 str타입으로 변경했습니다
            nutrition = str([x.text for x in drinks[0]("dd")][:8])[1:-1]
            image = drinks[0].findAll("img", {"class": re.compile("zoomImg")})[0]['src']

            
            # hot / ice 구분을 위한 리스트
            default_ice = ["콜드 브루","프라푸치노","스타벅스 피지오"]

            # hot 과 ice를 나누어주는 조건문
            if category in default_ice:
                hot = False
                ice = True
            elif "아이스" in name:
                hot = False
                ice = True
            else:
                hot = True
                ice = False

            # 알러지 조건문
            # 알러지가 없는 경우엔 그냥 값을 안넣어줄까하다가 그냥 none으로 넣는게 나을것같아서..
            try:
                allergy = drinks[0].findAll("div", {"class": re.compile("product_factor")})[0].findAll("p")[0].text.split(":")[1]
            except IndexError: allergy = "none"

            doc = {"menu":menu,"category":category,"eng_name":eng_name,"name":name
                ,"description":description,"price":price,"eng_category":eng_category
                ,"nutrition":nutrition,"image":image,"allergy":allergy,
                "hot":hot,"ice":ice}
            db.menus.insert_one(doc)
    except:
        print("에러확인!!!!!!!!!")

 

 

1. try - catch

스타벅스 메뉴를 보면 메뉴마다 존재하는 태그가 있고 존재하지않는태그도있다. 예를 들어 스타벅스 어플내에는 알러지조건이 6개밖에 명시되어있지않는데 홈페이지에선 숨겨진 태그까지 존재해서 8개의 알러지태그가 나온다. 이를 고려치않고 처음에 코드를 알러지 6개 기준으로 짰다가 어느메뉴에서인가 크롤링이 멈춰있는 경우가 있었다. 이렇게 존재하지 않는 컬럼을 요구하는 경우에는 조건문만으로는 예외처리를 하지못한다.

그래서 셀레니움 라이브러리가 크롬페이지를 켜는 동작을 하는동안 예상치못한 태그가 발생하면 catch문으로 console에 찍어주게 한뒤 바로 그 다음 try를 진행하는 전반적인 예외처리가 필요했다.

즉, 

 > 페이지 하나가 켜질때마다 try문에 넣는다

 > 그리고 그 안에서 예외가 있는 알러지 항목을 또한 try문에 넣는다

이런식으로 이중 예외처리를 해주니 130개가량의 데이터를 불러올 수 있었다.

 

 

2. 파이썬에서 구현하는 swith/case

그러다가 다시금 난관에 봉착하게됐는데, 메뉴의 이름을 한글뿐만아니라 영어이름도 column에 추가해줘야했다. 사실 예외 처리에 익숙하지 않은 나는 바로 조건문으로 해결하려했는데 그렇게따지면 메뉴종류가 9개인데 if - else처리를 9번씩이나 해줘야하는 것이다. 할수야 있겠지만 아무리봐도 개발자들은 일을 그렇게 처리할리가 없다. 와던중에 다른 서버개발자분께서 swith/case문을 사용해보는건어때요? 라고 하셨다. key값과 value를 매칭해 key값이 들어오면 value로 흘려보내는 로직이다. 듣자마자 떠오른것은 파이썬의 dictonary였다.

그리고 끝에 나온 swith/case의 파이썬 버전이다.

def switch_eng_menu(idx):
                dict_menu = {
                        "2021 CherryBlossom":"New",
                        "추천":"Recommend",
                        "콜드 브루":"Cold Brew",
                        "에스프레소":"Espresso",
                        "프라푸치노":"Frappuccino",
                        "블렌디드":"Blended",
                        "스타벅스 피지오":"Starvbucks Fizzio",
                        "티":"Teavana",
                        "기타 제조 음료":"Others"
                }
                return dict_menu[idx]

            eng_category = switch_eng_menu(category)

category라는 한글변수를 받아서 함수에 넣은뒤에 영어이름인 value값을 받아주는셈이다.

 

tip. venv나 package.json에 있는 라이브러리를 한꺼번에 불러오는 방법

그리고 이건 git을 자주 이용하면서 깨닫게된것인데 지금까지는.. 부끄럽게도 pacakage.json이나 readme를 보고 필요한 라이브러리를 일일히 직접 설치했었다.. 그러나 그럴필요없이.. nodejs에서는 npm install , python에서는 pip install이라고 치면 라이브러리 패키지에 기록된 라이브러리들이 한꺼번에 설치가 된다.

 

 

// 

스크래핑 기본뼈대로 참고한글

velog.io/@may_soouu/%EC%9B%B9-%ED%81%AC%EB%A1%A4%EB%A7%81-%EC%8A%A4%ED%83%80%EB%B2%85%EC%8A%A4-%ED%81%AC%EB%A1%A4%EB%A7%81

 

📌 웹 크롤링 & 스타벅스 크롤링

위의 라이브러리를 설치 후 웹 크롤링을 진행해보려고 한다.미니콘다 가상환경이 설치되어있다는 가정하에 진행하였다.나는 스타벅스 메뉴 중 '음료'리스트의 품목명과 이미지 주소를 따오려

velog.io

 

댓글