데이터 사이언스 04 웹자료 가져오기(2)-Selenium

게시: by Creative Commons Licence

1. Selenium

  • BeautifulSoup는 requests 모듈 등으로 접근한 웹주소(URL)에서 웹자료를 쉽게 가져올 수 있지만,

1) Beautiful Soup의 한계 및 Selenium 소개

  • 접근할 웹주소를 알 수 없거나
  • 자바스크립트를 사용하는 경우
  • 웹 브라우저로만 접근해야하는 경우(예를들어, 로그인 등이 필요한 경우)

위의 경우에는 원격 웹브라우저인 selenium이 필요하다. selenium은,

  • 웹 브라우저를 원격 조작하는 도구로서, 자동으로 URL을 열고, 클릭/스크롤/문자입력(로그인 시도)/화면캡쳐 등이 가능하다.

2) Selenium 설치

➡ pip install selenium

  Downloading selenium-3.8.1-py2.py3-none-any.whl (942kB)
    100% |████████████████████████████████| 952kB 851kB/s
Installing collected packages: selenium
Successfully installed selenium-3.8.1

3) 크롬 드라이버 설치

  • 최신 버전은 2.35이나 안정된 버전인 2.30설치함

2. 예제 1 - 네이버 메일 보낸 사람 리스트

  • 네에버 로그인

png

from selenium import webdriver
# 크롬 창 열기
driver = webdriver.Chrome('../crawler/driver/chromedriver')

png

# 네이버 :  이 창은 건들지 않는다.
## 별도의 네이버 창을 연다.
driver.get('http://naver.com')
# 화면 캡쳐
driver.save_screenshot('./data_science/04img/1-3-naver.png')
True
# 네이버 로그인
## id
naver_login = driver.find_element_by_id("id")
naver_login.clear()
naver_login.send_keys("ehfgk")
## pw
naver_login = driver.find_element_by_id("pw")
naver_login.clear()
naver_login.send_keys("@naver2559")
## 로그인버튼 클릭
### Copy Xpath : //*[@id="frmNIDLogin"]/fieldset/span/input
driver.find_element_by_xpath("""//*[@id="frmNIDLogin"]/fieldset/span/input""").click()
# naver 메일
driver.get("http://mail.naver.com")
from bs4 import BeautifulSoup

#현재 페이지 주소
mail_page = driver.page_source
# Soup
soup = BeautifulSoup(mail_page, 'lxml')
# 메일을 보낸 사람 태그
raw_list = soup.find_all('div', 'name _ccr(lst.from) ')
raw_list
[<div class="name _ccr(lst.from) "><span class="blind">보낸 이:</span><a class="_c1(myContextMenu|showSenderContextLayer|list|1931) _stopDefault" href="#" title='"Google" &lt;no-reply@accounts.google.com&gt;'>Google</a></div>,
 <div class="name _ccr(lst.from) "><span class="blind">보낸 이:</span><a class="_c1(myContextMenu|showSenderContextLayer|list|1928) _stopDefault" href="#" title='"Google" &lt;no-reply@accounts.google.com&gt;'>Google</a></div>,
 <div class="name _ccr(lst.from) "><span class="blind">보낸 이:</span><a class="_c1(myContextMenu|showSenderContextLayer|list|1836) _stopDefault" href="#" title='"네이버웹툰 주식회사" &lt;nv_webtoon_noreply@naver.com&gt;'>네이버웹툰 주식회사</a></div>,
 <div class="name _ccr(lst.from) "><span class="blind">보낸 이:</span><a class="_c1(myContextMenu|showSenderContextLayer|list|1717) _stopDefault" href="#" title='"!한국IT전문학교 입시정보!" &lt;navercafe@naver.com&gt;'>!한국IT전문학교 입..</a></div>,
 <div class="name _ccr(lst.from) "><span class="blind">보낸 이:</span><a class="_c1(myContextMenu|showSenderContextLayer|list|1662) _stopDefault" href="#" title='"2030산악회『저알콜서울경기/20대30대/하늘마루2030등산동호회』" &lt;navercafe@naver.com&gt;'>2030산악회『저알..</a></div>,
 <div class="name _ccr(lst.from) "><span class="blind">보낸 이:</span><a class="_c1(myContextMenu|showSenderContextLayer|list|1627) _stopDefault" href="#" title='"워드프레스앤(홈페이지,wordpress,테마,설치,쇼핑몰,강의,강좌)" &lt;navercafe@naver.com&gt;'>워드프레스앤(홈페..</a></div>,
 <div class="name _ccr(lst.from) "><span class="blind">보낸 이:</span><a class="_c1(myContextMenu|showSenderContextLayer|list|1534) _stopDefault" href="#" title='"한국사내변호사회" &lt;navercafe@naver.com&gt;'>한국사내변호사회</a></div>,
 <div class="name _ccr(lst.from) "><span class="blind">보낸 이:</span><a class="_c1(myContextMenu|showSenderContextLayer|list|1472) _stopDefault" href="#" title='"hwang sungyeon" &lt;latarte7@hanmail.net&gt;'>hwang sungyeon</a></div>,
 <div class="name _ccr(lst.from) "><span class="blind">보낸 이:</span><a class="_c1(myContextMenu|showSenderContextLayer|list|1404) _stopDefault" href="#" title='"achilles7" &lt;achilles7@naver.com&gt;'>achilles7</a></div>,
 <div class="name _ccr(lst.from) "><span class="blind">보낸 이:</span><a class="_c1(myContextMenu|showSenderContextLayer|list|1393) _stopDefault" href="#" title='"안나현" &lt;anh2002@hanmail.net&gt;'>안나현</a></div>,
 <div class="name _ccr(lst.from) "><span class="blind">보낸 이:</span><a class="_c1(myContextMenu|showSenderContextLayer|list|1387) _stopDefault" href="#" title='"노영빈" &lt;lawyb@naver.com&gt;'>노영빈</a></div>,
 <div class="name _ccr(lst.from) "><span class="blind">보낸 이:</span><a class="_c1(myContextMenu|showSenderContextLayer|list|1383) _stopDefault" href="#" title='"김경우" &lt;think810525@hotmail.com&gt;'>김경우</a></div>,
 <div class="name _ccr(lst.from) "><span class="blind">보낸 이:</span><a class="_c1(myContextMenu|showSenderContextLayer|list|1357) _stopDefault" href="#" title="&lt;sada111@naver.com&gt;">sada111@naver.com</a></div>,
 <div class="name _ccr(lst.from) "><span class="blind">보낸 이:</span><a class="_c1(myContextMenu|showSenderContextLayer|list|1354) _stopDefault" href="#" title='"낭만시대" &lt;newflag@naver.com&gt;'>낭만시대</a></div>,
 <div class="name _ccr(lst.from) "><span class="blind">보낸 이:</span><a class="_c1(myContextMenu|showSenderContextLayer|list|1353) _stopDefault" href="#" title='"낭만시대" &lt;newflag@naver.com&gt;'>낭만시대</a></div>]
send_list = [ raw_list[n].find('a').get_text() for n in range(0, len(raw_list)) ]
send_list
['Google',
 'Google',
 '네이버웹툰 주식회사',
 '!한국IT전문학교 입..',
 '2030산악회『저알..',
 '워드프레스앤(홈페..',
 '한국사내변호사회',
 'hwang sungyeon',
 'achilles7',
 '안나현',
 '노영빈',
 '김경우',
 'sada111@naver.com',
 '낭만시대',
 '낭만시대']
driver.close()

3. 셀프 주유소가 저렴한가?

1) 대상

# 크롬 창 열기
driver = webdriver.Chrome('../crawler/driver/chromedriver')
# opinet/지역별 주유소 찾기
driver.get("http://www.opinet.co.kr/searRgSelect.do")
# 주유소 목록 구하기: 서울:  구
## 자바스크립트 부분 -  xpath : //*[@id="SIGUNGU_NM0"]
gu_list_raw = driver.find_element_by_xpath( """//*[@id="SIGUNGU_NM0"]""" )
### option들:  elements
gu_list = gu_list_raw.find_elements_by_tag_name("option")
### 확인
gu_names = [ option.get_attribute("value") for option in gu_list ]
gu_names[:5]
['', '강남구', '강동구', '강북구', '강서구']
gu_names.remove('')
gu_names[:5]
['강남구', '강동구', '강북구', '강서구', '관악구']

2) 데이터 얻기

자바스크립트가 작동함.

  • 리스트 박스에서 지역을 선택한다.
  • '조회' 버튼을 누른다. ➔ xpath: //[@id="searRgSelect"] : seaRch gu Select import time '엑셀 저장'버튼을 누른다
import time


# 주유소 - 지역 - 서울 -  id: 자치 구 표시 칸
element = driver.find_element_by_id("SIGUNGU_NM0")
# 강남구 이름 입력
element.send_keys(gu_names[0])
time.sleep(2)

# 조회
element_select_gu = driver.find_element_by_xpath("""//*[@id="searRgSelect"]""")
time.sleep(2)

# 엑셀버튼 누르기
element_get_excel = driver.find_element_by_xpath("""//*[@id="glopopd_excel"]""").click()
for gu in gu_names:
    # 주유소 - 지역 - 서울 -  id: 자치 구 표시 칸
    element = driver.find_element_by_id("SIGUNGU_NM0")
    # 구 이름 입력
    element.send_keys(gu)
    time.sleep(2)

    # 조회
    element_select_gu = driver.find_element_by_xpath("""//*[@id="searRgSelect"]""")
    time.sleep(2)

    # 엑셀버튼 누르기
    element_get_excel = driver.find_element_by_xpath("""//*[@id="glopopd_excel"]""").click()
    time.sleep(2)

    print(gu)
강남구
강동구
강북구
강서구
관악구
광진구
구로구
금천구
노원구
도봉구
동대문구
동작구
마포구
서대문구
서초구
성동구
성북구
송파구
양천구
영등포구
용산구
은평구
종로구
중구
중랑구

3) 데이터 읽기 : glob

  • 파일 집합 다루기

https://docs.python.org/3/library/glob.html

System∑Sec†ion :: 파이썬 – os.glob 모듈 - Tistory

  • 데이터 다듬기

데이터 자료구조 파악하기 - [data].info()

from glob import glob
import pandas as pd

png

glob('./data_ouput/04.oil_stations_in_Seoul/지역*.xls')
['./data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (11).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (20).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (10).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (4).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (19).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (8).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (9).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (21).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (13).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (12).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (3).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (23).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (2).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (22).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (24).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (5).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (1).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (6).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (14).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (17).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (7).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (16).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (18).xls',
 './data_ouput/04.oil_stations_in_Seoul/지역_위치별(주유소) (15).xls']
stations_files = glob('./data_ouput/04.oil_stations_in_Seoul/지역*.xls')
tmp_raw = []

for file_name in stations_files:
    tmp = pd.read_excel(file_name, header=2)
    tmp_raw.append(tmp)

station_raw = pd.concat(tmp_raw)
station_raw.tail()
지역 상호 주소 상표 전화번호 셀프여부 고급휘발유 휘발유 경유 실내등유 셀프
13 서울특별시 SK세원2주유소 서울 성동구 광나루로 184 (성수동1가) SK에너지 02-462-5665 N 1890 1660 1465 - False
14 서울특별시 금호주유소 서울 성동구 금호로 39 (금호동4가) GS칼텍스 02-2297-0066 N - 1698 1478 1260 False
15 서울특별시 동일주유소 서울 성동구 광나루로 254 (성수동2가) 현대오일뱅크 02-461-7100 N 1998 1745 1538 1149 False
16 서울특별시 청계로주유소 서울 성동구 청계천로 454 (하왕십리동) SK에너지 02-2294-4225 N - 1848 1647 1095 False
17 서울특별시 (주)옥수하이웨이스테이션 서울 성동구 독서당로 168 (옥수동) GS칼텍스 02-2282-5151 N 2151 1856 1685 - False
  • 셀프여부 데이터 유형 바꾸기 : string('N'/'Y') ➔ boolean(True/False)
station_raw['셀프'] =  station_raw['셀프여부'] == 'Y'
station_raw.head()
지역 상호 주소 상표 전화번호 셀프여부 고급휘발유 휘발유 경유 실내등유 셀프
0 서울특별시 매일주유소 서울특별시 동작구 상도로 139 (상도동) S-OIL 02-817-4085 N - 1529 1369 1000 False
1 서울특별시 서경주유소 서울 동작구 대림로 46 (신대방동) 현대오일뱅크 02-843-5130 N - 1529 1329 - False
2 서울특별시 대방주유소 서울특별시 동작구 여의대방로214 (대방동) GS칼텍스 02-826-5145 N 1755 1538 1338 950 False
3 서울특별시 한원케미칼(주) 서울특별시 동작구 동작대로 135 (사당동) GS칼텍스 02-2183-3824 Y - 1539 1329 - True
4 서울특별시 금성주유소 서울특별시 동작구 사당로 59 (상도동) 현대오일뱅크 02-825-5151 N - 1539 1329 - False
stations = pd.DataFrame(
    {
        'Oil_store' : station_raw['상호'].values,
        '주소' : station_raw['주소'].values,
        '가격' : station_raw['휘발유'].values,
        '셀프' : station_raw['셀프'].values,
        '상표' : station_raw['상표'].values
    }
)

stations.head()
Oil_store 가격 상표 셀프 주소
0 매일주유소 1529 S-OIL False 서울특별시 동작구 상도로 139 (상도동)
1 서경주유소 1529 현대오일뱅크 False 서울 동작구 대림로 46 (신대방동)
2 대방주유소 1538 GS칼텍스 False 서울특별시 동작구 여의대방로214 (대방동)
3 한원케미칼(주) 1539 GS칼텍스 True 서울특별시 동작구 동작대로 135 (사당동)
4 금성주유소 1539 현대오일뱅크 False 서울특별시 동작구 사당로 59 (상도동)
stations['구'] = [ eachAddress.split()[1] for eachAddress in stations['주소'] ]
stations.head()
Oil_store 가격 상표 셀프 주소
0 매일주유소 1529 S-OIL False 서울특별시 동작구 상도로 139 (상도동) 동작구
1 서경주유소 1529 현대오일뱅크 False 서울 동작구 대림로 46 (신대방동) 동작구
2 대방주유소 1538 GS칼텍스 False 서울특별시 동작구 여의대방로214 (대방동) 동작구
3 한원케미칼(주) 1539 GS칼텍스 True 서울특별시 동작구 동작대로 135 (사당동) 동작구
4 금성주유소 1539 현대오일뱅크 False 서울특별시 동작구 사당로 59 (상도동) 동작구
stations.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 536 entries, 0 to 535
Data columns (total 6 columns):
Oil_store    536 non-null object
가격           536 non-null object
상표           536 non-null object
셀프           536 non-null bool
주소           536 non-null object
구            536 non-null object
dtypes: bool(1), object(5)
memory usage: 21.5+ KB
stations['구'].unique()
array(['동작구', '용산구', '동대문구', '강서구', '영등포구', '노원구', '도봉구', '특별시', '은평구',
       '서대문구', '마포구', '강북구', '중구', '강동구', '종로구', '중랑구', '관악구', '강남구',
       '광진구', '구로구', '서초구', '송파구', '금천구', '성북구', '양천구', '성동구', '서울특별시'], dtype=object)
# 서울특별시,  특별시
stations[ stations['구']=='서울특별시' ]
Oil_store 가격 상표 셀프 주소
529 SK네트웍스(주)효진주유소 1654 SK에너지 False 1 서울특별시 성동구 동일로 129 (성수동2가) 서울특별시
stations[ stations['구']=='특별시' ]
Oil_store 가격 상표 셀프 주소
144 서현주유소 1524 S-OIL True 서울 특별시 도봉구 방학로 142 (방학동) 특별시
#  '서울특별시'가 쓰여진 raw의 '구'부분
stations.loc[ stations['구']=='서울특별시', '구' ] = '성동구'
# '특별시'가 쓰여진 raw의 '구' 부분
stations.loc[ stations['구']=='특별시', '구' ] = '도봉구'
stations['구'].unique()
array(['동작구', '용산구', '동대문구', '강서구', '영등포구', '노원구', '도봉구', '은평구', '서대문구',
       '마포구', '강북구', '중구', '강동구', '종로구', '중랑구', '관악구', '강남구', '광진구', '구로구',
       '서초구', '송파구', '금천구', '성북구', '양천구', '성동구'], dtype=object)
# 가격 부분 : 실수형으로 정하기
stations['가격'] = [float(values) for values in stations['가격']]
---------------------------------------------------------------------------

ValueError                                Traceback (most recent call last)

<ipython-input-68-f062bc7c80cf> in <module>()
      1 # 가격 부분
----> 2 stations['가격'] = [float(values) for values in stations['가격']]


<ipython-input-68-f062bc7c80cf> in <listcomp>(.0)
      1 # 가격 부분
----> 2 stations['가격'] = [float(values) for values in stations['가격']]


ValueError: could not convert string to float: '-'
stations[ stations['가격'] == '-' ]
Oil_store 가격 상표 셀프 주소
118 하나주유소 - S-OIL False 서울특별시 영등포구 도림로 236 (신길동) 영등포구
217 (주)에이앤이청담주유소 - SK에너지 True 서울특별시 강북구 도봉로 155 (미아동) 강북구
248 명진석유(주)동서울주유소 - GS칼텍스 True 서울특별시 강동구 천호대로 1456 (상일동) 강동구
339 SK서광그린주유소 - SK에너지 False 서울특별시 강남구 봉은사로 222(역삼동) 강남구
# 위 부분은 지우기 : 가격 정보가 없으므로,  데이터 분석을 할 수 없음
stations = stations[ stations['가격'] != '-' ]
stations[ stations['가격'] == '-' ]
Oil_store 가격 상표 셀프 주소
# 가격 부분 : 실수형으로 정하기
stations['가격'] = [float(values) for values in stations['가격']]
stations.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 532 entries, 0 to 535
Data columns (total 6 columns):
Oil_store    532 non-null object
가격           532 non-null float64
상표           532 non-null object
셀프           532 non-null bool
주소           532 non-null object
구            532 non-null object
dtypes: bool(1), float64(1), object(4)
memory usage: 25.5+ KB

4) 시각화

import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline

# 한글 문제
import matplotlib.font_manager as fm
## 폰트 적용
font_location = '/usr/share/fonts/truetype/nanum/NanumBarunGothic.ttf'
font_name = fm.FontProperties(fname=font_location).get_name()

from matplotlib import rc
rc('font', family=font_name)
plt.figure( figsize=(12, 8) )
sns.boxplot(
    x='상표',
    y='가격',
    hue='셀프',
    data=stations,
    palette="Set3"
)
sns.swarmplot(
    x='상표',
    y='가격',
    data=stations,
    color='.6'
)
plt.show()

png