[AI 여행 가이드 회고록] 네이버맵 메뉴 크롤링

atdawn·2025년 3월 1일
0

AIVLE

목록 보기
25/25

🔗 프로젝트 GitHub 바로 가기

📌 담당 역할 : AI 음성 가이드 챗봇 Full- Stack

📌 서비스 프로세스

📌 네이버 크롤링이 필요했던 이유

GPT API를 활용하여 챗봇을 제작하며, 가장 큰 문제였던 것은 할루시네이션 문제였다. 만연하게 GPT의 성능을 믿고, 프롬프트만 잘 작성하면 되겠지~ 했었지만 그럴싸하게 내놓은 추천 카페, 음식점에 대한 답변은 사실 존재하지 않는 가게가 더 많았다.
이를 해결하기 위해! 우리는 먼저 GPT를 관광지 데이터를 기반으로 파인튜닝하였고, 이후 RAG(Retrieval-Augmented Generation)를 도입하여 좀 더 정확한 정보를 input으로 제공하였다.
그리고 우리가 수집한 관광 장소 이후에도 더 다양한 장소를 추천해주기 위해 Google Place API와 네이버 크롤링을 도입하였다!
🌟 결론은, Google Place API는 음식점, 카페의 메뉴를 제공하지 않았기 때문에 네이버맵 크롤링을 통해 메뉴와 가격 정보를 수집하여 GPT input으로 넣어주었다.
여행 중에는 맛집 탐방이 중요한 요소이기 때문에, 사용자가 특정 가게를 검색하면 해당 가게의 메뉴와 가격을 한눈에 확인할 수 있도록 했다. 이를 통해 사용자의 여행 경험을 보다 직관적이고 편리하게 만들고자 했다.


📌 코드

def crawling_get_menus(store_name, driver):
    # 결과를 담을 딕셔너리 (초기에 "menu"는 빈 리스트)
    menu_data = {
        "place_name": store_name,
        "menu": []
    }

    # 1. 네이버 지도 접속
    driver.get("https://map.naver.com/v5/search/" + store_name)

    # 2. 자바스크립트 렌더링 대기
    time.sleep(2.5)

    # 3. searchIframe 진입
    try:
        WebDriverWait(driver, 2).until(
            EC.frame_to_be_available_and_switch_to_it((By.ID, "searchIframe"))
        )
    except TimeoutException:
        print("searchIframe 로딩 실패. 함수 종료.")
        return menu_data  # 메뉴가 없는 상태로 딕셔너리 반환
    
    # 4. 검색 리스트 컨테이너 로드 대기
    try:
        container = WebDriverWait(driver, 2).until(
            EC.presence_of_element_located((By.XPATH, '//*[@id="_pcmap_list_scroll_container"]/ul'))
        )
    except TimeoutException:
        print("검색 리스트 컨테이너 로딩 실패. 함수 종료.")
        return menu_data

    # 5. a 태그들 찾기
    buttons = container.find_elements(By.TAG_NAME, 'a')
    if not buttons:
        print("가게 리스트 버튼을 찾지 못했습니다.")
        return menu_data

    # 첫 번째 버튼의 class 속성을 확인
    first_btn_class = buttons[0].get_attribute("class")

    # place_thumb가 포함되어 있다면 → buttons[1] 엔터
    if "place_thumb" in first_btn_class:
        if len(buttons) > 1:
            buttons[1].send_keys(Keys.ENTER)
        else:
            print("클릭할 다른 버튼이 없습니다.")
            return menu_data
    else:
        # 기존 로직
        buttons[0].send_keys(Keys.ENTER)

    # 7. 메인 프레임으로 복귀
    driver.switch_to.default_content()
 
    # 8. entryIframe 로드 & 진입
    try:
        entry_iframe = WebDriverWait(driver, 3).until(
            EC.presence_of_element_located((By.ID, "entryIframe"))
        )
        driver.switch_to.frame(entry_iframe)
    except TimeoutException:
        print("entryIframe 로딩 실패. 함수 종료.")
        return menu_data

    # 9. 스크롤해서 메뉴 영역 로딩 (lazy loading 방지용)
    driver.execute_script("window.scrollTo(0, 700)")

    # 10. "div.place_section.Sv9Ys" 등장 대기 → 사진 메뉴 있는 가게인지 확인
    try:
        WebDriverWait(driver, 2).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, "div.place_section.Sv9Ys"))
        )
    except TimeoutException:
        print("메뉴 로딩 실패(사진 섹션 미확인). 함수 종료.")
        return menu_data

    # 11. 사진 있는 메뉴 (ul.t1osG / li.ipNNM)
    ul_t1osG = driver.find_elements(By.CSS_SELECTOR, "ul.t1osG")
    if ul_t1osG:
        li_elements = ul_t1osG[0].find_elements(By.CSS_SELECTOR, "li.ipNNM")
        if li_elements:
            for li in li_elements:
                menu_name = li.find_element(By.CSS_SELECTOR, "span.VQvNX").text
                menu_price = li.find_element(By.CSS_SELECTOR, "div.gl2cc").text
                menu_data["menu"].append((menu_name, menu_price))
            return menu_data  # 딕셔너리 반환

    # 12. 사진 없는 메뉴 (ul.jnwQZ / li.gHmZ_)
    ul_jnwQZ = driver.find_elements(By.CSS_SELECTOR, "ul.jnwQZ")
    if ul_jnwQZ:
        li_elements = ul_jnwQZ[0].find_elements(By.CSS_SELECTOR, "li.gHmZ_")
        if li_elements:
            for li in li_elements:
                menu_name = li.find_element(By.CSS_SELECTOR, "div.ds3HZ a").text
                menu_price = li.find_element(By.CSS_SELECTOR, "div.mkBm3").text
                menu_data["menu"].append((menu_name, menu_price))
            return menu_data

    # 13. 둘 다 못 찾으면 메뉴가 없는 것으로 판단
    print("메뉴 목록이 없습니다.")
    return menu_data

이 함수는 Selenium을 활용하여 네이버 지도를 크롤링하고, 특정 가게의 메뉴 정보를 가져오는 과정으로 이루어져 있다.
즉 Google Place API에서 가져온 가게 이름을 파라미터로 입력받아, 그 가게의 메뉴를 크롤링 해온다.

• 네이버 지도 접속: 검색어(store_name)를 이용해 네이버 지도 검색 결과 페이지를 연다.
• 프레임 전환 및 검색 리스트 확인: 네이버 지도는 iframe을 사용하므로, 먼저 searchIframe으로 전환한 후 가게 목록을 탐색한다.
• 가게 상세 페이지 이동: 검색된 첫 번째 가게의 상세 페이지로 이동한다. 네이버 지도에서는 대표 이미지가 있는 가게와 없는 가게가 다르게 표시되기 때문에, 이를 고려하여 적절한 버튼을 클릭하도록 설정했다.
• 메뉴 섹션 확인: 메뉴는 Lazy Loading 방식이므로, 스크롤을 내려야 메뉴 리스트가 정상적으로 로딩된다. 이후 CSS Selector를 이용해 메뉴와 가격을 추출한다.
• 데이터 반환: 사진이 있는 메뉴와 사진이 없는 메뉴 두 가지 경우를 모두 탐색한 뒤, 최종적으로 크롤링한 데이터를 딕셔너리 형태로 반환한다.


📌 테스트

위 코드를 테스트 해보자!

driver = webdriver.Chrome()
menus = crawling_get_menus("단디 잠실본점", driver)
print(menus)
driver.quit()

결과를 보면 메뉴와 가격이 잘 가져와진 것을 확인할 수 있다!


📌 3. 겪었던 문제와 해결 과정

✅ 네이버 지도는 iframe 구조로 되어 있음
네이버 지도는 searchIframe과 entryIframe이라는 두 개의 iframe을 사용하여 데이터를 렌더링한다. 처음에는 searchIframe에 접근해 검색 리스트를 확인하고, 이후에는 entryIframe을 통해 상세 페이지 정보를 가져와야 한다.
→ 해결 방법: driver.switch_to.frame()을 적절히 사용하여 각 단계에서 iframe을 변경하도록 구현했다.

✅ 가게 리스트에서 정확한 버튼 클릭이 필요함
네이버 지도는 검색된 가게에 따라 UI가 조금씩 다를 수 있다. 첫 번째 검색 결과가 이미지 썸네일이 포함된 경우와 아닌 경우가 있어, 이를 고려하여 버튼 클릭 로직을 조정했다.
→ 해결 방법: get_attribute("class")를 활용하여 버튼의 클래스를 확인한 후 적절한 요소를 클릭하도록 구현했다.

✅ Lazy Loading으로 인해 메뉴가 로딩되지 않는 문제
스크롤을 내리기 전에는 메뉴 정보가 로드되지 않기 때문에, 단순히 find_element()를 실행하면 메뉴를 찾지 못하는 경우가 발생했다.
→ 해결 방법: driver.execute_script("window.scrollTo(0, 700)")을 사용하여 메뉴 섹션까지 스크롤을 내린 후 WebDriverWait으로 요소가 나타날 때까지 기다렸다.

✅ CSS Selector 변경 문제
네이버 지도는 자주 UI가 업데이트되면서 CSS Selector가 변경될 수 있다. 개발 과정 중 몇 번의 업데이트가 있었고, 이로 인해 기존 코드가 작동하지 않는 경우가 발생했다.
→ 해결 방법: 정적인 XPath보다는 유니크한 CSS 클래스를 기준으로 요소를 찾도록 변경하고, 필요할 때마다 셀렉터를 업데이트했다.


이 프로젝트를 진행하면서 웹 크롤링의 기본적인 원리뿐만 아니라, iframe 구조를 다루는 방법, Lazy Loading 처리, UI 변화에 대한 대응 방식 등을 배울 수 있었다. 또한, 크롤링이 단순히 데이터를 가져오는 것이 아니라, 실제로 유지보수와 성능 최적화까지 고려해야 한다는 점도 깨달았다.

추후 다른 프로젝트에서도 크롤링이 필요하다면, 단순히 HTML 요소를 긁어오는 것뿐만 아니라, 서비스의 구조를 분석하고 API 활용 가능성을 검토하는 것이 중요하다는 점을 기억해야겠다.

+) 다음은 GPT 파인튜닝과 RAG를 회고록으로 작성할 예정이다!

profile
복습 복습 복습

0개의 댓글