이전 글에선 Beautiful soup을 통해서 KBO 기록실 크롤러를 라이트하게 만들어봤는데,
이번엔 Selenium과 Beautiful soup을 활용해서 만들어보려 한다.
Selenium을 활용하기 위해선 자신이 사용하는 브라우저의 Web driver가 필요하다.
내가 가장 자주 사용하는 것이 Chrome이기 때문에 Chrome에 맞춰 진행하려 한다.
(업무상에선 파이어 폭스도 사용할 때가 있기도 하다.)
https://sites.google.com/a/chromium.org/chromedriver/downloads
해당 링크를 통해서 web driver를 다운받을 수 있는데 주의할 점은 자신이 사용하고 있는 브라우저의 버전에 맞춰서 받아야한다는 것이다.
해당 버전 확인은 Setting - About Chrome에 들어가면 설치되어 있는 버전을 확인할 수 있다.
이번 코드는 Beautiful soup에서 작성했던 코드를 최대한 활용해서 진행할 예정이다.
from selenium.webdriver.common.keys import Keys
from selenium import webdriver
from bs4 import BeautifulSoup as bs
from datetime import datetime, timedelta
import time
import pandas as pd
import re
import zipfile
import os
import requests
from tqdm import tqdm
url_base = 'https://www.koreabaseball.com/Record/Player/{category}'
category_list = ['HitterBasic/Basic1.aspx', 'PitcherBasic/Basic1.aspx',
'Defense/Basic.aspx', 'Runner/Basic.aspx']
driver = webdriver.Chrome(r'driver_path/chromedriver')
driver.implicitly_wait(3)
기본적으로 url을 활용해 진행할 예정이다.
기존 Beautiful soup의 경우 바로 웹페이지에 접속했던 반면에 Selenium에서는 웹 드라이버를 통해 가상의 웹페이지를 띄우고 해당 웹 페이지에 접속해 조작(?)하는 방식이다.
이전 Beautiful soup으로 크롤러를 짰을 땐, 단순 당해연도만 수집하게 했는데, Selenium에선 연도별로 수집할 수 있도록 구성할 예정이다.
해당 부분을 검사로 확인해보면 tag는 select이며 name은 블라블라~, class값이 select03인것을 확인할 수 있다.
그리고 추가로 현재 선택된 부분은 selected='selected'로 표시되어 있다.
selenium에선 tag_name, class, xpath,id 등을 활용해 크롤링을 진행할 수 있다.
find_element_by_tag_name, find_elements_by_tag_name의 차이는 전체를 다 보여주느냐 첫번째만 보여주느냐의 차이이다.
기록실 홈페이지를 봤을 때 연도 외에 포지션, 팀 등이 있었기 때문에 tag_name이 select인 경우는 여러 개가 나타났다.
이번엔 class를 사용해보면
위의 그림에서처럼 한 개만 나타나는 것을 알 수 있다.
tag_name을 사용하기 위해선 자신이 원하는 부분이 몇번째에 있는지, 여러 개 중에서 어떤게 내가 원하는 부분인지에 대해 알아야만 쓸 수 있는 반면에 class_name의 경우 그럴 필요없이 바로 사용할 수 있다.
아래의 그림처럼 해당 부분에서의 텍스트를 추출하면 아래와 같이 전체 연도 리스트를 확인할 수 있다.
for category in category_list:
url = url_base.format(category = category)
driver.get(url)
driver.maximize_window()
year_list = [x.strip() for x in driver.find_element_by_class_name('select03').text.split('\n')\
if len(x) >1 ]
줄바꿈을 기준으로 연도를 구분하고 공백과 길이 조건을 통해서 전체 연도 리스트를 수집했다.
그럼 이제 다음 부분은 각 연도별로 선택한 후 변하는 값을 수집하기만 하면 된다.
for year in tqdm(year_list[:3]):
driver.find_element_by_class_name('select03').click()
xpath_format = '//option[@value="{year}"]'
driver.find_element_by_xpath(xpath_format.format(year = year)).click()
아까 연도를 수집했던 부분을 클릭해주고 특정 연도의 값을 클릭해주면 된다.
연도 클릭은 바로 직전 위에서 전체 연도를 수집하기 위해 추출했던 코드 뒤에 .click() 만 추가해주면 된다.
문제는 이후의 특정 연도를 선택하는 것인데, 나는 이부분은 xpath를 활용해서 진행했다.
여기에서 연도 부분을 확인해보면
tag name은 option으로 되어 있으며 value값이 특정 연도로 구성되어 있는 걸 알 수 있다.
xpath는 '//{tag_name}[@{attribute}={value}]'와 같이 구성되어 있다고 할 수 있다.
(위에 attribute의 경우 위에서 사용되었던 class_name이나 id 등도 적용할 수 있기 때문에 활용폭이 크다.
심지어 특정 텍스트 값으로 매칭 시켜서 할 수 도 있다.)
또한, 전체 연도 리스트 가운데 특정 연도 하나씩을 수집해야하기 때문에, year가 변할 때 xpath도 같이 변할 수 있도록 구성했다.
이제 데이터를 수집할 수 있는 조건 설정 부분은 모두 끝이 났다.
남은 건 바로 밑에 테이블을 수집하면 되는데, 이전 Beautiful soup에서는 각 칼럼 수만큼 데이터를 잘라서 수집했다.
하지만 이번엔 테이블을 통으로 가져온 다음 각 행별로 빈 데이터 프레임에 넣는 방식으로 할 예정이다.
테이블의 tag_name은 table인 것을 알 수 있고, 그외 table이라는 tag_name은 더 없었다.
(위에서 마찬가지로 find_elements_by_tag_name('table')을 사용하면 알 수 있다.)
해당 부분의 text 값을 뽑아보면 연도에서처럼 줄바꿈을 기준으로 각 row가 구분되는 것을 알 수 있다.
이후 띄어쓰기를 기준으로 구분하면 데이터 프레임에 바로 넣을 수 있는 하나의 row가 될 수 있다.
사실 쓰는 나도 이해가 잘 안가기 때문에 코드와 결과 값을 추가 했다.
해당 리스트의 0번째를 컬럼명으로 지정하고 이후부터 한 줄 씩 데이터 프레임에 추가하면 된다.
위에 정리한 부분들을 잘 정리해서 돌리면, 결과 값이 아름다울 것 같았는데
실제로 투수 데이터를 수집하는 과정에서 오류가 발생했다.
왜냐하면 칼럼의 수와 데이터의 수가 맞지 않아서 나는 오류 였는데, 이닝을 표기하는 방식 때문이다.
보통 투수의 이닝을 나타날땐 1이닝을 다 채우지 못하면 잡은 아웃카운트에 따라 1/3, 2/3과 같이 표기한다.
근데 이럴 경우 띄어쓰기를 통해서 구분했기 때문에 칼럼 수에 비해 데이터 수가 더 많은 경우가 나타났기 때문에 오류 나타나는 것이었다.
temp_list = [x.split(' ') for x in driver.find_element_by_tag_name('table').text.split('\n')]
temp_data = pd.DataFrame(columns = temp_list[0])
for i,temp in enumerate(temp_list[1:]):
try:
temp_data.loc[i] = temp
time.sleep(0.5)
except:
ip_index = temp_list[0].index('IP')
ip = temp[ip_index]
del temp[13]
temp[13] = ip+' '+temp[13]
temp_data.loc[i] = temp
time.sleep(0.5)
각 row별로 데이터 프레이
각 리스트에 데이터 프레임에 넣는 데 만약 오류가 나는 경우에는 이닝 부분을 삭제하고 다시 업데이트 하는 형태로 수정했다.
오류가 나는 경우 기존 칼럼 명에서 이닝을 나타내는 인덱스 값을 찾고, 이닝 값을 ip 변수에 할당한다.
이후 리스트에서 이닝 값을 삭제해 뒤에 있던 1/3이나 2/3와 같은 값을 이닝의 인덱스로 채운다.
그리고 해당 값과 ip 변수 값을 결합하는 방식으로 했다.
from selenium import webdriver
import time
import pandas as pd
url_base = 'https://www.koreabaseball.com/Record/Player/{category}'
category_list = ['HitterBasic/Basic1.aspx', 'PitcherBasic/Basic1.aspx',
'Defense/Basic.aspx', 'Runner/Basic.aspx']
driver = webdriver.Chrome(r'driver_path/chromedriver')
driver.implicitly_wait(3)
from tqdm import tqdm
baseball_data = []
for category in category_list:
url = url_base.format(category = category)
driver.get(url)
driver.maximize_window()
year_list = [x.strip() for x in driver.find_element_by_class_name('select03').text.split('\n')\
if len(x) >1 ]
category_data_list = []
for year in tqdm(year_list[:3]):
driver.find_element_by_class_name('select03').click()
xpath_format = '//option[@value="{year}"]'
driver.find_element_by_xpath(xpath_format.format(year = year)).click()
time.sleep(0.1)
temp_list = [x.split(' ') for x in driver.find_element_by_tag_name('table').text.split('\n')]
temp_data = pd.DataFrame(columns = temp_list[0])
for i,temp in enumerate(temp_list[1:]):
try:
temp_data.loc[i] = temp
time.sleep(0.5)
except:
ip_index = temp_list[0].index('IP')
ip = temp[ip_index]
del temp[13]
temp[13] = ip+' '+temp[13]
temp_data.loc[i] = temp
time.sleep(0.5)
temp_data['Year'] = year
category_data_list.append(temp_data)
time.sleep(0.1)
time.sleep(1)
category_data = pd.concat(category_data_list)
baseball_data.append(category_data)
사실 급하게 만드느라 주석이고 뭐고 없다.
하지만 한두페이지 정도를 수집하는 것은 큰 어려움이 없을 거라고 생각한다.
이후엔 모은 데이터를 바탕으로 분석하는 과정을 올리고자 한다.
'Analysis Tips' 카테고리의 다른 글
[공공데이터] DB에 저장하고 Flask와 연결해보기 (0) | 2021.01.10 |
---|---|
[공공데이터] 아파트 실거래 매매 API 연결 (0) | 2021.01.03 |
[Python] Beautifulsoup으로 KBO 기록실을 털어보자 -1탄 (6) | 2020.06.21 |
[Python] 네트워크 분석 시각화(networkx+bokeh) (0) | 2020.03.04 |
[Selenium] 체크박스 상태 확인 (0) | 2019.03.30 |
댓글