본문 바로가기
■ Tools/Python

[Crawler] 네이버 지도 크롤링 - 여러 URL을 하나의 탭에서 크롤링

by 대장고양이샤샤 2024. 10. 25.

 

검색어가 정해져있을 때, 드라이브 한탭 에서 크롤링 하는 방법 📌

크롤링할때마다 새 창이 켜져서 예상시간 3일뜨는 거보고 남기는 기록,,

- 하나의 탭에서 크롤링 할수 있게 수정했고 10시간정도로 줄였다.

 

네이버 지도를 크롤링할 때, 주로 페이지번호 1, 페이지 번호 2 등 페이지 번호 기준으로 크롤링을 한다.

그러나 검색어를 지정하고, 검색어별/URL별로 크롤링을 해야할 때 속도와 성능을 최적화하는 코드를 설명해보고자 한다.

 

 

 

def crawl_multiple_naver_places(urls_data):
    driver = None
    all_places_data = []

    try:
        # Chrome 드라이버 한 번만 초기화
        driver = webdriver.Chrome()
        wait = WebDriverWait(driver, 10)

        # 각 URL에 대해 크롤링 수행
        for idx, url_data in enumerate(urls_data, 1):
            try:
                url = url_data['url']
                print(f"\n크롤링 진행 중... ({idx}/{len(urls_data)}): {url}")

                driver.get(url)

                # iframe 전환
                iframe = wait.until(EC.presence_of_element_located((By.ID, "entryIframe")))
                driver.switch_to.frame(iframe)

                # 1. 홈탭 정보 수집
                base_info = get_base_info(driver, wait)

                # 2. 리뷰탭 정보 수집
                review_info = get_review_info(driver, wait)

                # 원본 데이터와 크롤링 데이터 병합
                # original_data에서 첫 번째 쉼표까지만 가져오기
                original_data = url_data['original_data'].split(',')[0]

                # 장소별 정보를 리스트에 추가
                place_data = {
                    "original_data": original_data,  # 가맹점명만 포함
                    "base_info": base_info,
                    "review_info": review_info
                }
                all_places_data.append(place_data)

                # 기본 프레임으로 돌아가기
                driver.switch_to.default_content()

                print(f"✓ {idx}번째 장소 크롤링 완료")

            except Exception as e:
                print(f"⚠ {idx}번째 URL 크롤링 실패: {str(e)}")
                continue

        # 모든 데이터를 하나의 JSON 파일로 저장
        if all_places_data:
            save_multiple_to_json(all_places_data)

        return all_places_data

    except Exception as e:
        print(f"크롤링 중 치명적 에러 발생: {str(e)}")
        return None

    finally:
        if driver:
            driver.quit()
            print("\n크롤링 완료 - 드라이버 종료")
사용 예시 - TEST
if __name__ == "__main__":
    urls = [
    {
        "original_data": "통큰돼지, 가정식, 제주 제주시 용담이동 2682-9번지 통큰돼지",
        "search_term": "통큰돼지 용담이동",
        "url": "https://map.naver.com/p/search/%ED%86%B5%ED%81%B0%EB%8F%BC%EC%A7%80%20%EC%9A%A9%EB%8B%B4%EC%9D%B4%EB%8F%99"
    },
    {
        "original_data": "한그릇, 단품요리 전문, 제주 서귀포시 색달동 2315-1번지 한그릇",
        "search_term": "한그릇 색달동",
        "url": "https://map.naver.com/p/search/%ED%95%9C%EA%B7%B8%EB%A6%87%20%EC%83%89%EB%8B%AC%EB%8F%99"
    }
    ]
    
    result = crawl_multiple_naver_places(urls)

 

 

 

단계별로 설명해보겠다

 

STEP 1 . 함수 정의와 초기화, Chrome 웹드라이버 설정

def crawl_multiple_naver_places(urls_data):
    driver = None
    all_places_data = []
    
    driver = webdriver.Chrome()
	wait = WebDriverWait(driver, 10)

 

URL과 원본 데이터를 포함한 딕셔너리 리스트를 매개변수로 받는다.

Chrome 웹드라이버는 최초에 크롤링 창을 열때 초기화한다.

WebDriverWait로 최대 10초까지 대기하도록 설정한다.

 

for idx, url_data in enumerate(urls_data, 1):
    url = url_data['url']
    print(f"\n크롤링 진행 중... ({idx}/{len(urls_data)}): {url}")

 

각 URL 에 대해 순차적으로 크롤링을 수행한다. (진행 상황 표시)
여러 URL을 반복문을 통해 드라이버를 재사용하여 효율적으로 처리할 수 있다.

 

 

 

 

✅ WebDriverWait 이란?

WebDriverWait(driver, 10)은  Selenium에서 특정 조건을 만족할때까지 대기하는 명시적 대기 기능을 제공합니다.


지정된 조건이 만족될 때까지 대기하다가 , 조건 만족 시 즉시 다음 코드 실행한다.
최대 대기 시간 초과 시 TimeoutException가 발생한다. 

# 기본 설정
wait = WebDriverWait(driver, 10)  # 최대 10초 대기

# 사용 예시
# 요소가 클릭 가능할 때까지 대기
element = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "button.submit")))

# iframe이 존재할 때까지 대기
iframe = wait.until(EC.presence_of_element_located((By.ID, "entryIframe")))

 

✅ expected_conditions 이란?

# iframe이 존재할 때까지 대기
iframe = wait.until(EC.presence_of_element_located((By.ID, "entryIframe")))

 

위 코드를 예시로 보면, Selenium의 'Expected Conditions(EC)'는 특정 요소가 DOM에 존재할때까지 기다리는 조건으로,

By.ID를 통해 "entryframe"요소가 나올때 까지 대기하는 코드이다. 

 

구성요소:

  • presence_of_element_located: 요소가 DOM에 존재하는지 확인하는 메서드
  • By.ID: 요소를 찾을 방법 (ID로 찾기)
  • "entryIframe": 찾고자 하는 요소의 ID 값
from selenium.webdriver.support import expected_conditions as EC

# 1. 요소가 존재하는지 확인
wait.until(EC.presence_of_element_located((By.ID, "myElement")))

# 2. 요소가 클릭 가능한지 확인
wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "button.submit")))

# 3. 요소가 화면에 보이는지 확인
wait.until(EC.visibility_of_element_located((By.CLASS_NAME, "menu")))

# 4. 요소가 사라질 때까지 대기
wait.until(EC.invisibility_of_element_located((By.CLASS_NAME, "loading")))

# 5. 특정 텍스트가 요소에 존재하는지 확인
wait.until(EC.text_to_be_present_in_element((By.CLASS_NAME, "price"), "1000원"))

요소를 찾는 다른 방법들

 

# ID로 찾기
By.ID


# CSS 선택자로 찾기
By.CSS_SELECTOR


# 클래스 이름으로 찾기
By.CLASS_NAME


# XPath로 찾기
By.XPATH

 

 

 

 

 

STEP2. 네이버 지도의 iframe을 찾아 전환한다. (동적 네트워크 웹 크롤링의 안정성 향)

 

✅ iframe이란?

iframe(Inline Frame)은 웹페이지 안에 또 다른 웹페이지를 삽입할 수 있는 HTML 요소, "웹페이지 속의 웹페이지"

예)

지도 서비스 내에 가게 정보를 보여줄 때

유튜브 영상을 다른 웹사이트에 임베드할 때

페이스북 좋아요 버튼을 웹사이트에 넣을 때

  • iframe에 접근하기 위해서는 반드시 해당 iframe으로 전환해야함
  • 작업 완료 후에는 반드시 기본 프레임으로 돌아가야 함
  • iframe이 로드될 때까지 기다려야 할 수 있음 (아래 코드에서 WebDriverWait 사용)
<iframe id="entryIframe" src="페이지주소">
    <!-- iframe 내부의 콘텐츠 -->
</iframe>

 

네이버 지도는 메인 페이지 안에 가게 상세 정보를 iframe으로 보여주는 구조를 사용하고 있어서, 크롤링 시 이 iframe으로의 전환이 필요하다. iframe으로 아래 창(entryIframe)으로 전환한다.

 

 

# iframe이 나타날 때까지 최대 10초 기다림
iframe = wait.until(EC.presence_of_element_located((By.ID, "entryIframe")))

# 찾은 iframe으로 드라이버의 컨텍스트 전환
driver.switch_to.frame(iframe)

 

Selenium의 By.ID를 통해 "entryframe"요소가 나올때 까지 대기한다.

  • EC: Expected Conditions의 약자
  • presence_of_element_located: 요소가 DOM에 존재하는지 확인하는 메서드
  • By.ID: 요소를 찾을 방법 (ID로 찾기)
  • "entryIframe": 찾고자 하는 요소의 ID 값

 

 

 

STEP3.데이터를 수집한다

base_info = get_base_info(driver, wait)  # 홈탭 정보
review_info = get_review_info(driver, wait)  # 리뷰탭 정보

 

두개의 별도 함수를 통해 기본 정보와 리뷰 정보를 수집했다.

따로 설명하진 않겠다.

selenuim을 사용해, HTML코드를 분석해 크롤링했다.

##### 탭 별로 크롤링하는 함수 정의


# 1. 홈 탭의 정보 저장
def get_base_info(driver, wait):
    
    try:
        # # 홈 탭 클릭
        # home_tab = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "a._tab-menu[href*='/home']")))
        # driver.execute_script("arguments[0].click();", home_tab)
        # time.sleep(3)  # 페이지 로딩을 위한 대기 시간
        
        # 기본 정보 수집
        base_info = {
            "store_name": safe_find_element(driver, By.CLASS_NAME, "GHAhO"),
            # "address": safe_find_element(driver, By.CLASS_NAME, "LDgIH"),
            "business_type": safe_find_element(driver, By.CLASS_NAME, "lnJFt"),
            "rating": safe_find_element(driver, By.CSS_SELECTOR, "span.PXMot.LXIwF").replace("별점", "").strip(),
            "visitor_reviews": safe_find_element(driver, By.CSS_SELECTOR, "a[href*='/review/visitor']").replace("방문자 리뷰 ", ""),
            "blog_reviews": safe_find_element(driver, By.CSS_SELECTOR, "a[href*='/review/ugc']").replace("블로그 리뷰 ", ""),
            "location_info": safe_find_element(driver, By.CLASS_NAME, "zPfVt"),
            "phone": safe_find_element(driver, By.CLASS_NAME, "xlx7Q"),
            "Amenities and Services": safe_find_element(driver, By.CLASS_NAME, "xPvPE")
        }
        
        
        # 영업 시간 수집
        try:
            # 요일별 영업시간 수집
            business_hours = {}
            
        #     # 영업시간 버튼 클릭 (펼치기)
        #     try:
        #         business_button = driver.find_element(By.CSS_SELECTOR, "a.gKP9i.RMgN0")
        #         driver.execute_script("arguments[0].click();", business_button)
        #         time.sleep(1)  # 클릭 후 잠시 대기
        #     except:
        #         pass
            
        #     # 요일별 영업시간 요소들 찾기
        #     time_elements = driver.find_elements(By.CSS_SELECTOR, "div.w9QyJ:not(.vI8SM):not(.yN6TD) span.A_cdD")
            
        #     for element in time_elements:
        #         try:
        #             day = element.find_element(By.CLASS_NAME, "i8cJw").text
        #             hours = element.find_element(By.CLASS_NAME, "H3ua4").text
        #             business_hours[day] = hours
        #         except Exception as e:
        #             continue

            # 정기 휴무일 정보 수집 (CSS 선택자 수정)
            try:
                holiday_element = driver.find_element(By.CSS_SELECTOR, "div.w9QyJ.yN6TD span.A_cdD")
                holiday_info = holiday_element.text
            except:
                holiday_info = "정보 없음"

            # # 정렬된 영업시간 정보를 base_info에 추가
            # day_order = ['월', '화', '수', '목', '금', '토', '일']
            # ordered_business_hours = {}
            # for day in day_order:
            #     ordered_business_hours[day] = business_hours.get(day, '영업시간 정보 없음')

            # base_info["business_hours"] = ordered_business_hours
            
            # if holiday_info and holiday_info != "정보 없음":
            #     base_info["holiday_info"] = holiday_info
            # else:
            #     base_info["holiday_info"] = "휴무일 정보 없음"

        except Exception as e:
            # print(f"영업 시간 수집 중 에러: {str(e)}")
            # base_info["business_hours"] = {}
            base_info["holiday_info"] = "정보 없음"
            
        return base_info
        
    except Exception as e:
        print(f"기본 정보 수집 중 에러: {str(e)}")
        return None
    


# 2. 리뷰 탭 정보 저장

def get_review_info(driver, wait):
    try:
        # 리뷰 탭 클릭
        review_tab = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "a._tab-menu[href*='/review']")))
        driver.execute_script("arguments[0].click();", review_tab)
        time.sleep(0.2)  # 탭 전환 대기 0.5초 - 매우 빠름 / 3초 - 안정적
        
        review_info = {
            "menu_keywords": [],
            "characteristic_keywords": [],
            "reviews": [],
            "companions": []
        }
        
        # 1. 메뉴 키워드 수집 
        try:
            menu_keywords = driver.find_elements(By.XPATH, "//span[contains(@class, 'TT3eD') and text()='메뉴']/following-sibling::span//a[@class='T00ux']/span[1]")
            review_info["menu_keywords"] = [keyword.text for keyword in menu_keywords if keyword.text.strip()]

        except Exception as e:
            print(f"메뉴 키워드 수집 중 에러: {str(e)}")
            
        # 2. 특징 키워드 수집
        try:
            characteristic_keywords = driver.find_elements(By.XPATH, "//span[contains(@class, 'TT3eD') and text()='특징']/following-sibling::span//a[@class='T00ux']/span[1]")
            review_info["characteristic_keywords"] = [keyword.text for keyword in characteristic_keywords if keyword.text.strip()]

        except Exception as e:
            print(f"특징 키워드 수집 중 에러: {str(e)}")
            
        # 3. 리뷰 수집 (10개)
        try:
            reviews = []
            while len(reviews) < 5:
                # 현재 페이지의 리뷰 수집
                current_reviews = driver.find_elements(By.CSS_SELECTOR, "div.pui__vn15t2 a.pui__xtsQN-")
                
                # 새로운 리뷰 추가
                for review in current_reviews:
                    if review.text.strip() not in reviews:  # 중복 제거
                        reviews.append(review.text.strip())
                
                # 목표 개수 달성했으면 종료
                if len(reviews) >= 5:
                    break
                    
                try:
                    # 더보기 버튼 찾기 및 클릭
                    more_button = driver.find_element(By.CSS_SELECTOR, "a.fvwqf")
                    if more_button.is_displayed():
                        driver.execute_script("arguments[0].click();", more_button)
                        time.sleep(0.5)  # 로딩 대기
                    else:
                        break  # 더보기 버튼이 없으면 종료
                except:
                    break  # 더보기 버튼을 찾을 수 없으면 종료
            
            review_info["reviews"] = reviews[:30]  # 최대 30개까지만 저장
            # review_info["total_reviews"] = len(reviews)  # 실제 수집된 리뷰 수 저장
            
        except Exception as e:
            print(f"리뷰 수집 중 에러: {str(e)}")
            
        # 4. 동행자 정보 수집
        try:
            companions = driver.find_elements(By.CSS_SELECTOR, "span.pui__V8F9nN em")
            companion_texts = []
            for companion in companions:
                companion_text = companion.text.strip()
                if companion_text and not any(keyword in companion_text for keyword in ["예약", "대기", "바로","이내"]):
                    companion_texts.append(companion_text)
            review_info["companions"] = list(set(companion_texts))  # 중복 제거
        except Exception as e:
            print(f"동행자 정보 수집 중 에러: {str(e)}")
        
        return review_info
    
    except Exception as e:
        print(f"리뷰 정보 수집 중 에러 발생: {str(e)}")
        return None

 

def safe_find_element(driver, by, value):
    try:
        # 1. 최대 1초 동안 요소를 찾기를 시도
        element = WebDriverWait(driver, 1).until(
            EC.presence_of_element_located((by, value))
        )
        # 2. 찾은 요소의 텍스트 반환
        return element.text
    except:
        # 3. 요소를 찾지 못하면 "정보 없음" 반환
        # 누락된 정보 또한 안전하게 처리
        return "정보 없음"

 

safe_find_element함수는 웹 요소를 안전하게 찾고 텍스트를 추출하는 함수이다.

  • driver: Selenium WebDriver 인스턴스
  • by: 요소 찾기 방법 (By.ID, By.CLASS_NAME 등)
  • value: 찾을 요소의 식별자
#사용예시

# 가게 이름 찾기
store_name = safe_find_element(driver, By.CLASS_NAME, "GHAhO")

# 전화번호 찾기
phone = safe_find_element(driver, By.CLASS_NAME, "xlx7Q")

 

 

 

 

STEP4. 데이터를 저장한다.

original_data = url_data['original_data'].split(',')[0] #가맹점명 추출
place_data = {
    "original_data": original_data, #가맹점명
    "base_info": base_info, #홈탭정보
    "review_info": review_info #리뷰탭 정보
}
all_places_data.append(place_data) #데이터저장

 

원본데이터는  아래Json형식과 같다.

 

{
        "original_data": "한그릇, 단품요리 전문, 제주 서귀포시 색달동 2315-1번지 한그릇",
        "search_term": "한그릇 색달동",
        "url": "https://map.naver.com/p/search/%ED%95%9C%EA%B7%B8%EB%A6%87%20%EC%83%89%EB%8B%AC%EB%8F%99"
    }

 

따라서, 원본데이터에서 첫번째 쉼표까지 텍스트만 추출해서 original_data에 가맹점명을 넣어준다.

 

 

STEP5. 예외처리 및 결과저장

try:
    # 크롤링 코드
except Exception as e:
    print(f"⚠ {idx}번째 URL 크롤링 실패: {str(e)}")
    continue
    
    
if all_places_data:
    save_multiple_to_json(all_places_data)
finally:
    if driver:
        driver.quit()

 

개별 URL의 크롤링 실패 시에도 전체 프로세스를 계속 진행하기 위한 코드이다.

에러 메세지(e)를 출력한 뒤 계속 진행한다.

수집된 데이터는 Json 파일로 저장한다.