🔎 LEARNING POINT🔎
- tqdm.notebook을 활용할 수 있게 됨
- 웹크롤링으로 다양한 정보를 추출한 경험
- python : replace, astype 습득
- 그래프를 통해 확인하고 싶은 결과값 확인(swarmplot)
- CSV 내보내기
오늘은 내 풀이와 보완한 풀이를 모두 담아 비교한다.
문제1) 수집한 데이터를 pandas dataframe으로 정리하고, 부가 정보 데이터는 셀프여부와 마찬가지로 Y또는 N으로 저장한다.
주유소명, 주소, 브랜드, 휘발유 가격, 경유 가격,
셀프 여부, 세차장 여부, 충전소 여부, 경정비 여부, 편의점 여부, 24시간 운영 여부, 구, 위도, 경도
[ MY ]
import pandas as pd
from bs4 import BeautifulSoup
import requests
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
각각 필요한 정보를 넣을 리스트를 만든 후 데이터를 삽입하는 방식으로 풀었다.
rom tqdm.notebook import tqdm
import numpy as np
driver = webdriver.Chrome()
driver.maximize_window()
driver.get('https://www.opinet.co.kr/searRgSelect.do')
time.sleep(2)
title = [] #주유소명
address = [] #주소
brand = [] #주유소 브랜드
ga_price = [] #휘발유 가격
die_price = [] #경유 가격
self_yn = [] #셀프 여부
wash_yn = [] #세차장 여부
charge_yn = [] #충전소 여부
fix_yn = [] #경정비 여부
conv_yn = [] #편의점 여부
allday_yn = [] #24시간 운영여부
gu = [] #구 (주소)
lat = np.NaN #위도
lng = np.NaN #경도
#지역 선택하기 > 서울
sido_nmo_btn = driver.find_element(By.CSS_SELECTOR, '#SIDO_NM0')
sido_nmo_btn.click() #서울이 있는 목록 클릭
sido_list = sido_nmo_btn.find_elements(By.TAG_NAME, 'option')
sido_list[1].click() #서울 클릭
time.sleep(2)
#시군구 선택하기
sigungu_btn = driver.find_element(By.ID, "SIGUNGU_NM0")
sigungu_btn.click()
sigungu_list = sigungu_btn.find_elements(By.TAG_NAME, 'option') #구 리스트
for i in tqdm(range(1, len(sigungu_list))):
sigungu_list = driver.find_element(By.ID, "SIGUNGU_NM0").find_elements(By.TAG_NAME, 'option')
sigungu_list[i].click()
time.sleep(1)
search_len = driver.find_element(By.ID, "totCnt").text #총 갯수 뽑아내기
for j in range(1, int(search_len)+1):
each_station_btn = driver.find_element(By.CSS_SELECTOR, f"#body1 > tr:nth-child({j}) > td.rlist > a")
each_station_btn.click()
html = driver.page_source
soup = BeautifulSoup(html, "html.parser")
#웹페이지 내용 가져오기
#제목
indiviual_title = soup.select_one('#os_nm').get_text()
title.append(indiviual_title)
#주소
indiviual_add = soup.select_one('#rd_addr').get_text()
address.append(indiviual_add)
#구
ind_gu = indiviual_add.split(" ")[1]
gu.append(ind_gu)
#브랜드
indiviual_bra = soup.select_one('#poll_div_nm').get_text()
brand.append(indiviual_bra)
#휘발유
indiviual_ga = soup.select_one('#b027_p').get_text()
ga_price.append(indiviual_ga)
#경유
indiviual_die = soup.select_one('#d047_p').get_text()
die_price.append(indiviual_die)
#셀프
self_icon = soup.select_one('#self_icon')
if self_icon and self_icon['id'] == 'self_icon':
self_yn.append('Y')
else:
self_yn.append('N')
#세차장
if 'off' in soup.select_one('#cwsh_yn')['src']:
wash_yn.append('N')
else:
wash_yn.append('Y')
#충전소
if 'off' in soup.select_one('#lpg_yn')['src']:
charge_yn.append('N')
else:
charge_yn.append('Y')
#경정비 여부
if 'off' in soup.select_one('#maint_yn')['src']:
fix_yn.append('N')
else:
fix_yn.append('Y')
#편의점
if 'off' in soup.select_one('#cvs_yn')['src']:
conv_yn.append('N')
else:
conv_yn.append('Y')
#24시간
if 'off' in soup.select_one('#sel24_yn')['src']:
allday_yn.append('N')
else:
allday_yn.append('Y')
df = pd.DataFrame({'주유소명' : title,
'주소' : address,
'브랜드' : brand,
'휘발유 가격' : ga_price,
'경유 가격' : die_price,
'셀프 여부' : self_yn,
'세차장 여부' : wash_yn,
'충전소 여부' : charge_yn,
'경정비 여부' : fix_yn,
'편의점 여부' : conv_yn,
'24시간 운영여부' : allday_yn,
'구' : gu,
'위도' : lat,
'경도' : lng
})
df
🤔 구해야할 값이 많아서 이것저것 구해보고 시도하느라 시간이 많이 소요됐고, 특히 each_station_btn에서#body1 > tr:nth-child({j}) > td.rlist > a 여기를 결과값이 나오게 정리하는 과정이 힘들었다ㅠㅠ
셀프임을 얻는 부분에서 한번에 알지 못해서 힌트가 필요했다. (한 번 이런 방식이 있다는 걸 알았으니까 다음 번에 기억해서 풀어나갈 수 있겠지)
.
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)
m
for idx, rows in tqdm(enumerate(df['주소'])):
tmp = gmaps.geocode(rows, language='ko')
if tmp:
lat = tmp[0].get('geometry')['location']['lat']
lng = tmp[0].get('geometry')['location']['lng']
df.loc[idx, '위도'] = lat
df.loc[idx, '경도'] = lng
else:
print(f'에러 지점 : {idx}, {rows}')
driver.quit()
df
[ OUT ]
주유소명 주소 브랜드 휘발유 가격 경유 가격 셀프 여부 세차장 여부 충전소 여부 경정비 여부 편의점 여부 24시간 운영여부 구 위도 경도
0 HD현대오일뱅크㈜직영 도곡셀프주유소 서울 강남구 남부순환로 2718 (도곡2동) HD현대오일뱅크 1,784 1,719 Y Y N Y N N 강남구 37.485462 127.043218
1 (주)중앙에너비스 수서지점 서울 강남구 광평로 202 (수서동) SK에너지 1,787 1,737 Y Y N N N N 강남구 37.484105 127.095040
2 방죽주유소 서울 강남구 밤고개로 215 (율현동) GS칼텍스 1,787 1,739 Y Y N N N N 강남구 37.474993 127.106887
3 오일프러스 셀프 서울 강남구 남부순환로 2651 (도곡동) SK에너지 1,794 1,734 Y Y N Y N Y 강남구 37.485839 127.039672
4 삼성동주유소 서울 강남구 테헤란로 619 (삼성동) HD현대오일뱅크 1,795 1,755 Y Y N N N N 강남구 37.509858 127.065110
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
429 신내주유소 서울 중랑구 용마산로 705 (신내동) SK에너지 1,799 1,698 Y Y N N N N 중랑구 37.617125 127.095896
430 (주)기지에너지 서울 중랑구 용마산로 716 (신내동) S-OIL 1,799 1,699 N N N N N N 중랑구 37.617628 127.094381
431 범아주유소 서울 중랑구 동일로 881 (묵동) S-OIL 1,819 1,759 N Y N Y N N 중랑구 37.609311 127.077673
432 용마로주유소 서울 중랑구 용마산로 309 (면목동) SK에너지 1,838 1,738 Y Y N N N N 중랑구 37.579878 127.092182
433 신일셀프주유소 서울 중랑구 상봉로 58 (망우동) SK에너지 1,849 1,748 Y Y N Y N Y 중랑구 37.590942 127.093807
434 rows × 14 columns
CSV 파일로 내보내기
#결측치 있는지 확인
df.isnull().sum()
#내보내기
df.to_csv('./seoul_oil_station.csv', sep = ',' , encoding = 'utf-8')
#파일 확인하기
df2 = pd.read_csv('./seoul_oil_station.csv', index_col = 0, encoding = 'utf-8')
df.head(5)
[ 다른 풀이]
import pandas as pd
from selenium import webdriver
from selenium.webdriver.common.by import By
from bs4 import BeautifulSoup
import time
import numpy as np
#1. 페이지 접근
url = 'https://www.opinet.co.kr/searRgSelect.do'
driver = webdriver.Chrome()
driver.get(url)
#2. 서울 고정
#seoul = driver.find_elements(By.CSS_SELECTOR, '#SIDO_NM0 > option') 이렇게 할 수 있으나 하지 않는 이유는 뒷 쪽에서 send_keys를 해서 값을 보내야 하기 때문이다/
seoul = driver.find_element(By.CSS_SELECTOR, '#SIDO_NM0')
sido_list = seoul.find_elements(By.CSS_SELECTOR, 'option')
sido_names = [option.get_attribute('value') for option in sido_list]
print(sido_names) #맨 앞이 빈 칸이므로
sido_names = sido_names[1:]
seoul.send_keys(sido_names[0]) #서울특별시 클릭
#3. 구별 선택
gu_list_raw = driver.find_element(By.CSS_SELECTOR, '#SIGUNGU_NM0') #부모태그
gu_list = gu_list_raw.find_elements(By.CSS_SELECTOR, 'option')
gu_names = [option.get_attribute('value') for option in gu_list if option.get_attribute('value')] #공백 제거 맨 앞이 value값이 없음
print(gu_names)
import googlemaps
gmaps_key = 'AIzaSyCiHXsM5pgR02fjYgLbDPhqE3Je4p1PBQg '
gmaps = googlemaps.Client(key=gmaps_key)
datas = []
for i in range(len(gu_names)):
gu_selector = f'#SIGUNGU_NM0 > option:nth-child({i+2})'
driver.find_element(By.CSS_SELECTOR, gu_selector).click()
station_items = driver.find_elements(By.CSS_SELECTOR, '#body1 > tr') #주유소 목록
for idx in range(len(station_items)):
detail_selector = f'#body1 > tr:nth-child({idx+1}) > td.rlist > a'
driver.find_element(By.CSS_SELECTOR, detail_selector).click()
name = driver.find_element(By.CSS_SELECTOR, '.header #os_nm').get_attribute('innerText')
gasoline = driver.find_element(By.CSS_SELECTOR, '#b027_p').get_attribute('innerText')
diesel = driver.find_element(By.CSS_SELECTOR, '#d047_p').get_attribute('innerText')
address = driver.find_element(By.CSS_SELECTOR, '#rd_addr').get_attribute('innerText')
brand = driver.find_element(By.CSS_SELECTOR, '#poll_div_nm').get_attribute('innerText')
cwsh_yn = 'N' if '_off' in driver.find_element(By.CSS_SELECTOR, '.service #cwsh_yn').get_attribute('src').split('/')[-1] else 'Y'
lpg_yn = 'N' if '_off' in driver.find_element(By.CSS_SELECTOR, '.service #lpg_yn').get_attribute('src').split('/')[-1] else 'Y'
main_yn = 'N' if '_off' in driver.find_element(By.CSS_SELECTOR, '.service #maint_yn').get_attribute('src').split('/')[-1] else 'Y'
cvs_yn = 'N' if '_off' in driver.find_element(By.CSS_SELECTOR, '.service #cvs_yn').get_attribute('src').split('/')[-1] else 'Y'
sel24_yn = 'N' if '_off' in driver.find_element(By.CSS_SELECTOR, '.service #sel24_yn').get_attribute('src').split('/')[-1] else 'Y'
try:
driver.find_element(By.CSS_SELECTOR, '#self_icon').get_attribute('alt')
is_self = 'Y'
except:
is_self = 'N'
gu = address.split()[1]
tmp = gmaps.geocode(address, language = 'ko')
lat = tmp[0].get('geometry')['location']['lat']
lng = tmp[0].get('geometry')['location']['lng']
datas.append({
'name' : name,
'address' : address,
'brand' : brand,
'is_self' : is_self,
'gasoline' : gasoline,
'diesel' : diesel,
'car_wash' : cwsh_yn,
'charging_station' : lpg_yn,
'car_maintenance' : main_yn,
'convenience_store': cvs_yn,
'24_hours' : sel24_yn,
'gu' : gu,
'lat' : lat,
'lng' : lng
})
time.sleep(0.2)
time.sleep(0.5)
driver.quit()
df = pd.DataFrame(datas)
df.tail()
❗find_element로 추출했고, comprehension으로 한줄로 마무리. 나는 ['scr']라고 한 걸, get_atrribute('src')로 풀어냈음.
import datetime
now = datetime.datetime.now()
nowDate = now.strftime('%Y%m%d')
df.to_csv(f'./oilstation_oneday_{nowDate}.csv', encoding = 'utf-8')
stations = pd.read_csv('./oilstation_oneday_20231108.csv', encoding = 'utf-8', thousands = ',' ,index_col=0)
#thousands를 해줘야 읽을 때 ,가 들어간 것을 str이 아닌 숫자로 인식!
stations
문제2) 분석한 결과를 바탕으로 휘발유와 경유 가격이 셀프주유소에서 정말 저렴한지 의견을 제시하라
[ MY ]
import matplotlib.pyplot as plt
import seaborn as sns
import matplotlib.pyplot as plt
from matplotlib import font_manager, rc
import warnings
warnings.filterwarnings('ignore')
df2['휘발유 가격'] = df2['휘발유 가격'].apply(lambda x: x.replace(',', '')).astype(int)
df2['경유 가격'] = df2['경유 가격'].apply(lambda x: x.replace(',', '')).astype(int)
❗ 가격에 ,이 있어서 str형식이기에 컴마를 replace로 해서 제거하고 astype으로 형식을 변경
print(df2['휘발유 가격'].sort_values().min())
print(df2['경유 가격'].sort_values().min())
print(df2['휘발유 가격'].sort_values().max())
print(df2['경유 가격'].sort_values().max())
minx = int(df2['경유 가격'].sort_values().min())
maxx = int(df2['경유 가격'].sort_values().max())
f_path = 'C:\Windows\Fonts\malgun.ttf'
font_manager.FontProperties(fname=f_path).get_name()
rc('font', family='Malgun Gothic')
fig, axes = plt.subplots(1, 2, figsize = (15, 5))
axes[0].set(ylim=(minx, maxx))
ax1 = sns.swarmplot(x='셀프 여부', y='휘발유 가격', data=df2, ax = axes[0])
ax1.set_title('휘발유 가격 비교')
axes[1].set(ylim=(minx, maxx))
ax2 = sns.swarmplot(x='셀프 여부', y='경유 가격', data=df2, ax = axes[1])
ax2.set_title('경유 가격 비교')
plt.show()
df2[df2['휘발유 가격'] == df2['휘발유 가격'].sort_values().min()]
df2[df2['경유 가격'] == df2['경유 가격'].sort_values().min()]
[ 다른 풀이 ]
#주유소 정보 시각화
#설정 부분
import matplotlib.pyplot as plt
import seaborn as sns
import matplotlib.pyplot as plt
from matplotlib import font_manager, rc
import warnings
warnings.filterwarnings('ignore')
f_path = 'C:\Windows\Fonts\malgun.ttf'
font_manager.FontProperties(fname=f_path).get_name()
rc('font', family='Malgun Gothic')
#그래프 그리기
plt.figure(figsize = (12, 8))
sns.boxplot(x='is_self', y='gasoline', data=stations, palette='Set1')
plt.grid(True)
plt.show()
#휘발유 브랜드별 비교
plt.figure(figsize = (12, 8))
sns.boxplot(x='brand', y='gasoline', hue='is_self', data=stations, palette='Set1')
plt.grid(True)
plt.show()
#휘발유 구별 비교
plt.figure(figsize = (12, 8))
sns.boxplot(x='gu', y='gasoline', data=stations, palette='Set1')
plt.grid(True)
plt.show()
#경유 브랜드별 비교
plt.figure(figsize = (12, 8))
sns.boxplot(x='brand', y='diesel', hue='is_self', data=stations, palette='Set1')
plt.grid(True)
plt.show()
#경유 구별 비교
plt.figure(figsize = (12, 8))
sns.boxplot(x='gu', y='diesel', data=stations, palette='Set1')
plt.grid(True)
plt.show()
#지도시각화
import json
import folium
#가장 값싼 주유소 10개
stations[['gu', 'name', 'is_self', 'gasoline']].sort_values(by='gasoline', ascending=True).head(10).reset_index(drop=True)
#지도 시각화용 데이터 프레임
import numpy as np
gu_data = pd.pivot_table(data=stations, index = 'gu', values='gasoline', aggfunc=np.mean)
geo_path = '..data/02/ skorea_municipalities_geo_simple.json'
geo_str = json.load(open(geo_path, encoding = 'utf-8'))
m = folium.Map(location = [37.5502, 126.982], zoom_start = 10.5)
m.choropleth(
geo_data = geo_str,
data = gu_data,
columns = [gu_data.index, 'gasoline'],
key_one = 'feature.id',
fill_color='PuRd'
)
m
[ MY ]
셀프주유소를 선택했을 때 일반 주유소보다 휘발유, 경유 모두 저렴할 확률이 높다. 그래프에서 '셀프 여부'가 'Y'(yes)일 때 'N'(No)보다 하단에 점들이 많이 위치해있다는 것을 확인할 수 있다. 그러나 '경유 가격'의 경우 가장 저렴한 주유소는 셀프 주유소가 아닌 점을 볼 때, 절대적으로 셀프주유소가 일반 주유소보다 싸다고 말하기는 어렵다.
🤔 그래프를 그리는 게 경험이 별로 없어서 어떤 그래프가 적절할 지 생각하는 것부터 코드를 짜는 것까지 시간이 오래걸렸다.