약 한 달 반 전, 팀 프로젝트에서 가장 먼저 시도했던 네이버 지도 크롤러 개발 과정을 공유하려 한다.
당시 내가 맡은 부분은 맛집 추천 에이전트였다. 한국에서는 네이버 플레이스가 가장 많은 정보를 보유하고 있지만 공식 API가 없었기 때문에, 초기에는 셀레니움을 활용한 동적 크롤링을 시도했다. 솔직히 동적 크롤링은 처음 해보는 거라 신기하고 흥미로웠다.
결과적으로 1박 2일 일정에 하루 3끼, 최소 15개 이상의 식당 데이터가 필요했고 크롤링 속도 문제로 인해 구글 플레이스 API로 방향을 전환했지만, 오늘은 당시 개발했던 크롤러를 다시 살펴보려 한다.
개발 목표
- 네이버 지도에서 "부산 해운대 음식점" 검색 결과 수집하기
- 각 음식점의 이름, 카테고리, 평점, 주소 정보 추출하기
- 자동으로 모든 검색 결과를 스크롤하며 데이터 수집하기
개발 환경 설정
크롤링을 위해 Python과 Selenium을 사용했다. Selenium은 웹 브라우저를 자동화할 수 있는 강력한 도구라서 네이버 지도처럼 동적으로 로딩되는 페이지를 크롤링하기에 적합했다. 처음 써보는 기술이라 설레는 마음으로 코드를 작성했던 기억이 난다.
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException
import time
개발 과정
1. iframe 핸들링의 어려움
네이버 지도는 검색 결과와 상세 정보를 각각 다른 iframe에 표시한다. 처음에는 이 부분을 간과했다가 요소를 찾을 수 없다는 에러가 계속 발생했다. 몇 시간 동안 고민하다가 개발자 도구를 뒤져보니 iframe 구조로 되어 있는 것을 발견했다. 디버깅을 통해 iframe 간 전환이 필요하다는 것을 알게 되었고, 이를 위한 유틸리티 함수를 만들었다.
def switch_left():
try:
driver.switch_to.default_content()
iframe = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "#searchIframe"))
)
driver.switch_to.frame(iframe)
except Exception as e:
print(f"iframe 전환 중 오류: {e}")
def switch_right():
try:
driver.switch_to.default_content()
iframe = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "#entryIframe"))
)
driver.switch_to.frame(iframe)
except Exception as e:
print(f"iframe 전환 중 오류: {e}")
왼쪽 프레임(searchIframe)은 검색 결과 목록을, 오른쪽 프레임(entryIframe)은 선택한 장소의 상세 정보를 보여준다. 이 두 프레임 사이를 자유롭게 오가며 데이터를 수집해야 했다. 마치 두 개의 웹페이지를 동시에 다루는 느낌이었다.
2. 동적 로딩 컨텐츠와 대기 시간
네이버 지도는 스크롤을 내릴 때마다 새로운 콘텐츠를 로드한다. 처음에는 단순히 모든 요소를 한 번에 가져오려고 했는데, 검색 결과가 10개 정도밖에 안 나왔다. 알고보니 스크롤을 내려야 더 많은 결과가 로드되는 방식이었다. 이를 처리하기 위해 스크롤 관련 코드를 구현했다:
# 스크롤 처리
last_height = driver.execute_script("return document.body.scrollHeight")
while True:
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(2)
new_height = driver.execute_script("return document.body.scrollHeight")
if new_height == last_height:
break
last_height = new_height
이 코드는 페이지 끝까지 스크롤하면서 모든 결과를 로드한다. 페이지 높이가 더 이상 변하지 않으면 모든 결과가 로드된 것으로 판단하고 스크롤을 멈춘다. 처음 이 코드가 작동했을 때는 정말 신기했다. 마치 마법처럼 브라우저가 자동으로 스크롤을 내리면서 데이터를 모두 불러오는 모습을 보니 감탄이 절로 나왔다.
3. 선택자(Selector) 찾기
네이버 지도의 HTML 구조는 꽤 복잡했다. 특히 class 이름이 의미 없는 문자열(UEzoS, TYaxT 등)로 되어 있어 이해하기 어려웠다. 원하는 정보의 정확한 CSS 선택자를 찾아내는 것이 중요했다. 개발자 도구를 통해 여러 시행착오 끝에 다음과 같은 선택자를 사용했다:
- 장소 목록: li.UEzoS
- 가게 이름: span.TYaxT
- 카테고리: span.lnJFt
- 평점: span.PXMot
- 주소: span.LDgIH
완성된 코드로 데이터 수집하기
각 음식점의 이름을 클릭하고, 상세 정보 페이지로 전환한 다음, 필요한 정보를 수집한다. 그리고 다시 검색 결과 목록으로 돌아가서 다음 음식점을 처리한다.
# 네이버 지도 접속
URL = 'https://map.naver.com/p/search/부산%20해운대%20음식점'
driver.get(URL)
time.sleep(3) # 페이지 로딩 대기
# iframe 전환
switch_left()
time.sleep(2)
# (스크롤 코드는 위에서 설명)
# 검색 결과 수집
places = driver.find_elements(By.CSS_SELECTOR, "li.UEzoS")
print(f"총 {len(places)}개의 장소를 찾았습니다.")
for i, place in enumerate(places, 1):
try:
# 가게 이름 클릭
name_element = place.find_element(By.CSS_SELECTOR, "span.TYaxT")
name = name_element.text
name_element.click()
time.sleep(2)
# 상세 정보 수집을 위해 iframe 전환
switch_right()
time.sleep(2)
# 상세 정보 수집
try:
category = driver.find_element(By.CSS_SELECTOR, "span.lnJFt").text
except:
category = "정보 없음"
try:
rating = driver.find_element(By.CSS_SELECTOR, "span.PXMot").text
except:
rating = "평점 없음"
try:
address = driver.find_element(By.CSS_SELECTOR, "span.LDgIH").text
except:
address = "주소 정보 없음"
# 정보 출력
print(f"\n{i}. {name}")
print(f"카테고리: {category}")
print(f"평점: {rating}")
print(f"주소: {address}")
print("-" * 50)
# 다음 가게를 위해 검색 결과 프레임으로 전환
switch_left()
time.sleep(1)
except Exception as e:
print(f"가게 정보 수집 중 오류: {e}")
switch_left()
continue
결과 및 배운 점
크롤러를 실행한 결과, 부산 해운대 주변의 수십 개 음식점 정보를 자동으로 수집할 수 있었다! 코드가 실행되는 모습을 보니 정말 뿌듯했다. 물론 중간중간 오류가 발생하기도 했지만, try-except 구문으로 대부분의 예외 상황을 처리했다.
- iframe 처리의 중요성: 현대 웹사이트는 종종 여러 iframe을 사용하므로, Selenium으로 크롤링할 때 frame 전환에 주의해야 한다.
- 동적 콘텐츠 처리: 무한 스크롤이나 동적 로딩 콘텐츠를 처리하는 방법을 익혔다. 이 부분이 가장 어려웠지만 가장 배움이 컸다.
- 예외 처리의 중요성: 크롤링 중에는 다양한 예외 상황이 발생할 수 있으므로, 강건한 예외 처리가 필수다. 처음에는 예외 처리를 안 했다가 한 번의 오류로 전체 프로그램이 멈춰서 당황했던 기억이 난다.
- 적절한 대기 시간: 웹페이지가 로드되는 시간을 고려해 적절한 대기 시간을 설정하는 것이 중요하다. time.sleep() 값을 너무 작게 주면 요소를 찾지 못하고, 너무 크게 주면 속도가 느려진다.
아쉬운 점과 다음 단계
결국 이 크롤러는 속도 문제 때문에 프로젝트에 적용하지 못했다. 15개 이상의 식당을 크롤링하는 데 너무 많은 시간이 소요되었고, 여행 일정이 길어질수록 데이터량이 기하급수적으로 증가하는 문제가 있었다. 그래서 구글 플레이스 API로 방향을 전환했지만, 여전히 네이버 데이터의 풍부함이 그리웠다.
추가 개선점
- 비동기를 활용해 크롤링 속도 개선하기
- 수집한 데이터를 CSV나 엑셀 파일로 저장하기
- 더 많은 정보(영업시간, 메뉴, 리뷰 등) 수집하기
- 식당 이름별 인덱싱으로 중복 처리 효율화하기
웹 크롤링은 정말 강력한 도구이지만, 웹사이트 구조가 변경되면 크롤러도 업데이트해야 한다는 점을 항상 기억해야 한다. 또한 적절한 시간 간격을 두고 크롤링하는 것이 서버에 부담을 주지 않는 예의바른 방법이라는 것도 잊지 말아야겠다.
처음 시도했던 기술이라 아쉬움이 남지만, 이 경험을 통해 동적 웹 크롤링에 대해 많이 배울 수 있었다. 다음 프로젝트에서는 이 경험을 살려 더 효율적인 크롤러를 만들 수 있을 것 같다. 생각보다 재미있는 경험이었다!
'Python' 카테고리의 다른 글
[Python] FastAPI JWT 인증 미들웨어 분석 – 어떻게 동작할까? (0) | 2025.02.24 |
---|---|
[Python] 1:1 문의 답변 이메일 전송하기(smtplib + Gmail) (0) | 2025.02.22 |
[Python] Redis를 활용한 중복 추천 방지 로직 상세 분석 (0) | 2025.02.20 |
[Python] Redis를 활용한 중복 추천 방지 캐싱 서비스 개발 (0) | 2025.02.19 |
[Python] print와 logging, 디버깅할 때 무엇을 쓸까? (0) | 2025.02.17 |