
Chapter
1. Selenium 기초
(1) 설치
(2) Selenium으로 기본 원격 작동하기
2. 유가 분석: 정말로 셀프주유소 가격이 더 저렴할까?
(1) 데이터 수집하기
(2) 주유 가격 정보 시각화
이번 시간에는 Selenium 라이브러리를 이용한 웹페이지 분석을 배웠다.
Selenium은 웹 브라우저를 원격 조작하는 도구로, 코드실행으로 인해 자동으로 URL을 열거나, 클릭, 스크롤, 문자 입력 등이 가능하게 한다. 지난 시간에도 BeaitifulSoup을 이용한 웹페이지 분석을 했지만, Selenium은 BeautifulSoup으로는 해결할 수 없는 기능들을 할 수 있다. 다음과 같은 경우이다.
먼저 라이브러리를 설치하기 전에 구글드라이버를 서치해야 한다.

다음과 같이 Chrome정보에서 크롬 정보를 확인한 다음, 버전에 맞는 구글드라이버를 검색해서 설치해주면 된다. 나의 경우에는, 자동 업데이트가 설정되어 있었기 때문에, 오늘 날짜 기준으로 가장 최근 버전(116.0) 이었다. 따라서, 'Chrome driver 116.0'을 설치해주었다.
이제 라이브러리를 설치하고 모듈을 불러오자.
conda install selenium
from selenium import webdriver
여기까지 끝났다면, 크롬드라이버를 가져온 다음, 간단하게 URL주소를 통해 브라우저를 열어보자.
driver = webdriver.Chrome("..\\driver\\chromedriver.exe")
driver.get("https://www.naver.com")

내가 작업하고 있는 Jupyter notebook이 적용되고 있는 크롬 브라우저말고 하나의 창이 열린 것을 볼 수 있다. 이렇게 열린 창은 URL주소창 아래에 'Chrome이 자동화된 테스트 소프트웨어에 의해 제어되고 있습니다.' 라는 문구가 적혀있다. 작업이 마쳤으면
driver.quit()
으로 테스트창을 꺼줘야 한다.
.
.
라이브러리를 이용해서 간단하게 원격 작동할 수 있는 다음과 같은 코드들이 있다.
driver.maximize_window()
driver.minimize.window()
driver.set_window_size(600,700) #원하는 규격 설정
driver.refresh()
driver.back()
driver.forward()
# window.open()안에 URL주소를 넣어주면 해당 웹페이지가 새로운 탭으로 열린다.
driver.execute_script('window.open("https://www.naver.com")')
# 빈페이지의 탭 생성
driver.execute_script('window.open("")')
# 0번째 탭으로 이동
driver.switch_to.window(driver.window_handles[0])
# 열려 있는 탭을 하나씩 닫음
driver.close()
# 브라우저 종료
driver.quit()
해당 경로에 지정해보자.
driver.save_screenshot('.\last_height.png')
먼저 스크롤이 가능한 최대 길이를 찾아낸다.
driver.execute_script('return document.body.scrollHeight')
최대 길이를 이용해서 회면 스크롤을 가장 하단으로 이동해본다.
driver.execute_script('window.scrollTo(0, document.body.scrollHeight)')
화면 스크롤 상단 이동
driver.execute_script('window.scrollTo(0,0);')
html 태그를 이용해서 해당 지점까지 스크롤을 이동할 수도 있다. 아래와 같은 지점까지 스크롤을 이동해보자.

from selenium.webdriver import ActionChains
some_tag = driver.find_element(By.CSS_SELECTOR, "#feed > div.ContentHeaderView-module__content_header___nSgPg > div > ul > li:nth-child(5) > a")
action = ActionChains(driver)
action.move_to_element(some_tag).perform()
우선 ActionChains 모듈을 불러온 다음find_element()메서드를 이용하는데, Selenium에서 제공하는 기본 By클래스를 이용한다. 아래 사진에서 처럼 원하는 코드를 'copy selector'한 다음 붙여넣어준다.

이전 드라이버를 종료해주고, 새로운 URL주소('https://pinkwink.kr')로 들어가서 검색어를 입력해보자.
from selenium import webdriver
# from selenium.webdriver.common.by import By
driver = webdriver.Chrome(executable_path='..\driver\chromedriver.exe')
driver.get('https://pinkwink.kr/')

화면과 같은 검색창에 '딥러닝'을 검색해보자.
먼저 html코드로 돋보기 버튼을 선택해보자. 사용할 ActionChains모듈을 불러와서 실행한다.

사진에서 보이는 것처럼 돋보기 버튼은 복사할 코드가 없기 때문에 부모 코드를 불러와서 클릭버튼을 실행해야 한다.
from selenium.webdriver import ActionChains
search_tag = driver.find_element(By.CSS_SELECTOR, '.search')
action = ActionChains(driver)
action.click(search_tag)
action.perform()

검색어 창이 열리면 검색창의 id='header'를 이용해 검색어를 넣어주자.
driver.find_element(By.CSS_SELECTOR, '#header > div.search.on > input[type=text]').send_keys('딥러닝')

검색어가 입력되면 돋보기 버튼을 누를 수 있는 코드가 활성화되었다.
이제 버튼을 눌러보자.
driver.find_element(By.CSS_SELECTOR, '#header > div.search.on > button').click()

검색이 성공적으로 마쳐졌다.
.
.
.
그렇다면 이제부터 '오피넷' 홈페이지를 통해서 서울시 자치구 주유소들의 가격을 비교해 보자. 프로젝트의 핵심 목표는 '정말 셀프주유소 가격이 더 저렴한가'를 확인하는 일이며, 더 나아가 자치구들의 주유소의 가격을 비교하고, 지도 시각화까지 해보는 것이다.
사실 강의 날짜 기준으로는 메인페이지와 지역별 휘발유 단가를 확인하는 하위 페이지의 URL주소가 같아서 이를 원격으로 이동해주는 작업이 필요했지만, 지금은 이미 URL주소가 구분되어서 그런 작업의 수고를 덜게 되었다.
페이지에 접근해서 주소창을 열어보자.
url = 'https://www.opinet.co.kr/searRgSelect.do'
driver = webdriver.Chrome("..\driver\chromedriver.exe")
driver.get(url)

페이지는 다음과 같이 되어있는데, 우리가 해야할 작업순서는 다음과 같다.
먼저 '시/도' 리스트를 가져와서 '서울특별시'를 선택해보자.

다음과 같이 선택칸의 id가 'SIDO_NM0'로 되어있으므로 이를 이용하자.
sidoListRaw = driver.find_element_by_id("SIDO_NM0")
sidoListRaw.text

각 시도명은 자식 태그안의 <option value> 안에 있다는 것이 보인다. 이를 이용해서 속성값 데이터를 가져오자.



다음과 같이 17개의 시도명을 리스트 형식으로 변수에 지정했다.
이제 '시도' 선택칸(sidoListRaw)에 '서울'명을 지정해준다.
sidoListRaw.send_keys(sido_names[0])

'서울'로 지정됨에 따라 '시/군/구'칸에 서울시 자치구 명의 리스트가 생겼음을 html 코드로 확인할 수 있다. 이제 자치구 리스트를 만들고, 엑셀 저장 버튼까지 눌러주자. 25개의 자치구가 있으므로 for문을 활용한다.

이때, selenium가 시간을 갖고 차근차근 진행될 수 있도록, 3초간의 휴지기를 두고서 자치구 선택 -> 엑섹버튼 누르기 과정을 진행할 수 있게 하였다.
이렇게 25개의 자치구별 주유소 단가 엑셀 파일이 저장되었다.
사용한 드라이버는 닫아주고, 엑셀 파일을 불러오자. glob모듈을 사용하면 한 번에 가져올 수 있다.

파일을 정해주고, 25개 자치구가 모두 불러왔는지 확인해보자.

파일이 잘 불러왔는지, 확인해보자. 그런데, 컬럼명이 제대로 확인되지 않는다.

엑셀 파일을 확인했더니, 처음 1,2행은 제목으로 되어있고, 3번째 행부터 컬럼이 시작된다.

앞의 1,2행을 header로 설정해준다.

이런과정을 25개구 전부 해야하기 때문에 모두 for문을 이용해서 하나의 리스트로 만들어 준다.

이제 데이터프레임으로 만들어주자. 25개 엑셀표 모두 형식이 동일했기 때문에, 연달아 붙이기만 하면 된다. 이런 경우에는 concat 메서드를 사용하면 간단하게 이어붙일 수 있다.


모두 441개의 주유소 데이터가 수집되었음을 확인하였다.
그런데 원하지 않은 데이터가 섞여있으므로, 몇 개의 컬럼만 따로 데이터프레임을 만들어 주었다.

우리는 자치구의 정보도 알아야 하므로 "주소" 컬럼에서 자치구 정보만 따로 빼내어 "구" 컬럼을 만들어 주었다.



다음으로 '단가'컬럼의 데이터가 object형으로 되어 있는 것을 'float'형으로 바꿔주자.
그런데 중간에 '-' value가 있어서 ValueError가 발생했다.

가격 정보가 없는 주유소는 배제하고, value를 확인할 수 있는 주유소만 사용하기로 하자.

마지막으로, 분명 440개의 데이터를 가지고 있음에도 25개의 엑셀 파일을 이어붙였기 때문에 맨 처음 칼럼의 번호가 맞지가 않다. 그래서 인덱스를 재정렬하고, 원래의 index 칼럼은 제거해 주었다.


.
.
이제 모든 데이터 수집 작업이 끝나고 시각화를 통한 통계분석을 해보자.
먼저 사용할 모듈을 전부 불러오자.
import matplotlib.pyplot as plt
import seaborn as sns
import platform
from matplotlib import font_manager, rc
plt.rcParams["axes.unicode_minus"] = False
rc("font", family = "Malgun Gothic")
# %matplotlib inline
get_ipython().run_line_magic("matplotlib", "inline")
우선 셀프주유소에 따른 단가를 비교해보자. boxplot을 그려주었다.
# boxplot (feat. seaborn)
plt.figure(figsize=(12,8))
sns.boxplot(x="셀프", y="단가", data=stations, palette="Set3")
plt.grid()
plt.show()

확실히 셀프주유소가 일반주유소보다 상대적으로 저렴하다는 것을 확인할 수 있다.
그렇다면, 주유소 상표별로 나뉘었을 때에도 차이가 날까?
plt.figure(figsize=(12,8))
sns.boxplot(x="상표", y="단가",hue="셀프", data=stations, palette="Set3")
plt.grid()
plt.show()

확실히 차이가 난다는 것을 확인할 수 있다.
셀프주유소가 일반주유소보다 저렴하다는 것을 확인했으므로, 이제 자치구별 주유소의 평균단가를 비교해보자. 지도 시각화를 통해서 비교를 해보려고 한다.
import json
import folium
import warnings
# 경고문구 무시하기
warnings.simplefilter(action="ignore", category=FutureWarning)
따로 '자치구별 휘발유 평균단가'의 pivot table을 만들어 주었다.

'단가'별로 내림차순했더니 용산구, 중구, 종로구, 강남구, 성동구 순으로 휘발유 평균 단가가 높음을 확인했다.

이제 지도를 그려서 한 눈에 확인해보자.
geo_path = "../data/02. skorea_municipalities_geo_simple.json"
geo_str = json.load(open(geo_path, encoding="utf-8"))
my_map = folium.Map(location = [37.5502, 126.982], zoon_start=15, tiles="Stamen Toner")
my_map.choropleth(
geo_data = geo_str,
data = gu_data,
columns = [gu_data.index, "단가"],
fill_color = "PuRd",
key_on = "feature.id"
)
my_map

지도로 확인하니, 역시나 용산구의 단가가 제일 높다. 6개의 자치구의 평균 단가가 높다는 것을 확인할 수 있고, 나머지 자치구들은 대략적으로 비슷해 보인다.
.
.
.
.
정리하자면, 2023/09/06 서울특별시 25개 자치구를 기준으로 셀프주유소의 휘발유 단가가 일반 휘발유 단가보다 확실히 저렴하다는 것을 확인할 수 있다. 더 나아가, 자치구들의 평균 휘발유 단가를 확인했을 때 용산구, 중구, 종로구, 강남구, 성동구 순으로 단가가 높음을 확인했다.