본문 바로가기
개발일지/개인 프로젝트

프로젝트 첫번째. 스타벅스, 이디야의 시각화 기본데이터 스크래핑 후 엑셀저장

by 개발에정착하고싶다 2022. 7. 20.

태블루 과제 때문에 없는 데이터 만드느라고 하루를 보냈지만 암튼 끝냈다.

새롭게 하니, 이젠 딱히 어렵진 않지만 마냥 쉽지만도 않다. 적당 난이도 느낌?

 

중간에 난제도 몇가지 있긴했다.

1. '입력값이 너무 많습니다'에 대한 에러 처리

 - 구글링이든 어디든 그냥 알럿끄기에만 급급하지, 데이터에 구멍이 생기는걸 신경쓰는건 유튜버 한분 뿐이였다. (빅공잼 님)

2. 이디야는 위도, 경도가 듬성듬성 있었기 때문에 googlemaps 이용해서 위도 경도 따오기

 

이 두개 외에는 적당 난이도였다고 본다.

 

혹시라도 고수 분들이 코드를 보신다면 궁금한 부분이 있습니다.

 

1. 파이썬 정규표현식을 사용하여 혹은 어떤 방법으로든 주소 값에

(망우동) 1544-3512 라고 나올때, 스타벅스의 경우는 다행히도 같은 번호여서 제거가 replace로 쉬웠습니다.

하지만 1000개가 넘는 데이터의 전화번호가 모두 다를때를 대비해서

(로 시작 되는 것이 발견되면 (의 앞에 공백까지 포함하여 맨 뒤까지 싹 날려버리는 방법으로 좋은게 없을까요?

 

예: 서울 중랑구 망우로 460 (망우동) 1544-5266

제가 원하는 것: 서울 중랑구 망우로 460

 

이것을 슬라이스로 대략 (앞까지 끊어서 출력도 해보았습니다만, 값이 수백개가 되다보니 당연히 더 값이 긴것도 생기게 되고

그러다보니 '(망우' 이런식으로 잘리기도 하더라구요.

 

좀 더 나아가서

 

서울 중랑구 망우로 460, 1층

이런식의 주소도 있었는데, 이런 경우는 , 이후의 글자를 모두 날려버리는 방법도 알려주시면 감사하겠습니다.

 

댓글 주시면 감사하겠습니다!!

 



코드 나열은 시도했던 것도 포함하여 70% 정도로 주석과 함께

고민했던 과정을 표현해 보았다.

 

# 스타벅스

# Starbucks data

from selenium import webdriver
from bs4 import BeautifulSoup
from selenium.webdriver.common.by import By
from tqdm import tqdm
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

import chromedriver_autoinstaller
import pandas as pd
import time
import re

# chrome driver 생성
chromedriver_autoinstaller.install()
url = 'https://www.starbucks.co.kr/store/store_map.do'
driver = webdriver.Chrome('/Users/daniel_choi/miniforge3/envs/ds_study/lib/python3.10/site-packages/chromedriver_autoinstaller/103/chromedriver')
driver.get(url)
# 비는 부분이 없도록 최대화
driver.maximize_window()
# 매장찾기 - 지역 검색 탭 클릭
driver.find_elements(By.CSS_SELECTOR, 'header.loca_search > h3 > a')[0].click()
# 지역 검색 중 "서울" 선택

# 확인 코드
# driver.find_elements(By.CSS_SELECTOR, 'ul.sido_arae_box > li')[0].text
# '서울'
driver.find_elements(By.CSS_SELECTOR, 'ul.sido_arae_box > li')[0].click()
# 서울 에서 "전체"클릭

# 확인코드
# driver.find_elements(By.CSS_SELECTOR, 'ul.gugun_arae_box > li')[0].text
# '전체'
driver.find_elements(By.CSS_SELECTOR, 'ul.gugun_arae_box > li')[0].click()
# 서울 전체 검색결과가 22.07.20 기준으로 585개다.

# 확인 1 (갯수)
# len(driver.find_elements(By.CSS_SELECTOR, 'ul.quickSearchResultBoxSidoGugun > li'))
# 585

# 확인 2 (처음 text)
# driver.find_elements(By.CSS_SELECTOR, 'ul.quickSearchResultBoxSidoGugun > li')[0].text
# '역삼아레나빌딩\n서울특별시 강남구 언주로 425 (역삼동)\n1522-3232\n리저브 매장 2번'

# 확인 3 (마지막 text)
# driver.find_elements(By.CSS_SELECTOR, 'ul.quickSearchResultBoxSidoGugun > li')[-1].text
# '' 라고 나온다. 뭔가 잘못되었다.

# 확인 4 (마지막 text)를 체크했을 때, 확인 3이 안먹어서
# driver.find_elements(By.CSS_SELECTOR, 'ul.quickSearchResultBoxSidoGugun > li')[184].text
# 페이지에서 보이질 않기때문에 긁어올 수 없는것일까? 184번 인덱스 (185번째)의 것도 ''로 표기가 된다.

# 확인 5
# driver는 보이는 페이지에 국한해서 나올 수 있기 때문에 Beautifulsoup로 시도해주자.
html = driver.page_source
soup = BeautifulSoup(html, 'html.parser')
len(soup.select('ul.quickSearchResultBoxSidoGugun > li'))
# 585
main = soup.select('ul.quickSearchResultBoxSidoGugun > li')
main[-1]
'''
<li class="quickResultLstCon" data-code="3801" data-hlytag="null" data-index="584" data-lat="37.60170912407773" data-long="127.07841136432036" data-name="중화역" data-storecd="1749" style="background:#fff"> <strong data-my_siren_order_store_yn="N" data-name="중화역" data-store="1749" data-yn="N">중화역  </strong> <p class="result_details">서울특별시 중랑구 봉화산로 35 <br/>1522-3232</p> <i class="pin_general">리저브 매장 2번</i></li>
'''

# ok 이걸로 진행하자. 0번 인덱스의 값도 이상없다.
# 정규표현식으로 내가 원하는 값을 제거해보려 했지만, 내가 원하는 값은 아니였다.
# 즉, 코드는 여기선 별반 도움이 안되었다.

def clean_text(inputString):
  text_rmv = re.sub('[-=+,#/\?:^.@*\"※~ㆍ!』‘|\(\)\[\]`\'…》\”\“\’·]', ' ', inputString)
  return text_rmv
# 자료 긁어오기
# 확인 1
# 매장 명
store = i('strong')[0].text.strip()
# 매장 주소
# 반복문 버전에 성공했지만, 
# 다음엔 연산을 줄이기 위해 다른 방법을 찾자.
for i in main[0]('p')[0].text.split()[:-1]:
    print(i, end=' ')

# 매장 구
gu = i('p')[0].text.split()[1]
# 매장 전화 번호
# 현재로써는 전화번호는 데이터상 중요하지 않다.
# 하지만 반드시 얻어내자.
# main[0]('p')[0].text.split()[-1:][0]
# '(역삼동)1522-3232'

# 매장 위도 (lat)
lat = i['data-lat']
# 매장 경도 (lng)
lng = i['data-long']
datas = []
for i in tqdm(main):

    # 자료 긁어오기
    # 확인 1
    # 매장 명
    name = i('strong')[0].text.strip()
    # 매장 주소
    # 반복문 버전에 성공했지만, 
    # 다음엔 연산을 줄이기 위해 다른 방법을 찾자.


    # address = [a for a in i('p')[0].text.split()[:-1]]
    address = i('p')[0].text.replace('1522-3232', '')

    gu = i('p')[0].text.split()[1]
    # 매장 전화 번호
    # 현재로써는 전화번호는 데이터상 중요하지 않다.
    # 하지만 반드시 얻어내자.
    # main[0]('p')[0].text.split()[-1:][0]
    # '(역삼동)1522-3232'

    # 매장 위도 (lat)
    lat = i['data-lat']
    # 매장 경도 (lng)
    lng = i['data-long']

    # pandas DataFrame을 위해서 이렇게 미리 지정하여 만들어 준다.
    data = {
        '매장이름':name,
        '주소':address,
        '구':gu,
        '위도':lat,
        '경도':lng,
        '브랜드':'스타벅스'
    }
    datas.append(data)
    
print(datas)
# 주소 테스트 코드
soup.select('div#mCSB_3_container > ul > li')[0]('p')[0].text[:-15]
# '서울특별시 강남구 언주로 425'
# 이게 올바르지 않다고 생각한다. 근본적으로 주소에 변동성이 많으면 이 코드는 무쓸모다.
# 다음엔 좀 더 나은 방법을 찾아내자.
# 내용 체크

# len(datas)
# 585
datas[0]

'''
{'매장이름': '역삼아레나빌딩',
 '주소': '서울특별시 강남구 언주로 425',
 '구': '강남구',
 '위도': '37.501087',
 '경도': '127.043069'}
'''
df_starbucks = pd.DataFrame(datas)
df_starbucks.to_excel('starbucks_data.xlsx')
# 스타벅스로 볼일 다 봤으니 꺼주자.

driver.quit()


# 이디야

 

# Ediya Data

# import 는 starbucks때 다 해왔으니깐 생략하고 진행 ㄱㄱ

new_url = 'https://www.ediya.com/contents/find_store.html'
# 이 변수가 이미 선언 되어있다 하더라도 같은 셀에 없으면 ConnectionRefusedError 뜨니깐 꼭 같이!
driver = webdriver.Chrome('/Users/daniel_choi/miniforge3/envs/ds_study/lib/python3.10/site-packages/chromedriver_autoinstaller/103/chromedriver')
driver.get(new_url)
# 화면 최대화
driver.maximize_window()
# '주소'클릭
driver.find_elements(By.CSS_SELECTOR,'#contentWrap > div.contents > div > div.store_search_pop > ul > li:nth-child(2) > a')[0].click()
# 검색을 위한 구 리스트 만들기
# 스타벅스 만들때 활용한 데이터 이용

gu_list = df_starbucks.구.unique()
html = driver.page_source
soup = BeautifulSoup(html, 'html.parser')
# 이디야 지역 스크래핑을 위한 메인 변수 설정
main = soup.select('ul#placesList >li.item')
# 각 구별로 매장 수가 다를테니, main을 재설정 해주는 것을 생각해보자.

# 매장 명
# main[0]('dt')[0].text

# 매장 주소
main[0]('dd')[0].text[:-6]
# '서울 중랑구 망우로 460'
# 주소 값에다가 서울 각 구 이름 넣어서 빼주기

# 테스트 1 (값 보내기)
driver.find_elements(By.CSS_SELECTOR, '#keyword')[0].send_keys('강남구')

# 테스트 2 (기존 값 지우기)
driver.find_elements(By.CSS_SELECTOR, '#keyword')[0].clear()

# 테스트 3 (엔터키 누르기)
driver.find_elements(By.CSS_SELECTOR, '#keyword')[0].send_keys(Keys.ENTER)
# 위도, 경도를 뽑아내고 싶었지만, 있는것도 있고 없는것도 있고 빈도가 듬성듬성해서 googlemap을 이용하기로 결정함
main[1]('a')[0]['onclick']
# "panLatTo('127.094182772191','37.5896269575279','1');fnMove();"
# 반복문 테스트 (위도, 경도는 추후 넣기위해 나머지 3개)

ediya_datas = []
for i in tqdm(gu_list):
        
    # 기존 입력코드, 시도 실패.
    # UnexpectedAlertPresentException: Alert Text: 검색 결과가 너무 많습니다. 범위를 좁혀 주시기 바랍니다.
    # 라는 에러가 떴다. 
    # 따라서 대기시간, 입력시간에 문제가 있다고 판단하고, 이를 보완하기 위해서 코드를 변경해줬다.

    '''
    # (기존 값 지우기)
    driver.find_elements(By.CSS_SELECTOR, '#keyword')[0].clear()
    # (값 보내기)
    driver.find_elements(By.CSS_SELECTOR, '#keyword')[0].send_keys(i)
    # (엔터키 누르기)
    driver.find_elements(By.CSS_SELECTOR, '#keyword')[0].send_keys(Keys.ENTER)
    
    time.sleep(2)
    '''
    
    # 2차시도 기본
    keyword_css = '#keyword'
    
    # 기존 값 지우기
    WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.CSS_SELECTOR, keyword_css))).clear()
    
    # 값 보내기
    # 이게 이번의 핵심 코드였다. ****
    WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.CSS_SELECTOR, keyword_css))).send_keys(f'서울 {i}')
    
    # 엔터키 누르기
    search_css = '#keyword_div > form > button'
    WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.CSS_SELECTOR, search_css))).click()
        
    
    # 각 구마다 매장의 수가 다를테니, 재설정을 해준다.
    main = soup.select('ul#placesList >li.item')
    for a in main:
        
        # 매장 명
        name = a('dt')[0].text

        # 매장 주소
        address = a('dd')[0].text
        
        # 구
        gu = i
        
        data = {
            '매장이름':name,
            '주소':address,
            '구':gu,
            '브랜드':'이디야'
        }
        
        ediya_datas.append(data)

# 계속 해서 UnexpectedAlertPresentException: Alert Text: 검색 결과가 너무 많습니다. 범위를 좁혀 주시기 바랍니다.
# 에러가 떴다. 구글링을 해봐도, 알럿을 무시하는 코드만 나오지, 그것은 내가 원하는것이 아니였다.
# 값에 대해서 누락 없이 가져가고 싶으니깐, 그래서 유튜브 자료로 찾아서 참조했다.
# 참조: https://www.youtube.com/watch?v=qobe7k1CmGc&t=1099s

# 그외 참조는 selenium 공식 문서를 기반으로 만들어 지긴 했는데,
# 유튜브에서 딱 하나지만 '강서구' 를 예로 들었을때, 부산에도 강서구, 서울에도 강서구가 있기 때문에 결과가 많이 나오는 것이였다.
# 그래도 그렇지 그게 많으면 얼마나 많다고 그거에 출력을 제한하냐;
# len(ediya_datas)
# 675

ediya_datas[0]
# {'매장이름': '금란망우점', '주소': '서울 중랑구 망우로 460', '구': '강남구'}
df_ediya = pd.DataFrame(ediya_datas)
df_ediya.tail()


# 이디야 위도, 경도 따주기

# 이디야 위도, 경도 따주기

# 먼저 위도, 경도 컬럼 생성해주기

import numpy as np
df_ediya['위도'] = np.nan
df_ediya['경도'] = np.nan
# 이디야 위도, 경도 따주기

import googlemaps
google_key = '본인의 구글API 값 사용하세염'
gmaps = googlemaps.Client(key=google_key)
gmaps
# 이 자료가 위도, 경도따기의 척도다.
# 이 자료를 토대로 보게되면 위도 경도를 딸 수 있는 루트는 3가지가 있다.
# key값으로는 geometry - location,
# viewport - northeast,
# southwest
# 하지만 이중에서 가장 '짧게'나온 위도 경도는 geometry와 location으로 이루어진 것으로써, 이것을 사용하도록 한다.

for idx, rows in df_ediya.iterrows():
    # rows.주소
    print(gmaps.geocode(rows.주소, language = 'ko'))
    break
    
'''
[{'address_components': [{'long_name': '460', 'short_name': '460', 'types': 
['premise']}, {'long_name': '망우로', 'short_name': '망우로', 'types': 
['political', 'sublocality', 'sublocality_level_4']}, {'long_name': '중랑구', 'short_name': 
'중랑구', 'types': ['political', 'sublocality', 'sublocality_level_1']}, 
{'long_name': '서울특별시', 'short_name': '서울특별시', 'types': ['administrative_area_level_1', 'political']}, 
{'long_name': '대한민국', 'short_name': 'KR', 'types': ['country', 'political']}, 
{'long_name': '131-230', 'short_name': '131-230', 'types': ['postal_code']}], 
'formatted_address': '대한민국 서울특별시 중랑구 망우로 460', 'geometry': {'location': 
{'lat': 37.6000681, 'lng': 127.1031213}, 'location_type': 'ROOFTOP', 'viewport': {'northeast': 
{'lat': 37.6014170802915, 'lng': 127.1044702802915}, 'southwest': {'lat': 37.59871911970851, 
'lng': 127.1017723197085}}}, 'place_id': 'ChIJ6U1Y60G6fDURcbe4fw-yeh8', 'plus_code': 
{'compound_code': 'J423+26 대한민국 서울특별시', 'global_code': '8Q99J423+26'}, 'types': ['street_address']}]
'''
# *** 위도 경도 딸때 등, 응용할때 정말 중요한 부분이다.

for idx, rows in tqdm(df_ediya.iterrows()):
    # rows.주소
    # print(gmaps.geocode(rows.주소, language = 'ko'))
    # break
    
    lat_lng = gmaps.geocode(rows.주소, language = 'ko')
    # lat, lng을 포함하고 있는 딕셔너리의 key 값이 geometry니깐 이걸 써준다.
    lat = lat_lng[0].get('geometry')['location']['lat']
    # 37.6000681
    lng = lat_lng[0].get('geometry')['location']['lng']
    # 127.1031213
    df_ediya.loc[idx, '위도'] = lat
    df_ediya.loc[idx, '경도'] = lng
df_ediya

'''

	매장이름	주소	구	브랜드	위도	경도
0	금란망우점	서울 중랑구 망우로 460 (망우동)	강남구	이디야	37.600068	127.103121
1	동원사거리점	서울 중랑구 겸재로 240 (면목동, 행복오피스텔)	강남구	이디야	37.589608	127.094165
2	망우동점	서울 중랑구 망우로 416 (망우동)	강남구	이디야	37.599128	127.098336
3	망우중앙점	서울 중랑구 용마산로115길 109 (망우동, 한일써너스빌리젠시2단지)	강남구	이디야	37.597644	127.094346
4	먹골역점	서울 중랑구 동일로157길 13 (묵동)	강남구	이디야	37.609701	127.076882
...	...	...	...	...	...	...
670	중랑교차로점	서울 중랑구 동일로 683 (면목동)	중랑구	이디야	37.591397	127.079841
671	중랑역점	서울 중랑구 망우로 198 (상봉동)	중랑구	이디야	37.593212	127.074866
672	중화동점	서울 중랑구 동일로129길 1 (중화동)	중랑구	이디야	37.599293	127.078348
673	중화역점	서울 중랑구 동일로 815, 1층	중랑구	이디야	37.603092	127.078876
674	화랑대역점	서울 중랑구 신내로25가길 2 (묵동, 현동학당)	중랑구	이디야	37.619451	127.084160
675 rows × 6 columns
'''
df_ediya.to_excel('ediya.xlsx')

 

나의 목적은 3가지 데이터 엑셀을 만드는 거였는데, 마무리 되었으니 이제 마치자.