본문 바로가기

개발/Python

크롤링 - 오늘 저녁은 맥도날드

저번 포스팅에서 크롤링에 필요한 python 및 관련 라이브러리를 설치했었다.

2020/03/16 - [개발/설치&설정후기] - mac python3 설치 및 크롤링 준비

맥(mac) 환경이라면 위 내용을 참고삼아 순서대로 비교하면서 세팅할 수 있겠지만, 원도우 환경이라면 따로 연관 내용을 직접 찾아서 설치하도록 하자. 어쨌든 우리가 설치해야 하는 건 - python3, beautifulSoup4, (selenium, webdriver)  4개만 잘 설치하면 된다.

 

환경 세팅 이후, 간단히 예제로 다뤄볼 사이트가 없을까 하고 살펴보다가... 최근 늦은 저녁 햄버거를 시켜먹으려고 방문한 맥딜리버리 사이트를 가볍게 분석&크롤링해보기로 했다.

 

사이트를 크롤링하기로 정했다면, 먼저 해당 사이트의 구조 분석이 선행되어야 한다. 이때 chrome 브라우저에 내장된 개발자도구가 유용하게 쓰이는데, 사이트에서 우클릭 > [검사] 버튼으로 진입할 수도 있지만 우클릭을 방지해놓은 사이트도 많기 때문에 단축키도 알아두도록 하자. mac용 단축키는 cmd+alt+i / 윈도우는 ctrl+shift+i / 공통적으로 F12를 눌러보면, 뭔가 보기 불편한 창이 하나 뜬다.

 

만약 새 창이 뜨지 않았다면, 현재 보던 브라우저의 왼쪽 or 오른쪽 or 아래쪽 부근에 생겼을 것이다. 보기 불편하니 개발자도구 영역 우상단의 쩜3개 아이콘을 클릭해보자. Dock side 메뉴가 보일것이다. 가장 왼쪽 아이콘(Undock into separate window)을 누르면 새 창으로 분리될 것이다. 모니터가 충분히 커서 따로 분리 안 해도 잘 보인다면 패스.

 

 

step 1) 맥딜리버리 사이트 분석


자, 이제 맥딜리버리 사이트(https://www.mcdelivery.co.kr/kr/browse/menu.html)에 방문해보자.

여기서 우리가 파악해야 할 것은 크게 2가지다.

맥딜리버리 메뉴정보가 출력된 url 주소

출력된 메뉴연관 정보를 가진 html 객체명과 해당 속성

맥딜리버리 사이트는 크게 일반 메뉴 / 아침 메뉴 2개의 대분류와 하위 6개의 소분류 추천 메뉴, (버거&세트 or 맥모닝&세트),  스낵&사이트, 음료, 디저트, 해피밀 로 이루어져 있음을 쉽게 알 수 있다.

 

우선 일반 메뉴에 마우스 커서를 가져간 다음, 우클릭 > 노출된 메뉴에서 [검사] 를 클릭해보자.

 

메뉴 url 을 찾아보자.

개발자도구에 출력된 Elements 중에서 [검사] 를 클릭한 영역에 하이라이트가 되어 있을 것이다.

 

바로 위를 살펴보면 일반 메뉴의 href 속성 ?daypartId=1 보인다. 이어서 밑의 아침 메뉴의 href 속성은 ?daypartId=2 임을 알 수 있다.

 

이어서 하위 소분류 메뉴들을 살펴보자.

 

하위 메뉴 url 정보.

추천메뉴?daypartId=1&catId=10 ... 가장 아래의 해피밀 메뉴의 url 은 ?daypartId=1&catId=16 이다.

 

위에서 살펴보았듯이, 맥딜리버리 사이트는 아주 심플한 구조임을 알 수 있다.

메뉴정보 url 은 https://www.mcdelivery.co.kr/kr/browse/menu.html

상위 대분류는 daypartId 파라미터값으로 구분. 1=일반메뉴, 2=아침메뉴


하위 소분류는 catId 파라미터값으로 구분. 12를 제외한 10~16 까지의 총 6개 값.

대분류+소분류 파라미터 조합으로 url 이 구성됨.
- 일반메뉴 > 추천메뉴 : ?daypartId=1&catId=10
- 아침메뉴 > 추천메뉴 : ?daypartId=2&catId=10

 

2020.3월 현재, 모든 메뉴정보를 획득하려면 2 x 6 = 총 12개의 메뉴 페이지를 탐색하면 된다.

 

step 2) 메뉴정보  분석


코드부분과 나눠 설명하면 너무 길어져서, 해당 python 코드 부분을 보면서 간단히 설명하도록 하겠다.

 

이번에도 개발자도구를 적극 사용해 메뉴명이 적힌 부분을 우클릭하고 [검사]를 해보자.

 

product-title 메뉴명이 보인다.

각 메뉴들은 div 태그들로 쌓여있는데, 이 div들은 모두 'product-card' 란 class 명을 가지고 있다.

 

내부의 <h5> 태그에 메뉴명이 적혀있는데, 이 태그의 class는 'product-title' 다.  python코드로 표현해보자.

# 예제로 단건만 찾아보자.
# 'product-card'란 class를 가진 div내에서
#  - 'product-title' class를 가진 h5 태그를 찾는다.

title = soup.find('div', 'product-card').find('h5', 'product-title').text.strip()

 

이어서 옆 메뉴(상하이버거 싱글팩)의 가격, 칼로리, 메뉴이미지url 부분을 살펴보자. 

 

메뉴명은 마찬가지로 <h5> 태그에 있고 가격, 칼로리 정보는 각각 동일한 구조의 <span> 태그에 존재한다.

 

메뉴이미지url 은 <img> 태그의 src 속성값을 보면 된다. 여기서 메뉴이미지url을 살펴보면, 해당 메뉴의 고유 id 를 얻을 수 있다.

# 예제로 단건만 찾아보자.
# 'product-card'란 class를 가진 div내에서, 
#  - 'starting-price' class를 가진 span 태그를 찾는다.

price = soup.find('div', 'product-card').find('span', 'starting-price').text.strip()

#  - 'product-nutritional-info' div 태그를 찾은 후, 'text-default' span 태그 탐색.
kcal = soup.find('div', 'product-card').find('div', 'product-nutritional-info').find('span', 'text-default').text.strip()

#  - 'img' 태그를 찾아 src 속성값 획득
img_src = soup.find('div', 'product-card').find('img').get('src')

#  - 획득한 src로부터 정규식을 사용해 id 값 획득
menu_id = re.search('^.*\/(\d+)\.png\?$', img_src, re.IGNORECASE).group(1)

 

예제에서는 거의 beautifulsoup 의 find 함수만 사용했지만, 제공되는 다양한 함수를 통해 다른 방법으로 문서를 탐색할 수 있다. 시간이 된다면, Beautiful Soup Documentation - 함수 종류와 사용 예시를 참고하도록 하자.

 

Beautiful Soup Documentation — Beautiful Soup 4.4.0 documentation

Non-pretty printing If you just want a string, with no fancy formatting, you can call unicode() or str() on a BeautifulSoup object, or a Tag within it: str(soup) # ' I linked to example.com ' unicode(soup.a) # u' I linked to example.com ' The str() functio

www.crummy.com

 

오늘은 정말 짧게 코드만 정리하는 식으로 남겨두려 했으나 ㅠㅠ 너무 길어지게 된 것 같다. 분량조절 실패로 인해(출근압박)... python 코드와 코드 내 주석으로 남은 설명을 대신하겠다. 

 

from selenium import webdriver
from pathlib import Path
import os
import bs4
import re
import collections
import json
import time
import random

def main():
    # Selenium 을 위한 크롬드라이버 로딩
    # 실행 py 파일 경로 획득
    file_path = os.path.dirname(os.path.abspath(__file__))

    # root_path는 직접 지정해도 무방.
    root_path = os.path.dirname(Path(file_path))

    # 크롬 webdriver 로딩
    driver = webdriver.Chrome(root_path+'/resource/chromedriver_80')

    # 웹 자원 로드를 위해 3초 대기
    driver.implicitly_wait(3)

    # file 에 쓰기
    try:
        # 획득한 정보 저장할 파일 지정
        with open(root_path+'/resource/mc_menu.txt', 'w') as f:

            # 대/소분류id 정의
            daypartid_list = ['1', '2']
            catid_list = ['10', '11', '13', '14', '15', '16']

            # 줄바꿈 char
            lnchar = ''

            # daypartId=2&catId=11
            for daypartid in daypartid_list:
                for catid in catid_list:
                    callback_list = get_mc_menu(driver, daypartid, catid)

                    if len(callback_list) == 0:
                        break
                    else:
                        # 처리하는걸 넣자
                        for item in callback_list:
                            f.write(lnchar)
                            f.write(item)
                            lnchar = '\n'

                    # 연속 호출로 인한 대상 웹서버부하 방지용 대기시간 설정
                    time.sleep((random.random()*10%3)/50)

    finally:
        driver.quit()


def get_mc_menu(drv, daypartid, catid):
    query = 'https://www.mcdelivery.co.kr/kr/browse/menu.html?daypartId={}&catId={}'

    # 호출 메뉴 url 구성
    drv.get(query.format(daypartid, catid))

    # 맥메뉴카드 div 탐색
    html = drv.find_element_by_xpath('//div[@class="product-cards"]').get_attribute('innerHTML')

    # html문서 파싱을 위해 parser 지정.
    soup = bs4.BeautifulSoup(html, 'html.parser')

    rtn_list = []

    for div in soup.find_all('div', {'class': 'product-card'}):
        # 메뉴명
        title = div.find('h5', 'product-title').text.strip()

        # 가격
        price = div.find('span', 'starting-price').text.strip()

        # 칼로리
        kcal_span = div.find('div', 'product-nutritional-info').find('span', 'text-default')
        kcal = '';

        # 칼로리 정보가 없는 경우가 있어, 예외처리
        if kcal_span is not None:
            kcal = kcal_span.text.strip()

        img_src = div.find('img').get('src')

        # 메뉴이미지 url로부터 메뉴id 추출
        menu_id = re.search('^.*/(\d+)\.png\?$', img_src, re.IGNORECASE).group(1)

        # json 데이터 구성위한 dict 객체 생성 및 값 설정
        # python3.7 이전버전의 dict 순서가 보장되지 않는 케이스 케어
        obj = collections.OrderedDict()
        obj['menu_id'] = menu_id
        obj['title'] = title
        obj['price'] = price
        obj['kcal'] = kcal
        obj['img_src'] = img_src

        json_data = json.dumps(obj, ensure_ascii=False, sort_keys=False)

        # 콘솔창 출력. 확인용
        print(json_data)

        # 획득한 메뉴정보 담기
        rtn_list.append(json_data)

    return rtn_list


if __name__ == "__main__":
    main()

 

열심히 훔친(?) 결과는 mc_menu.txt 파일에 잘 기록되었다 =)

 

mc_menu.txt 파일 - 수집한 맥딜리버리 메뉴정보. 

 

마치며...


간단히 사이트의 크롤 대상 영역을 캡쳐하고, 관련 부분 코드 정리해서 기록하는 것만으로 시간이 많이 걸려버렸다. 다음에 크롤링 관련 포스팅을 쓰게 된다면, 좀 더 코드와 주석 위주로 공유하는 식으로 작성하려 한다. 

 

많이 부족한 글이지만, 크롤링 작업에 대해 조금은 도움이 되었길 바라며, 이번 포스팅을 마칩니다.