1
2
3
4
5
6
7
8
9
| <목차>
0. 시작 전
1. 사이트 분석 및 접근
1-1) 사이트 분석
1-2) 접근
2. 자료 확보(원큐에)
3. 자료 정리
4. 시각화
|
0. 시작 전
이 글은 제로베이스의 강의를 토대로 작성합니다.
이번 시간에는 Selenium을 활용해 셀프주유소 가격 분석을 해볼겁니다.
아래 항목들은 우리에게 필요한 자료입니다
실습 전 잠깐 Selenium에 대해 간략하게 알아봅시다!
♠다운로드 받을 것: 모듈
, 크롬 드라이버
1
2
3
4
5
6
7
8
9
| <Beautiful Soup로 해결 안되는 것>
- 접근할 웹 주소를 알 수 없을 때
- 자바스크립트를 사용하는 웹페이지의 경우
- 웹 브라우저로 접근하면 안될 때
<Selenium>
- 웹 브라우저를 원격 조작하는 도구
- 자동으로 URL을 열고 클릭이 가능
- 스크롤, 문자의 입력, 화면 캡처 등등
|
전 이 경로에서 pip install selenium 설치했습니다
1
| C:\Users\withj\Desktop\python1\Lib\site-packages>
|
1. 사이트 분석 및 접근
1-1) 사이트 분석
위와 같은 창을 순서대로 클릭하면 아래와 같이 필터링 할 수 있는 페이지로 넘어갑니다
우리가 눈으로 확인이 가능한 스크롤바, 각 체크항목, 그리고 아래에는 엑셀 저장도 있습니다
참고로 이 페이지는 우리가 필터링할 때마다 보여지는 자료는 바뀌지만, 주소는 그대로기에 정적페이지를 웹크롤링하는 Beautifulsoup는 당연히 안됩니다.
'이때 selenium이 빛을 발한다'
1-2) 접근
한큐에 원하는 페이지를 생성해봅시다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| from selenium import webdriver
from selenium.webdriver.common.by import By
import time
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# 1회 주소접근시 홈페이지로 이동
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument("webdriver.chrome.driver=C:/Users/withj/Desktop/chromedriver-win64/chromedriver.exe")
url = 'https://www.opinet.co.kr/searRgSelect.do'
driver = webdriver.Chrome(options=chrome_options)
driver.get(url)
# 너무 빠른 실행시 오류 발생이라 3초 휴식하고 다음 동작 실행
time.sleep(3)
# 2회 접근하면 원하는 url로 접근가능
driver.get(url)
|
F12(검사) 활용
다시 이 페이지로 돌아와서 코드를 짜봅시다
1번 버튼을 먼저 눌러야 왼쪽에 아무 버튼이나 갖다대도 좌측의 저런 상세내용이 우측 검사
칸에서 반응한다
사진에서 id값들은 각각 1개니 저 태그만 선택하면 되서 작업하기가 매우 편하고,
노란색을 보면 class = “hidden”인데 이건 속성값이 1개일지 n개일지 알 수 없스습니다.
이것 외에도 각각의 항목들을 저 1번버튼 켜고 검사해보면 조회
, 기름 종류
, 엑셀 저장
등 아무거나 싹 확인 가능합니다
시/도
1
2
3
4
5
6
7
| from selenium.webdriver.common.by import By
sido_list_raw = driver.find_element(By.ID, "SIDO_NM0")
sido_list = sido_list_raw.text.split('\n')
# 시/도 리스트 출력
for sido in sido_list:
print(sido)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| # 시/ F12 눌러서 보니 option 태그로 이렇게 뜨네
<option value="서울특별시">서울</option>
<option value="부산광역시">부산</option>
<option value="대구광역시">대구</option>
~ ~ ~
-----------------------------------------------------------------
# 인덱스 7번째 추출
sido_list = sido_list_raw.find_elements(By.TAG_NAME, 'option')
sido_list[7].get_attribute("value")
#➡️ 울산광역시
----------------------------------------------------------------------
# 시/도 전부 나열
sido_names = []
for option in sido_list:
sido_names.append(option.get_attribute("value"))
sido_names
|
1
2
3
4
5
6
7
8
9
10
| # 5개만 가져와보자
sido_names[:5]
#➡️ ['', '서울특별시', '부산광역시', '대구광역시', '인천광역시']
------------------------------------------------------------
# 공백 제거해주자
sido_names = sido_names[1:]
sido_names[:5]
#➡️ ['서울특별시', '부산광역시', '대구광역시', '인천광역시', '광주광역시']
|
1
2
| # 키 전송해서 `울산`으로 바꾸기
sido_list_raw.send_keys(sido_names[6])
|
시/군/구
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# 대기 시간 설정 (최대 10초 기다림)
wait = WebDriverWait(driver, 10)
# 대기 시간동안 해당 ID의 요소를 찾음
gu_list_raw = wait.until(EC.presence_of_element_located((By.ID, "SIGUNGU_NM0")))
# 부모 태그에서 자식 태그 찾음
gu_list = gu_list_raw.find_elements(By.TAG_NAME, "option")
# 자식 태그에서 값을 가져와서 출력
gu_names = [option.get_attribute("value") for option in gu_list]
gu_names = gu_names[1:]
print(gu_names[:5], len(gu_names))
# ➡️['남구', '동구', '북구', '울주군', '중구'] 5
gu_list_raw.send_keys(gu_names[0])
|
엑셀 저장
1
2
3
4
5
6
7
8
9
10
11
| ------3가지 중 아무거나 ㄱㄱ (단, 연결해서 한번에 재생하지말고 따로따로)--------------
# 1
driver.find_element(By.CSS_SELECTOR, "#glopopd_excel").click()
# 2
driver.find_element(By.XPATH, '//*[@id="glopopd_excel"]').click()
# 3
element_get_excel = driver.find_element(By.ID, "glopopd_excel")
element_get_excel.click()
|
2. 자료 확보 (원큐에)
반복문(모든 구
의 정보가 담긴 xlsx 다운 받읍시다)
1
2
3
4
5
6
7
8
9
10
11
12
| # ['남구', '동구', '북구', '울주군', '중구']
import time
from tqdm import tqdm_notebook
for gu in tqdm_notebook(gu_names):
element = driver.find_element(By.ID, "SIGUNGU_NM0")
element.send_keys(gu)
time.sleep(3)
element_get_excel = driver.find_element(By.ID, "glopopd_excel").click()
time.sleep(3)
|
다운로드 다 끝났으면 chrome팝업창 닫기 ㄱㄱ
이로써 울산의 남구
, 동구
, 북구
, 울주군
, 중구
의 주유소 xlsx 5개를 확보했습니다
3. 자료 정리
아까 다운받은 xlsx 파일들 정리합시다
1
2
3
4
5
6
| import pandas as pd
from glob import glob
src = "C:/Users/withj/Desktop/eda-practice/web"
# 파일 목록 한 번에 가져오기
glob(src + "/지역_*.xls")
|
참고
임의의 xlsx 하나 열었는데 header가 2칸 밀려있네??
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # 여러 엑셀 파일 경로 가져오기
oil_info = glob(src + "/지역_*.xls")
# 각 엑셀 파일을 읽어 DataFrame으로 저장한 후 리스트에 추가
box = []
for oil in oil_info:
tmp = pd.read_excel(oil, header=2)
box.append(tmp)
# 리스트에 있는 모든 DataFrame을 합치기
new_oil = pd.concat(box)
# 결과 확인
print(new_oil.tail())
|
참고: pd.concat VS pd.merge
1
2
3
4
5
6
7
8
9
| # 잠깐!
pd.concat
데이터를 단순히 열 이름이나 인덱스가 서로 달라도 이어붙인다
(행 갯수는 짝이 맞아야 함)
pd.merge
두 DataFrame을 특정 열을 기준으로 병합하는 데 사용되며, 공통된 열을 기준으로 병합한다.
(//)
|
병합
1
2
| oil_raw = pd.concat(box)
oil_raw
|
1
2
3
4
| oil_raw.info()
# 행은 59개인데 인덱스도 222개로 이상하고 기름이 object인 것도 이상하네
# 아래가서 조치해보겠습니다
|
1
2
3
4
5
6
7
8
9
10
| # 새로운 DF
gas = pd.DataFrame({
"상호": oil_raw["상호"],
"주소": oil_raw["주소"],
"가격": oil_raw["휘발유"],
"셀프": oil_raw["셀프여부"],
"상표": oil_raw["상표"]
})
gas.head()
|
1
2
3
4
| # 평균적으로 주유비가 비싼 구를 확인하기 위해 추가
gas["구"] = [eachAddress.split()[1] for eachAddress in oil_raw["주소"]]
gas
|
1
2
3
4
| # 열을 추가하면서 이상한 건 없네요!
gas['구'].unique()
#➡️ array(['동구', '북구', '울주군', '중구', '남구'], dtype=object)
|
아까 이상했던 인덱스들 다 초기화하기
1
2
3
4
| # 번호 다시 매기자
gas.reset_index(inplace=True)
gas.head()
|
맨 앞 컬럼 2개 제거
1
2
| gas.drop(['level_0', 'index'], axis=1, inplace=True)
gas
|
4. 시각화
총 4개 준비했습니다!
- matplotlib 가격&셀프
- seaborn 가격&셀프
- 브랜드&가격&셀프
- folium
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| # 기본 코드
import matplotlib.pyplot as plt
import seaborn as sns
import platform
from matplotlib import font_manager, rc
get_ipython().run_line_magic("matplotlib", "inline")
# %matplotlib inline
path = "C:/Windows/Fonts/malgun.ttf"
if platform.system() == "Darwin":
rc("font", family="Arial Unicode MS")
elif platform.system() == "Windows": # 수정된 부분
font_name = font_manager.FontProperties(fname=path).get_name() # 수정된 부분
rc("font", family=font_name)
else:
print("Unknown system. sorry~~")
|
1
2
3
| # matplotlib 가격&셀프
gas.boxplot(column="가격", by="셀프", figsize=(12, 8));
|
이걸 보니 울산의 셀프주유소가 아닌 곳이 가격이 더 비싸고,
특히 셀프 주유소가 아닌 곳에서 가격이 1800원대 근처가 2곳이고,
셀프 주유소는 값 범위 안에 없는 제일 비싼 곳들이 1700원이네요
이번엔 seaborn으로 봅시다
1
2
3
4
5
6
| # seaborn 가격&셀프
plt.figure(figsize=(12, 8))
sns.boxplot(x="셀프", y="가격", data=gas, palette="Set1")
plt.grid(True)
plt.show()
|
seaborn도 마찬가지다 matplotlib이랑 똑같네요!
이번엔 브랜드를 추가해봤습니다
1
2
3
4
5
6
| # 브랜드&가격&셀프
plt.figure(figsize=(12, 8))
sns.boxplot(x="상표", y="가격", hue="셀프", data=gas, palette="Set3")
plt.grid(True)
plt.show()
|
전반적으로 알뜰주유소만 셀프주유소고 제일 싸다는 것을 알 수 있고,
GS칼텍스가 평균적으로 많이 비싼편이네요
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # folium
import json
import folium
import warnings
warnings.simplefilter(action="ignore", category=FutureWarning)
# 가장 비싼 주유소 10개
gas.sort_values(by="가격", ascending=False).head(10)
# 가장 값싼 주유소 10개
gas.sort_values(by="가격", ascending=True).head(10)
import numpy as np
gu_data = pd.pivot_table(data=gas, index="구", values="가격", aggfunc=np.mean)
gu_data.head()
|
json 파일을 이용해 시각화 연동
1
2
3
4
| # csv 저장
import pandas as pd
gas.to_csv('gas.csv', index=False)
gas_csv = pd.read_csv('gas.csv')
|
전 제가 필터링한 조건 기준하에 울산 내 모든 주유소에 관한 경도, 위도를 추출하기 위해 gas.csv를 저장해 223개 주유소의 경도, 위도를 생성했으며, 그 정보가 없는 29번째 행이 있었는데 그것은 아래와 같이 지워줬습니다
1
2
3
| # 이화주유소가 경도, 위도 안나오더라
gas_c = gas.copy()
gas_c[gas_c['가격'] == 1619]
|
1
2
| gas_c.drop(gas_c.index[28], inplace=True)
gas_c[gas_c['가격'] == 1619]
|
여기까지 끝내고 json파일에 각 행정구역별로 222개의 주유소 경도, 위도를 작성해주었습니다
최종 시각화(실패)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| import json
import folium
src = "C:/Users/withj/Desktop/source"
# geo_path1 = src + "/original_ulsan_geo.json"
# geo_str = json.load(open(geo_path1, encoding="utf-8"))
geo_path2 = src + "/reversed_ulsan_geo.json"
geo_str = json.load(open(geo_path2, encoding="utf-8"))
ulsan_coordinates = [35.5384, 129.3114]
my_map = folium.Map(location=ulsan_coordinates, zoom_start=10.5, tiles="Stamen Toner", attr="Stamen")
folium.Choropleth(
geo_data=geo_str,
data=gu_data,
columns=[gu_data.index, "가격"],
key_on="feature.id",
fill_color="PuRd"
).add_to(my_map)
my_map
|
시각화하니 위와 같은 결과가 나왔습니다. 영 이상하네요ㅋㅋ
이런 저런 다양한 방법을 시도하다 도저히 해결이 되지 않아서 교수님께 여쭤보니 json 파일의 각 행정구역별 polygon을 다시 고쳐보라고 하셨지만, 들을 강의가 많이 남아있어 아쉽지만 시간 관계상 어쩔 수 없이 넘어가야할 것 같습니다.
참고
- 제로베이스 강의
- https://velog.io/@insung_na/Selenium과-주유소-가격-정보