프로그래밍 언어/Python

맛집 정보 수집하기

· 코딩마이데이

네이버 지도에서 검색한 서울의 미쉐린 맛집을 CSV 파일로 저장하겠습니다. 크로미움 웹 브라우저의 개발자 도구의 인스펙트를 사용하여 네이버 지도 검색 결과룰 분석한 후, 이를 바탕으로 데이터 수집을 자동화하는 함수를 작성할 것입니다.

 

데이터 추출

01 소스 코드 'step_1_3.py'를 실행하고 인스펙터에서 왼쪽 상단의 Pick locator 아이콘을 클릭합니다.

 

02 네이버 지도에서 검색 결과의 업체명을 클릭합니다. 인스펙터 하단 [Locator] 탭에 iframe 태그를 특정하고 내부의 요소를 제어하는 함수 frame_locator()가 포함된 코드가 생성됩니다.

locator("iframe[title=\"Naver Place Search\"]").content_frame.get_by_role("button", name="대삼계장인 톡톡 백숙,삼계탕")

 

03 HTML 요소를 분석하기 위해 크로미움 웹 브라우저에서 F12 키를 눌러 개발자 도구를 엽니다. 개발자 도구 왼쪽 상단의 검사 모드 아이콘을 클릭한 후, 네이버 지도에서 검색한 결과의 업체명을 클릭합니다.

 

개발자 도구 [요소] 탭을 클릭하고 마우스를 사용하여 업체명을 둘러싼 태그 구조를 살펴봅시다. 네이버 지도에서 키워드 검색 결과는 <ul> 태그와 <li> 태그로 구성되어 있습니다. 하단의 <div class="lazyload-wrapper">에 주목합니다.

<ul>
  <li class="UEzoS rTjJo" data-laim-exp-id="undefinedundefined">
    <div class="CHC5F">
      <div class="SgjqM">
        <a href="#" target="_self" role="button" class="YTJkH CtW3e"
          ><span class="TYaxT">3대삼계장인</span
          ><span class="W6IZ8"
            ><span class="KCMnt">백숙,삼계탕</span></a
        >
    ...중략...
  </li>
  <li class="UEzoS rTjJo"></li>
  <div class="lazyload-wrapper"></div>
  <div class="lazyload-wrapper"></div>
</ul>

 

<div> 태그의 속성 class의 값인 lazyload-wrapper는 지연 로딩을 의미합니다. 지연 로딩은 웹 문서의 모든 내용을 한 번에 불러오지 않고, 스크롤을 내리는 등으 동작을 할 때마다 필요한 데이터를 추가적으로 불러오는 기법입니다. 네이버 지도 페이지에 지연 로딩이 적용되어 있으므로, 필요한 데이터를 모두 수집하려면 스크롤을 움직여 현재 표시된 것보다 더 많은 데이터를 불러와야 합니다.

개발자 도구의 요소 탭에서 <div class="lazyload-wrapper"></div> 태그를 클릭한 후, 마우스 오른쪽 버튼을 누르고 [Scroll into view(보기로 스크롤)] 메뉴를 선택합니다. 해당 요소로 스크롤이 이동한 후 새로운 데이터가 추가로 로드됩니다. 이 과정을 <ul> 태그의 바로 하위 <div> 태그가 모두 사라질 때까지 반복되면 모든 데이터를 불러올 수 있습니다.

지금까지 분석한 내용을 바탕으로 네이버 지도 검색 결과에서 업체명과 카테고리를 추출하는 함수를 작성해 봅시다. 비주얼 스튜디오 코드에서 새로운 파일을 만들고, 파일명을 'step_1_3.py'로 저장합니다. 다음 코드를 입력한 후 실행 아이콘을 클릭하여 실행합니다. 크로미움 웹 브라우저가 네이버 지도에서 '미쉐린 서울'을 검색한 뒤, 검색 결과에서 업체명과 카테고리를 추출하여 화면에 출력합니다.

 

ch_12/step_1_4.py

from playwright.sync_api import Page
from step_1_3 import search_map  # 이전에 작성한 모듈을 불러옵니다.

def parse_names(page: Page) -> list[tuple[str, str]]: # 04
    iframe = page.locator('iframe[title="Naver Place Search"]').content_frame  # 검색 결과 창 # 05
    while iframe.locator("ul > div").count() > 0:  # ul 바로 하위의 div가 발견되면, # 06
        iframe.locator("ul > div").last.scroll_into_view_if_needed()  # div로 이동 # 07
        page.wait_for_timeout(500)  # 0.5초간 일시정지 # 08

    names = []  # 이 변수에 검색 결과를 저장 # 10
    for tag_li in iframe.locator("ul > li").all():  # ul 바로 하위의 모든 li 추출 # 11
        tag_a = tag_li.locator("a").first  # li 하위의 a 태그 중 첫 번째 # 12
        tag_span = tag_a.locator("span")  # a 태그 하위의 span 태그 # 13
        name = tag_span.first.inner_text()  # 첫 번째 span의 텍스트는 업체명 # 14
        category = tag_span.last.inner_text()  # 마지막 span의 텍스트는 카테고리
        names.append((name, category))  # (업체명, 카테고리) 형식으로 결과를 저장
    return names


if __name__ == "__main__":
    play, browser, page = search_map("미쉐린 서울")  # 네이버 지도에서 키워드 검색
    names = parse_names(page)  # 업체명과 카테고리 추출
    print(names)

    browser.close()  # Browser 객체 삭제
    play.stop()  # Playwright 객체 삭제

 

실행 결과

[('3대삼계장인', '백숙,삼계탕'), ('서령 본점', '한식'), ('광화문미진', '한식'), ('우래옥', '냉면'), ('봉밀가 강남구청점', '냉면'), ('진미평양냉면', '냉면'), ('오레노라멘 본점', '일본식라면'), ('평양면옥', '냉면'), ('고사리 익스프레스 신당', '한식'), ('필동면옥', '냉면')]

 

04 네이버 지도 검색 결과에서 업체명과 카테고리를 추출하는 함수 parse_names()를 정의합니다.

 

05 인스펙터로 녹화한 코드에서 함수 frame_locator()를 복사하여 코드 편집기에 붙여 넣으세요. 이 코드를 실행하면 <iframe> 태그를 찾고, 변수 iframe에 저장합니다.

 

06~07 <ul> 태그 바로 하위의 <div> 태그를 찾는 CSS 셀렉터 "ul > div"와 요소와 개수를 세는 함수 count()를 사용하여 <div> 태그를 찾고, 함수 scroll_into_view_if_needed()를 호출하여 해당 태그로 이동합니다. 이때 last 속성을 사용하여 마지막 <div> 태그를 선택합니다.

 

08 새로운 데이터가 로딩될 때까지 기다리기 위해 함수 wait_for_timeout(500)를 사용하여 실행을 0.5초간 일시정지합니다.

 

10 CSS 셀렉터 "ul > li"와 함수 all()을 사용해 <ul> 태그 하위의 <li> 태그를 추출합니다.

 

11~12 <li> 태그 하위의 <a> 태그 중 첫 번째를 선택하고, 해당 <a> 태그의 히위 <span> 태그를 선택합니다.

 

13~14 속성 first와 함수 inner_text()를 사용해 첫 번째 <span> 태그의 텍스트를 업체명으로, 속성 last과 함수 inner_text()를 사용해 마지막 <span> 태그의 텍스트를 카테고리를 추출합니다.