네이버 식당 메뉴 크롤링하기

장윤성·2022년 7월 27일
11

왜..?

이번에 작은 프로젝트를 진행하게 되었는데, 간단하게 말하자면 우리 학교 주변에 있는 맛집을 보여주고 등록할 수 있는 프로젝트다. 이 프로젝트를 할려면 일단 식당에 대한 정보를 데이터베이스에 저장해야됐다.
그럼 일단 학교 주위에 있는 식당을 찾아봐야 했는데, 처음으로 드는 생각이 "공공데이터에 대구광역시에 등록된 식당정보가 있지 않을까?" 였다. 그래서 일단 https://www.data.go.kr/ 에서 데이터가 있는지 보기로 했다.

있긴 한데..

https://www.data.go.kr/data/3056779/fileData.do?recommendDataYn=Y
대구광역시 등록식당이 있긴 한데, 우리 학교 주변을 어떻게 찾을지도 문제였고

연번		업소명		업태		업소주소
1		경북대학교	한식		대구광역시 북구 산격로 80(1층 산격동)

이런 식으로 정보가 너무 부실했다. 최소한 식당 위치를 지도 핀으로 찍고 식당에 대한 정보를 알 수 있도록 식당에 대한 세부정보가 있어야 될텐데 그거조차 존재하지 않았다. 그렇기에 일단 학교 주위에 있는 도로명에 있는 식당들에 대한 데이터를 모아보기로 했다.

일단 학교 주위에 있는 식당에 대한 정보는 얻었는데, 식당에 있는 메뉴나, 평점, 전화번호, 영업시간과 같은 세부정보는 공공데이터 api에 존재하지 않았기 때문에 네이버나 구글 사이트에서 크롤링하는거 이외에는 방법이 없었다. 그래서 식당에 대한 정보가 훨씬 많은 네이버에서 크롤링을 해보기로 했다.

어떤 방식으로 하지..

식당에 대해서 가장 잘 나타나있는 사이트가 네이버 지도였다.
https://map.naver.com/v5/search/ search/ 옆에 검색하고 싶은 단어를 치면 거기에 대한 정보가 랜더링되는데, 예를 들면 강남 맛집을 검색하면

이렇게 근처에 있는 식당정보를 보여준다. 그리고 특정 장소 + 식당 이름을 검색하면 거기에 가장 맞는 식당의 정보도 보여준다. 나는 이미 공공데이터에서 학교 주위에 있는 식당의 이름을 알고있었기 때문에 경북대+식당으로 검색해서 나온 결과를 크롤링하는게 효율적이라는 생각이 들었다.

식당이 있는지 확인

일단 식당이 폐업했는지 먼저 알아보기 위해서 naver에서 제공하는 검색 api를 사용해보기로 했다. https://developers.naver.com/docs/serviceapi/search/local/local.md#%EC%A7%80%EC%97%AD
쉽게 말하면, 이 api를 통해서 식당을 검색했을 때 return하는 배열이 있으면 식당에 대한 정보가 있는거고 없으면 식당이 없어진 경우라고 판단했다. 만약에, 내가 크롤링하는 정보가 api내에 존재했다면 이것만 사용해서 끝냈을 것 같다.

request: https://openapi.naver.com/v1/search/local.json?query=경북대 베스킨라빈스
{
    "lastBuildDate": "Wed, 27 Jul 2022 18:44:38 +0900",
    "total": 1,
    "start": 1,
    "display": 1,
    "items": [
        {
            "title": "배스킨<b>라빈스</b> 대구경대북문",
            "link": "http://www.baskinrobbins.co.kr",
            "category": "카페,디저트>아이스크림",
            "description": "",
            "telephone": "",
            "address": "대구광역시 북구 산격동 1341-2",
            "roadAddress": "대구광역시 북구 대학로 83",
            "mapx": "455197",
            "mapy": "366032"
        }
    ]
}

하지만 구하고 싶은 식당 메뉴랑 별점,리뷰와 같은 정보는 제공하지 않기 때문에 크롤링을 해서 정보를 가져와야했다ㅠㅠ

iframe내에 있는 데이터 크롤링

메뉴에 대한 데이터를 가지고 올려면 식당 세부정보를 나타내는 창을 크롤링 했어야 됐는데 아무리해도 메뉴 class를 selector로 해서 tag를 찾으려고 해도 나오지가 않는 것이었다.

그래서 왜 안나오지 하고 부모 태그를 쭉 따라가다보니 iframe내에 document로 이루어져있었다. frame전환을 하고나서 class를 찾았어야됐는데 보지 못했던 것이다.

driver.switch_to.frame('entryIframe')

그래서 이렇게 frame을 바꿔주고 크롤링을 하니 쉽게 됐다.

검색해도 바로 세부정보가 안나오는 경우

식당을 검색할 때, 특히, 유명한 체인점과 같은 식당들은 검색을 해도 세부정보 전에 나오는 검색리스트가 뜨는 경우가 있었다.

그래서 세부정보 창이 뜨는거와 상관없이 맨위에 있는 식당의 링크(a tag)를 클릭하고 크롤링을 진행할 수 있도록 코드를 작성했다.

driver.switch_to.frame('searchIframe') #  검색하고나서 가게정보창이 바로 안뜨는 경우 고려해서 무조건 맨위에 가게 링크 클릭하게 설정
driver.implicitly_wait(3)
temp = driver.find_element_by_xpath('//*[@id="_pcmap_list_scroll_container"]/ul') # 메뉴표에 있는 텍스트 모두 들고옴(개발자 도구에서 그때그때 xpath 복사해서 들고오는게 좋다)
driver.implicitly_wait(20) # selenium에서 가끔씩 태그 시간내에 못찾는 경우 때문에 일부러 길게 설정해놓음
button = temp.find_elements_by_tag_name('a')
driver.implicitly_wait(20)
if '이미지수' in button[0].text or button[0].text == '': # 가게 정보에 사진이 있는경우
    button[1].send_keys(Keys.ENTER) 
else: # 사진이 없는 경우
    button[0].send_keys(Keys.ENTER)
driver.implicitly_wait(3)

code

driver = webdriver.Chrome("C:/Program Files/chromedriver/chromedriver")
# chromedriver는 따로 설치를 해서 경로를 지정해줘야한다.
def menu(data): # 메뉴 크롤링
    driver.get("https://map.naver.com/v5/search/"+data) # 검색창에 가게이름 입력
    time.sleep(3)
    driver.implicitly_wait(3)
    # iframes = driver.find_elements_by_css_selector('iframe') # 창에 있는 모든 iframe 출력
    # for iframe in iframes:
    #     print(iframe.get_attribute('id'))
    driver.switch_to.frame('searchIframe') #  검색하고나서 가게정보창이 바로 안뜨는 경우 고려해서 무조건 맨위에 가게 링크 클릭하게 설정
    driver.implicitly_wait(3)
    temp = driver.find_element_by_xpath('//*[@id="_pcmap_list_scroll_container"]/ul') # 메뉴표에 있는 텍스트 모두 들고옴(개발자 도구에서 그때그때 xpath 복사해서 들고오는게 좋다)
    driver.implicitly_wait(20) # selenium에서 가끔씩 태그 시간내에 못찾는 경우 때문에 일부러 길게 설정해놓음
    button = temp.find_elements_by_tag_name('a')
    driver.implicitly_wait(20)
    if '이미지수' in button[0].text or button[0].text == '': # 가게 정보에 사진이 있는경우
        button[1].send_keys(Keys.ENTER) 
    else: # 사진이 없는 경우
        button[0].send_keys(Keys.ENTER)
    driver.implicitly_wait(3)
    time.sleep(3)
    driver.switch_to.default_content()# frame이 이상하게 넘어가는 경우 방지를 위해 원래 frame으로 변경한 후에 이동
    driver.switch_to.frame('entryIframe') # 메뉴정보가 entryIframe에 있기 때문에 frame 변경함
    driver.implicitly_wait(2)
    # time.sleep(3)
    start = driver.find_elements_by_class_name('_3ak_I') # 배달의 민족에서 제공하는 메뉴가 랜더링 되어 있는 경우
    if len(start) == 0: # 가게에서 직접 제공하는 메뉴가 랜더링 되어 있는 경우
        start = driver.find_elements_by_class_name('V1UmJ')
    if len(start) == 0: # 메뉴가 없는 경우
        print('메뉴가 없습니다')
        return -1
    return start[0].text

result

결과는 텍스트로 return했기 때문에 쓰레기 값들도 정리하고, 텍스트를 따로 파싱해서 데이터베이스에 넣어주는 작업을 진행해야한다.

파인트
 8,900원
 주문수 2
 쿼터
 17,000원
 패밀리
 24,000원
 하프갤론
 29,000원

Preprocessing

주문수, 2와 같은 쓰레기 값들을 처리해주기 위해 쓸데없는 텍스트를 날리는 코드를 작성했다.
내가 찾은 쓰레기 값들은 excepts 배열에 있는거 이외에는 없었기 때문에 그것을 찾아서 없애주는 작업을 진행했다. 그 이후에는 데이터베이스에 저장해주었다.

excepts = ['사진','별점','대표','주문','없습니다'] # 메뉴 크롤링 한 텍스트중에서 필요없는것들 모아놓음
connect = pymysql.connect(host=db_host, user=db_user, password=db_password, db='sys', charset='utf8')
cur = connect.cursor()
sql = "select * from sys.restaurant" # restaurant 테이블에 저장된 menu 다 들고옴
insert_sql = 'insert into menu (id,restaurant_id,name,price) VALUES (%s, %s, %s, %s)'
cur.execute(query=sql)
datas = cur.fetchall()
count = 1
for data in datas:
    restaurant_id = data[0] # 가게 id가 menu에 있는 데이터의 외부키임
    if data[-3] is None: # 메뉴가 없는 경우
        pass
    else: # 메뉴가 있는 경우
        menu = data[-3].split('\n') # 메뉴가 파인트\n8,900원\n쿼터17,000원\n 형식으로 들어가 있어서 '\n' 기준으로 나눠줘야함=
        new_menu = []
        for content in menu:
            flag = 0
            for ex in excepts: # 쓰레기 데이터가 들어가 있는 경우 없애준다
                if ex in content:
                    flag = 1
                    break
            if flag == 0: # 메뉴,가격에 대한 정보인 경우
                new_menu.append(content)
        for k in range(0,len(new_menu),2):
            print(new_menu[k]+ ' : '+ new_menu[k+1])
            cur.execute(insert_sql,(count,restaurant_id,new_menu[k],new_menu[k+1])) # 짝수 index에는 메뉴, 홀수 index에는 가격이 들어있기 때문에 2개씩 묶어서 넣어준다
            count+=1
connect.commit()
connect.close()

메뉴는 크롤링해왔지만 아직 번호, 리뷰, 평점은 크롤링하지 못한 상태이다. 나머지 작업과정은 다음 포스트에 올릴 예정이다.

profile
소개를 어떻게 한줄로 해요..

2개의 댓글

comment-user-thumbnail
2022년 12월 4일

네이버 지도 크롤링하면서 클래스 네임으로 아무리 찾아도 안 나와서 머리 아파하고 있었는데 frame 전환을 해주어야되는거군요..! 정말 감사합니다. 하나 배워갑니다!

답글 달기
comment-user-thumbnail
2023년 11월 3일

어떻게 해야하나 막막했는데 길잡이가 되는 글을 발견한 것 같습니다 감사해용

답글 달기