rubus0304 님의 블로그

[프로젝트 2일차] 본문

Data Analyst/daily

[프로젝트 2일차]

rubus0304 2025. 1. 8. 20:41

1. 네이버 지도 웹크롤링

2. 반려동물 동반여행 API 오류 수정 

 

 

1. 네이버 지도 웹크롤링

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
import json
import time

from time import sleep
import random
import re

# 크롤링 데이터 저장용 리스트
store_data = []

def switch_left():
    driver.switch_to.parent_frame()
    iframe = driver.find_element(By.XPATH, '//*[@id="searchIframe"]')
    driver.switch_to.frame(iframe)

def switch_right():
    driver.switch_to.parent_frame()
    iframe = driver.find_element(By.XPATH, '//*[@id="entryIframe"]')
    driver.switch_to.frame(iframe)

options = webdriver.ChromeOptions()
options.add_argument('user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3')
options.add_argument('window-size=1380,900')
driver = webdriver.Chrome(options=options)

# 네이버 지도 URL
URL = 'https://map.naver.com/p/search/%EB%8C%80%EC%A0%84%20%EC%A4%91%EA%B5%AC%20%EC%8B%9D%EB%8B%B9?c=10.00,0,0,0,dh'
driver.get(url=URL)
driver.implicitly_wait(3)

def scroll_to_bottom():
    last_height = driver.execute_script("return document.body.scrollHeight")  # 초기 높이 가져오기
    while True:
        # 페이지 끝까지 스크롤
        driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
        sleep(2)  # 로딩 대기
        new_height = driver.execute_script("return document.body.scrollHeight")  # 새로운 높이 가져오기
        if new_height == last_height:  # 높이가 더 이상 변하지 않으면 종료
            break
        last_height = new_height

total_elapsed_time = 0

while True:
    switch_left()
   
    ############## 맨 밑까지 스크롤 ##############
    scrollable_element = driver.find_element(By.CLASS_NAME, "Ryr1F")
    last_height = driver.execute_script("return arguments[0].scrollHeight", scrollable_element)
    while True:
        # 요소 내에서 아래로 600px 스크롤
        driver.execute_script("arguments[0].scrollTop += 600;", scrollable_element)

        # 페이지 로드를 기다림
        sleep(1)  # 동적 콘텐츠 로드 시간에 따라 조절

        # 새 높이 계산
        new_height = driver.execute_script("return arguments[0].scrollHeight", scrollable_element)

        # 스크롤이 더 이상 늘어나지 않으면 루프 종료
        if new_height == last_height:
            break

        last_height = new_height

    # 다음 페이지 확인
    next_page = driver.find_element(By.XPATH, '//*[@id="app-root"]/div/div[2]/div[2]/a[7]').get_attribute('aria-disabled')
    if next_page == 'true':
        break

    # 페이지의 가게 리스트
    page_no = driver.find_element(By.XPATH, '//a[contains(@class, "mBN2s qxokY")]').text
    # 40번째 index부터 크롤링
    elements = driver.find_elements(By.XPATH, '//*[@id="_pcmap_list_scroll_container"]//li')[40:] if page_no == '1' else driver.find_elements(By.XPATH, '//*[@id="_pcmap_list_scroll_container"]//li')
    for index, e in enumerate(elements, start=1):
       
        is_ad = False
   
        try:
            ad_tag = e.find_element(By.XPATH, './/span[@class="place_blind" and text()="광고"]')
            if ad_tag:
                is_ad = True
        except:
            pass
       
        # 데이터 초기화
        store_info = {
            'index' : index,
            'page' : page_no,
            'store_name' : '',
            'category' : '',
            'is_ad' : is_ad,
            # 'new_open' : '',
            'rating' : 0.0,
            'visited_review' : 0,
            'blog_review' : 0,
            'address' : '',
            'business_hours' : [],
            'phone_num' : '',
            'menu' : [],
            'top_reviews' : [],
            'theme_keywords' : {'분위기': [], '인기토픽': [], '찾는목적': []},
        }
       
        start_time = time.time()

        # 가게 클릭 및 상세 페이지 이동
        e.find_element(By.CLASS_NAME, 'CHC5F').find_element(By.XPATH, ".//a/div/div/span").click()
        sleep(2)
        switch_right()
       

        try:
            # 상세 정보 크롤링
            title = driver.find_element(By.XPATH, '//div[@class="zD5Nm undefined"]')
            store_info['store_name'] = title.find_element(By.XPATH, './/div[1]/div[1]/span[1]').text
            store_info['category'] = title.find_element(By.XPATH, './/div[1]/div[1]/span[2]').text
           
            # 새로 오픈
            # if len(title.find_elements(By.XPATH, './/div[1]/div[1]/span')) > 2:
            # store_info['new_open'] = title.find_element(By.XPATH, './/div[1]/div[1]/span[3]').text

            # 리뷰 정보
            review_elements = title.find_elements(By.XPATH, './/div[2]/span')
            if len(review_elements) > 2:
                store_info['rating'] = review_elements[0].text
                store_info['visited_review'] = review_elements[1].text
                store_info['blog_review'] = review_elements[2].text
           
            elif len(review_elements) > 1:
                store_info['visited_review'] = review_elements[0].text
                store_info['blog_review'] = review_elements[1].text
           
            # 가게 주소 및 영업 시간
            store_info['address'] = driver.find_element(By.XPATH, '//span[@class="LDgIH"]').text
            try:
                driver.find_element(By.XPATH, '//div[@class="y6tNq"]//span').click()
                sleep(2)
                parent_element = driver.find_element(By.XPATH, '//a[@class="gKP9i RMgN0"]')
                child_elements = parent_element.find_elements(By.XPATH, './*[@class="w9QyJ" or @class="w9QyJ undefined"]')
                store_info['business_hours'] = [child.text for child in child_elements]
                store_info['phone_num'] = driver.find_element(By.XPATH, '//span[@class="xlx7Q"]').text
            except:
                pass

            # 메뉴 정보
            scroll_to_bottom()
            sleep(2)
            menu_elements_1 = driver.find_elements(By.XPATH, '//ul[contains(@class, "jnwQZ")]/li')
            menu_elements_2 = driver.find_elements(By.XPATH, '//ul[contains(@class, "t1osG")]/li')
           
            store_info['menu'] = []
           
            # 첫 번째 구조 처리
            for menu_element in menu_elements_1:
                    name = menu_element.find_element(By.XPATH, './/a').text.strip()
                    price = menu_element.find_element(By.XPATH, './/div[@class="mkBm3"]').text.strip()
                    store_info['menu'].append({"name": name, "price": price})

            # 두 번째 구조 처리
            for menu_element in menu_elements_2:
                    name = menu_element.find_element(By.XPATH, './/span[@class="VQvNX"]').text.strip()
                    price = menu_element.find_element(By.XPATH, './/div[@class="gl2cc"]').text.strip()
                    store_info['menu'].append({"name": name, "price": price})

            # 방문자 리뷰
            scroll_to_bottom()
            sleep(2)
            review_elements = driver.find_elements(By.XPATH, '//ul[@class="K4J9r"]')
            store_info['top_reviews'] = [review_element.text for review_element in review_elements[:5]]

            # 테마 키워드
            scroll_to_bottom()
            sleep(2)
            theme_elements = driver.find_elements(By.XPATH, '//ul[contains(@class, "v4tIa")]/li')

            # 각 테마 키워드 요소를 순회
            for theme_element in theme_elements:
                # 카테고리 이름 추출
                category_name = theme_element.find_element(By.CLASS_NAME, 'pNnVF').text.strip()

                # 키워드 추출
                keyword_container = theme_element.find_element(By.CLASS_NAME, 'sJgQj')
                keywords = keyword_container.find_elements(By.TAG_NAME, 'span')
                keyword_texts = [keyword.text.strip() for keyword in keywords]

                # 카테고리에 맞게 키워드 추가
                if category_name in store_info["theme_keywords"]:
                    store_info["theme_keywords"][category_name].extend(keyword_texts)
                else:
                    print(f"알 수 없는 카테고리: {category_name}")

        except Exception as ex:
            print('------------ 데이터 크롤링 오류 ------------' )
            print(ex)
           
        store_data.append(store_info)
       
        switch_left()
        sleep(2)

        # JSON 형식으로 저장
        with open("store_data.json", "w", encoding="utf-8") as f:
            json.dump(store_data, f, ensure_ascii=False, indent=4)

        # 출력 (테스트용)
        print(json.dumps(store_info, ensure_ascii=False, indent=4))
       
        elapsed_time = time.time() - start_time  # 소요 시간 계산
        total_elapsed_time += elapsed_time  # 누적 시간 갱신
        print(f"Crawling Time: {elapsed_time:.2f} seconds")  # 소요 시간 출력
        print(f"Total Elapsed Time: {total_elapsed_time:.2f} seconds")  # 누적 시간 출력
       
    # 다음 페이지로 이동
    if next_page == 'false':
        driver.find_element(By.XPATH, '//*[@id="app-root"]/div/div[2]/div[2]/a[7]').click()
    else:
        break

 

 

 

 

2. 반려동물 동반여행 API 오류 수정 

한국관광공사_반려동물_동반여행_서비스 | 공공데이터포털

 

한국관광공사_반려동물_동반여행_서비스

반려동물 동반여행 가능한 관광지, 문화시설, 축제공연행사, 숙박, 음식점, 레포츠, 쇼핑의 관광정보를 제공한다.

www.data.go.kr

 

import requests
import pandas as pd
import pprint
import json

#내 인증키: SB%2FBZuuT7g6hbj6MdS5rIw%2BlSO1MJ7Lm%2FTxPV1AFLK4%2B2MF%2B3cFhbOIaexaDbRR9nHUqNfgIMEeCOmgsMSOPHA%3D%3D
 

 

import requests
import pandas as pd

# 수정된 URL
url = (
    "?serviceKey=SB%2FBZuuT7g6hbj6MdS5rIw%2BlSO1MJ7Lm%2FTxPV1AFLK4%2B2MF%2B3cFhbOIaexaDbRR9nHUqNfgIMEeCOmgsMSOPHA%3D%3D"
    "&numOfRows=1000&pageNo=1&MobileOS=ETC&MobileApp=TestApp&_type=json"
)

# 요청 보내기
response = requests.get(url)

# 응답 처리
if response.status_code == 200:
    data = response.json()
    print("API 호출 성공!")

    # 'item' 데이터 추출 및 DataFrame 변환
    if 'response' in data and 'body' in data['response'] and 'items' in data['response']['body']:
        items = data['response']['body']['items']['item']
        df = pd.DataFrame(items)
        print(df.head())  # 데이터프레임 확인

        # 데이터 저장
        df.to_csv("pet_tour_data.csv", index=False)
        print("데이터가 'pet_tour_data.csv'로 저장되었습니다.")
    else:
        print("응답 데이터에 'items'가 없습니다.")
else:
    print(f"API 요청 실패: {response.status_code}")
    print(response.text)  # 오류 메시지 출력
 

 

print(response.text)  # 응답 원문 출력
 

 

# 공통 인증키 및 기본 URL
AUTH_KEY = "0s/7WTcuzqAM59kXm92atnW6fm+lyzyIIpf3ALoRQmWxLj0DpfUVACTL3zZwDja049OT2hTuGj95TLmjTflKBw=="

# 데이터 저장을 위한 리스트
all_data = []
 

 

 

# 이미지 정보 조회
url = f"{BASE_URL}/detailImage"
params = {
    "serviceKey": AUTH_KEY,
    "numOfRows": 1000,
    "pageNo": 1,
    "MobileOS": "ETC",
    "MobileApp": "TestApp",
    "_type": "json",
    "contentId": 2627867
}

response = requests.get(url, params=params)
if response.status_code == 200:
    data = response.json()
    print(data)
    if 'items' in data.get('response', {}).get('body', {}):
        items = data['response']['body']['items']['item']
        df_images = pd.DataFrame(items)
        print("이미지 정보 데이터프레임:\n", df_images.head())
        all_data.append(("이미지 정보", df_images))
 

 

** result OK  그러나 오류남.

 

# 서비스 분류 코드 조회
url = f"{BASE_URL}/categoryCode"
params = {
    "serviceKey": AUTH_KEY,
    "MobileOS": "ETC",
    "MobileApp": "TestApp",
    "_type": "json"
}

response = requests.get(url, params=params)
if response.status_code == 200:
    data = response.json()
    if 'items' in data.get('response', {}).get('body', {}):
        items = data['response']['body']['items']['item']
        df_category = pd.DataFrame(items)
        print("서비스 분류 코드 데이터프레임:\n", df_category.head())
        all_data.append(("서비스 분류 코드", df_category))
else : print(response.status_code)
 

 

 

# 지역 기반 관광 정보 조회
url = f"{BASE_URL}/areaBasedList"
headers = {'Accept': 'application/json'}
params = {
    "serviceKey": AUTH_KEY,
    "numOfRows": 1000,
    "pageNo": 1,
    "MobileOS": "ETC",
    "MobileApp": "TestApp",
    "_type": "json",
    "arrange": "A",  # 제목순
    "contentTypeId": "",  # 콘텐츠 타입 지정 가능 (예: 숙박, 관광지 등)
    "areaCode": "",       # 지역 코드
    "sigunguCode": ""     # 시군구 코드
}

response = requests.get(url, params=params)
if response.status_code == 200:
    print(response.text)
    data = response.json()
       
    if 'items' in data.get('response', {}).get('body', {}):
        items = data['response']['body']['items']['item']
        df_area = pd.DataFrame(items)
        print("지역 기반 관광 정보 데이터프레임:\n", df_area.head())
        all_data.append(("지역 기반 관광 정보", df_area))
 

 

 

# 위치 기반 관광 정보 조회
url = f"{BASE_URL}/locationBasedList"
headers = {'Accept': 'application/json'}
params = {
    "serviceKey": AUTH_KEY,
    "numOfRows": 1000,
    "pageNo": 1,
    "MobileOS": "ETC",
    "MobileApp": "AppTest",
    "_type": "json",
    "arrange": "C",  # 거리순
    "mapX": 126.981611,
    "mapY": 37.568477,
    "radius": 1000,
    "contentTypeId": "",  # 콘텐츠 타입 지정 가능 (예: 숙박, 관광지 등)
    "areaCode": "",       # 지역 코드
    "sigunguCode": ""     # 시군구 코드
}

response = requests.get(url, params=params)
if response.status_code == 200:
    print(response.text)
    data = response.json()
       
    if 'items' in data.get('response', {}).get('body', {}):
        items = data['response']['body']['items']['item']
        df_area = pd.DataFrame(items)
        print("위치 기반 관광 정보 데이터프레임:\n", df_area.head())
        all_data.append(("위치 기반 관광 정보", df_area))
 

 

 

**  위치기반 관광 정보 조회/  이미지 조회  수정 필요! 

 

 

실행 및 진행 사항 정리

 

수집한 데이터 (식당, 카페): 네이버 지도 웹 크롤링 진행중 (구역구 나눠서) 

 

- Nan 값 나오는 경우:

  1. 별점이 없는 식당 다수 존재 가능성 있음

      -> 분포 확인 후 평균이나, 중앙값으로 대치

      -> 요즘 뜨는 으로 검색했으니 앞뒤 평점 확인하여 적당한 값 넣기

      -> 데이터의 양이 많은 경우, 회귀 예측 모델 시도해보기.

 

  2. 키워드 없는 식당 존재

      -> 다이닝코드에서 태그 가져오기

      -> 리뷰텍스트 기반

      -> 카테고리 기반으로 분류?

      -> 빵지순례, 로컬맛집 태그 넣어주기 (다이닝 코드나 다른 사이트 참고하여 리스트 만듣 뒤 해당하는 것에 매핑)

 

 

- 수집할 데이터 

데이터 수집: 지역 구별로 맡은 식당, 카페, 숙소 웹크롤링 진행

- 숙소 웹크롤링 코드 짜기

- 크롤링 하는 동안 다른 일정이나 추가 계획 세우기.

 

- 역할 분배하기

웹크롤링 - 준호

전처리 - 종학

API - 은혜 대전지역 모아서 가져오는 방법 찾아보기

추가 다이닝 코드, 다른 사이트에서 리스트 가져오기 (빵지순례, 로컬맛집) - 예림 

 

오전 - 웹크롤링 (카페, 식당) 

오후 - 전처리 / (은혜 대전지역 모아서 가져오는 방법 찾기)

 

 

 

 

 

숙소 

 

이름, 카테고리, 별점, 방문자 리뷰, 블로그 리뷰,  주소, 전화번호, 홈페이지, 가격비교, 시설정보, 방문자 리뷰 전체 (키워드 대체), 

 

구 나눠서 (여관, 기타숙박업 제외), 

 

ex) 대전 중구 호텔/ 모텔/ 게스트하우스/ 펜션 

 

 

 

 

 

'Data Analyst > daily' 카테고리의 다른 글

[최종프로젝트 1일차]  (0) 2025.01.07
[데이터 파이프라인 1강]  (0) 2025.01.07
[라이브세션] '5분 기록 보드'로 20시간 절약하기  (0) 2025.01.06
[QCC 5회차]  (1) 2025.01.03
[프로젝트 준비기간2]  (0) 2025.01.02