🔎LEARNING POINT🔎
- HTML을 읽고 원하는 정보가 있는 위치 태그를 파악
- BeautifulSoup와 Selenium을 활용한 웹크롤링 방법
- googlemap을 활용하여 지도를 불러와 위치를 표시하는 방법
- 자료로 그래프를 그려 의견 제시
0. 세팅
#필요한 모듈 임포트 및 설치
import pandas as pd
import numpy as np
import time
from bs4 import BeautifulSoup
!pip install selenium
from selenium import webdriver
from selenium.webdriver.common.by import By
%config Completer.use_jedi = False
1) 서울시 내 스타벅스 매장의 이름과 주소, 구 이름을 pandas dataframe으로 정리
[ IN ]
driver = webdriver.Chrome() #셀레니움이 업데이트 되어 크롭 드라이버를 다운받지 않아도 됨
driver.maximize_window()
driver.get('https://www.starbucks.co.kr/store/store_map.do?disp=locale')
time.sleep(2)
#클릭
sido_cd_btn = driver.find_element(By.CSS_SELECTOR, '#container > div > form > fieldset > div > section > article.find_store_cont > article > article:nth-child(4) > div.loca_step1 > div.loca_step1_cont > ul > li:nth-child(1) > a')
sido_cd_btn.click()
time.sleep(2)
gugun_cd_btn = driver.find_element(By.XPATH, '//*[@id="mCSB_2_container"]/ul/li[1]/a' )
gugun_cd_btn.click()
time.sleep(2)
html = driver.page_source
soup = BeautifulSoup(html, "html.parser")
locations = soup.select('#mCSB_3_container > ul > li')
#웹 크롤링 데이터 적재
names = []
address = []
gu_add = []
data_lat = []
data_long=[]
for i in range(len(locations)):
name = locations[i].select_one("strong").text
names.append(name)
addr = locations[i].select_one("p").text
address.append(addr)
gu = locations[i].select_one("p").text.split(" ")[1]
gu_add.append(gu)
lat = locations[i]['data-lat']
data_lat.append(lat)
long = locations[i]['data-long']
data_long.append(long)
time.sleep(3)
driver.quit()
#pandas dataframe으로 스타벅스 매장 정보 가져오기
starbucks = pd.DataFrame({'스타벅스 매장 이름' : names,
'스타벅스 매장 주소' : address,
'구 이름' : gu_add})
print(starbucks)
❗time.sleep을 중간중간에 넣지 않으면, 프로그램마다 속도가 달라서 에러가 뜬다. 코드에서 이상을 못 찾았는데, 자꾸 에러를 떠서 고생하다 time 넣고 다 해결..
생각했던 것보다 자주 넣어야했다. (물론 위 식은 내가 혹시나 해서 과하게 넣은 거일 수도 ㅎㅎ)
[ OUT ]
스타벅스 매장 이름 스타벅스 매장 주소 구 이름
0 역삼아레나빌딩 서울특별시 강남구 언주로 425 (역삼동)1522-3232 강남구
1 논현역사거리 서울특별시 강남구 강남대로 538 (논현동)1522-3232 강남구
2 신사역성일빌딩 서울특별시 강남구 강남대로 584 (논현동)1522-3232 강남구
3 국기원사거리 서울특별시 강남구 테헤란로 125 (역삼동)1522-3232 강남구
4 대치재경빌딩R 서울특별시 강남구 남부순환로 2947 (대치동)1522-3232 강남구
.. ... ... ...
601 사가정역 서울특별시 중랑구 면목로 3101522-3232 중랑구
602 상봉역 서울특별시 중랑구 망우로 307 (상봉동)1522-3232 중랑구
603 묵동 서울특별시 중랑구 동일로 952 (묵동, 로프트원 태릉입구역) 1층1522-3232 중랑구
604 양원역 서울특별시 중랑구 양원역로10길 3 (망우동)1522-3232 중랑구
605 중화역 서울특별시 중랑구 봉화산로 35 1522-3232 중랑구
[606 rows x 3 columns]
2) 서울시 내 이디야 매장의 이름과 주소, 구 이름을 pandas dataframe으로 정리
이디야는 직접 이름을 입력해야 하므로 스타벅스 구 이름을 가져와 사용한다. 이때 중구를 입력할 경우 결과 값이 많아 에러창이 뜨므로, 서울 중구로 검색한다.
[ IN ]
gu_list = list(starbucks['구 이름'].unique())
gu_list.remove('중구')
gu_list.append('서울 중구') #중구를 서울 중구로 변경
gu_list
[ OUT ]
['강남구',
'강북구',
'강서구',
'관악구',
'광진구',
'금천구',
'노원구',
'도봉구',
'동작구',
'마포구',
'서대문구',
'서초구',
'성북구',
'송파구',
'양천구',
'영등포구',
'은평구',
'종로구',
'강동구',
'구로구',
'동대문구',
'성동구',
'용산구',
'중랑구',
'서울 중구']
NEXT STEP 웹 크롤링으로 이디야 정보 가져오기
[ IN ]
#접속
driver = webdriver.Chrome()
driver.maximize_window()
driver.get("https://www.ediya.com/contents/find_store.html")
time.sleep(2)
driver.find_element(By.CSS_SELECTOR, "#contentWrap > div.contents > div > div.store_search_pop > ul > li:nth-child(2) > a").click()
names2 = []
address2 = []
gu_add2 = []
data_lat2 = []
data_long2= []
for i in range(len(gu_list)):
#구 이름 집어넣고 클릭하는 동작
kw = driver.find_element(By.CSS_SELECTOR, "#keyword")
kw.clear()
kw.send_keys(gu_list[i])
btn = driver.find_element(By.CSS_SELECTOR, "#keyword_div > form > button")
btn.click()
time.sleep(2)
html = driver.page_source
soup = BeautifulSoup(html, "html.parser")
locations2 = soup.select('#placesList > li > a')
time.sleep(2)
#주유소 하나씩 정보 스크랩
for i in range(len(locations2)):
name2 = locations2[i].select_one("dt").text
names2.append(name2)
add2 = locations2[i].select_one("dd").text
address2.append(add2)
gu = locations2[i].select_one("dd").text.split(" ")[1]
gu_add2.append(gu)
lat = locations2[i]['onclick'].split('\'')[3]
data_lat2.append(lat)
long = locations2[i]['onclick'].split('\'')[1]
data_long2.append(long)
driver.quit()
#pandas dataframe으로 이디야 매장 정보 가져오기
ediya = pd.DataFrame({'이디야 매장 이름' : names2,
'이디야 매장 주소' : address2,
'구 이름' : gu_add2})
print(ediya)
[ OUT ]
이디야 매장 이름 이디야 매장 주소 구 이름
0 강남YMCA점 서울 강남구 논현동 강남구
1 강남구청역아이티웨딩점 서울 강남구 학동로 338 (논현동, 강남파라곤) 강남구
2 강남논현학동점 서울 강남구 논현로131길 28 (논현동) 강남구
3 강남대치점 서울 강남구 역삼로 415 (대치동, 성진빌딩) 강남구
4 강남도산점 서울 강남구 도산대로37길 20 (신사동) 강남구
.. ... ... ...
631 태평로점 서울 중구 세종대로11길 26 (서소문동) 중구
632 퇴계로2길점 서울 중구 퇴계로2길 1 (남창동) 중구
633 한국은행점 서울 중구 남대문로 29-2 (남대문로3가) 중구
634 황학롯데캐슬점 서울 중구 청계천로 400 (황학동, 롯데캐슬베네치아) 중구
635 회현SK리더스점 서울 중구 퇴계로 72 (회현동1가, 리더스뷰남산) 중구
[636 rows x 3 columns]
❗ 스타벅스와 다르게 반복문을 중첩해서 활용해야하며, 그 과정에서 처음에 크롤링할 정보의 위치 정보를 정확하게 찾는 것에 어려움을 느꼈다. 특히 li:nth-child(2) 이런 부분 2를 i로 치환해서 for문을 돌리는 헛발질을 많이 했다.
❗클릭 등 동적인 경우는 셀레니움을 사용하고 정보를 크롤링할 때는 beautifulsoup을 사용하는 게 정석인가보다. 이 두 개를 초반에 비슷하게 여겨서 select_one을 쓸 지, find_element를 쓸 지 시행착오가 많았다.
3) 문제 1,2 번의 결과를 가지고 이디야 커피는 스타벅스 커피 매장 근처에 있는지를 분석, 지도의 위치를 표시하여 시각적으로 확인하기 위한 작업을 하였다.
[ IN ]
! pip install googlemaps
import folium
import json
import googlemaps
gmaps_key = 'AIzaSyCiHXsM5pgR02fjYgLbDPhqE3Je4p1PBQg '
gmaps = googlemaps.Client(key=gmaps_key)
m = folium.Map(location = [37.56653641975829, 126.98185562167849], zoom_start = 12)
#크롤링한 스타벅스 매장의 위도 경도를 데이터 프레임에 입력
starbucks['lat'] = data_lat
starbucks['long'] = data_long
print(starbucks)
print(starbucks.isnull().sum())
[ OUT ]
스타벅스 매장 이름 스타벅스 매장 주소 구 이름 \
0 역삼아레나빌딩 서울특별시 강남구 언주로 425 (역삼동)1522-3232 강남구
1 논현역사거리 서울특별시 강남구 강남대로 538 (논현동)1522-3232 강남구
2 신사역성일빌딩 서울특별시 강남구 강남대로 584 (논현동)1522-3232 강남구
3 국기원사거리 서울특별시 강남구 테헤란로 125 (역삼동)1522-3232 강남구
4 대치재경빌딩R 서울특별시 강남구 남부순환로 2947 (대치동)1522-3232 강남구
.. ... ... ...
601 사가정역 서울특별시 중랑구 면목로 3101522-3232 중랑구
602 상봉역 서울특별시 중랑구 망우로 307 (상봉동)1522-3232 중랑구
603 묵동 서울특별시 중랑구 동일로 952 (묵동, 로프트원 태릉입구역) 1층1522-3232 중랑구
604 양원역 서울특별시 중랑구 양원역로10길 3 (망우동)1522-3232 중랑구
605 중화역 서울특별시 중랑구 봉화산로 35 1522-3232 중랑구
lat long
0 37.501087 127.043069
1 37.510178 127.022223
2 37.5139309 127.0206057
3 37.499517 127.031495
4 37.494668 127.062583
.. ... ...
601 37.579594 127.087966
602 37.59689 127.08647
603 37.615368 127.076633
604 37.6066536267232 127.106359790053
605 37.60170912407773 127.07841136432036
[606 rows x 5 columns]
스타벅스 매장 이름 0
스타벅스 매장 주소 0
구 이름 0
lat 0
long 0
dtype: int64
[ IN ]
#크롤링한 이디야 매장의 위도 경고를 데이터 프레임에 입력
ediya['lat'] = np.nan
ediya['long'] = np.nan
for idx, rows in enumerate(ediya['이디야 매장 주소']):
tmp = gmaps.geocode(rows, language='ko')
if tmp:
lat = tmp[0].get('geometry')['location']['lat']
long = tmp[0].get('geometry')['location']['lng']
ediya.loc[idx, 'lat'] = lat
ediya.loc[idx, 'long'] = long
else:
print(idx, rows)
ediya
[ OUT ]
94 서울 관악구 남부순환로 1369 (신림동, 관악농협농산물백화점)
243 서울 마포구 신촌로 66 (노고산동, 농협중앙회)
254 서울 서대문구 수색로 100 (북가좌동, DMC래미안e편한세상)
277 서울 서초구 서초대로 108 (방배동, 삼보빌딩)
326 서울 송파구 충민로 66 (문정동, 가든파이브라이프)
358 서울 송파구 송파대로 567 (잠실동, 잠실주공아파트)
393 서울 영등포구 63로 40 (여의도동, 라이프오피스텔)
이디야 매장 이름 이디야 매장 주소 구 이름 lat long
0 강남YMCA점 서울 강남구 논현동 강남구 37.513679 127.031712
1 강남구청역아이티웨딩점 서울 강남구 학동로 338 (논현동, 강남파라곤) 강남구 37.516551 127.040139
2 강남논현학동점 서울 강남구 논현로131길 28 (논현동) 강남구 37.515190 127.027554
3 강남대치점 서울 강남구 역삼로 415 (대치동, 성진빌딩) 강남구 37.501434 127.052328
4 강남도산점 서울 강남구 도산대로37길 20 (신사동) 강남구 37.522282 127.031480
... ... ... ... ... ...
631 태평로점 서울 중구 세종대로11길 26 (서소문동) 중구 37.563828 126.975206
632 퇴계로2길점 서울 중구 퇴계로2길 1 (남창동) 중구 37.560929 126.978763
633 한국은행점 서울 중구 남대문로 29-2 (남대문로3가) 중구 37.561443 126.978964
634 황학롯데캐슬점 서울 중구 청계천로 400 (황학동, 롯데캐슬베네치아) 중구 37.571219 127.021300
635 회현SK리더스점 서울 중구 퇴계로 72 (회현동1가, 리더스뷰남산) 중구 37.559230 126.980664
636 rows × 5 columns
googlemap으로 가져오지 못한 정보가 있어서 하드코딩 진행
#결측치 확인
ediya[ediya.isna().any(axis=1)]
#결측치 보완
ediya.iloc[94, -2] = 37.48426
ediya.iloc[94, -1] = 126.93070
ediya.iloc[243, -2] = 37.55692
ediya.iloc[243, -1] = 37.55692
ediya.iloc[254, -2] = 37.57519
ediya.iloc[254, -1] = 126.90643
ediya.iloc[277, -2] = 37.49798
ediya.iloc[277, -1] = 127.02747
ediya.iloc[326, -2] = 37.47814
ediya.iloc[326, -1] = 127.12569
ediya.iloc[358, -2] = 37.51619
ediya.iloc[358, -1] = 127.09309
ediya.iloc[393, -2] = 37.51920
ediya.iloc[393, -1] = 126.93743
내 하드코딩 방법은 크롤링을 새로할 때마다 조금씩 바뀌는 이슈 있음
이후 맵 위에 위치를 찍는 방식으로 시각화를 진행했다.
#지도 시각화
mapPoint = [37.517692, 126.989912]
comMap = folium.Map(location = mapPoint, zoom_start = 11)
#스타벅스 매장 위치 찍어보기
for idx, rows in starbucks.iterrows():
folium.CircleMarker(
location = [rows['lat'], rows['long']],
radius=4,
fill=True,
color = 'green',
fill_opacity = 0.5,
tooltip = 'starbucks'
).add_to(comMap)
comMap
#이디야 매장도 찍어보자
for idx, rows in ediya.iterrows():
folium.CircleMarker(
location = [rows['lat'], rows['long']],
radius=4,
fill=True,
color = 'blue',
fill_opacity = 0.5,
tooltip = 'ediya'
).add_to(comMap)
comMap
명확히 알아보기 어려워 그래프를 통해 정량적으로 확인 시도
import matplotlib.pyplot as plt
from matplotlib import font_manager, rc
f_path = 'C:\Windows\Fonts\malgun.ttf'
font_manager.FontProperties(fname=f_path).get_name()
rc('font', family='Malgun Gothic')
#그래프를 만들어서 구마다 각 매장의 차이가 있는지 파악
plt.figure(figsize = (14,10))
starCnt = starbucks['구 이름'].value_counts()
ediyaCnt = ediya['구 이름'].value_counts()
plt.bar(starCnt.index, starCnt, color = 'green', label = '스타벅스', alpha = 0.5)
plt.bar(ediyaCnt.index, ediyaCnt, color = 'darkblue', label = '이디야', alpha = 0.5)
for i in range(len(starCnt)):
plt.text(starCnt.index[i], starCnt.values[i], str(starCnt.values[i]), ha='center', va='bottom')
for i in range(len(ediyaCnt)):
plt.text(ediyaCnt.index[i], ediyaCnt.values[i], str(ediyaCnt.values[i]), ha='center', va='top')
plt.title('구 별 매장 갯수')
plt.xticks(rotation = 45)
plt.xlabel('구 이름')
plt.ylabel('매장 수')
plt.legend()
plt.show()
도에서 스타벅스는 밀집된 지역이 많으나, 이디야는 고루 퍼져있는 형상을 보였다. 좀더 정확한 파악을 위해 지역 별 매장의 갯수를 표시하는 막대그래프를 위와 같이 그렸다.
초록색은 스타벅스, 보라색은 이디야 매장 수를 나타낸다. 해당 지역에서 매장 수가 더 많은 브랜드의 색깔이 그래프 상단에 보인다.
그래프를 살펴보면, [스타벅스]는 강남구, 중구, 서초구 등 특정 지역에 압도적으로 많은 수의 매장을 보유하고 있으며, 중랑구, 강북구, 도봉구에서는 6-8개의 적은 매장이 위치해 있다. 이는 가장 많은 매장을 보유한 강남구와 10배 이상의 차이가 난다.
그에 반하여 [이디야]는 지역별로 20개 지점 정도가 균일하게 분포되어있는 것을 확인하였다.
'이디야 커피는 스타벅스 커피 매장 근처에 있다' 가설은 '참이 아님'을 데이터로 확인하였다. 만일 이디야가 스타벅스 매장 근처에 지점을 오픈하는 비즈니스 전략을 가지고 있었다면, 스타벅스 매장 수가 월등히 높은 강남구, 중구에서 이디야 매장 수 또한 이디야 지점 평균과 비교해 유의미한 차이를 보여야 하나, 그러지 않았다. 또한 스타벅스 매장이 10개 이하로 현저히 낮은 지점을 보유하고 있는 구에서 이디야 커피 매장의 수가 오히려 높은 경우도 찾아볼 수 있었다.(중랑구)