Date:     Updated:

카테고리:

태그: , , , , ,


baseball 카테고리에서 similarity scores에 관한 글을 올렸었다. 해당 글에서 만든 엑셀 파일의 데이터를 스탯티즈에서 크롤링해왔는데, 그 방법에 대해 소개하는 글을 써보려한다.

아래 글 참고

[야구 탈럼] Similarity Scores로 닮은꼴 선수 찾아내기 + 스탯티즈 크롤링해서 직접 계산기 만들기


크롬드라이버, selenium, BeautifulSoup 설치

우선 웹 크롤링에 필요한 사전준비를 하자.
가장 먼저 크롬 웹 드라이버를 설치해야 한다. 아래 링크로 들어가서 자신의 크롬 버전에 알맞은 드라이버를 설치해주면 된다. 버전 호환이 안 될 시 실행이 되지 않는다.

ChromeDriver 설치 링크

본인의 크롬 버전을 알고 싶다면 주소 창에 chrome://version을 붙여넣어 보면 확인할 수 있다.

이제 본격적인 모듈 설치를 해보자.

  • selenium: 웹 브라우저 상에서 동적인 움직임을 제어할 수 있게 도와주는 프레임워크. 크롬드라이버를 사용할 수 있게 한다.
  • BeautifulSoup: html과 xml 문서를 parsing하는 패키지

이 두가지 모듈을 조합하여 웹 크롤링을 수행해볼 것이다.

!pip install selenium
import selenium
from selenium import webdriver
from selenium.webdriver import ActionChains

from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By

from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import Select
from selenium.webdriver.support.ui import WebDriverWait

from bs4 import BeautifulSoup

import pandas as pd
import numpy as np

import re

이제 크롬 드라이버를 실행해볼 것이다. excutable_path에는 앞서 설치한 크롬드라이버의 디렉터리 경로를 적어주면 된다.

driver = webdriver.Chrome(executable_path = "C:/Users/희준/downloads/chromedriver_win32/chromedriver.exe")


스탯티즈 크롤링 전체 코드

아래 코드가 전체 코드이다.

for i in range(4):
    url = 'http://www.statiz.co.kr/stat.php?mid=stat&re=0&ys=2022&ye=2022&sn=100&pa={}'.format(i*100)
    driver.get(url)
    driver.implicitly_wait(time_to_wait=5)
    html = driver.find_element(By.XPATH,'//*[@id="mytable"]/tbody').get_attribute("innerHTML")
    soup = BeautifulSoup(html, 'html.parser')

    temp = [i.text.strip() for i in soup.findAll("tr")] #tr태그에서 text만 저장하고 공백 제거
    temp = pd.Series(temp) #list 객체를 series 객체로 변경
    
    #중간중간에 '순'이나 'WAR'로 시작하는 행들이 있는데 이를 제거해준다
    #그리고 index를 reset
    temp = temp[~temp.str.match("[순W]")].reset_index(drop=True)
    
    #띄어쓰기 기준으로 분류해서 데이터프레임으로 만들기
    temp = temp.apply(lambda x: pd.Series(x.split(' ')))
    
    #선수 팀 정보 이후 첫번째 기록과는 space 하나로 구분, 그 이후로는 space 두개로 구분이 되어 있음 
    #그래서 space 하나로 구분을 시키면, 빈 column들이 존재 하는데, 해당 column들 제거 
    temp = temp.replace('', np.nan).dropna(axis=1) 
    
    #WAR이 두 열이나 존재해서 처음 나오는 WAR열을 삭제. 1열에 있음
    temp = temp.drop(1,axis=1)
    
    #선수 이름 앞의 숫자 제거
    temp[0] = temp[0].str.replace("^\d+", "")
    
    #선수들의 생일 정보가 담긴 tag들 가져오기
    birth = [i.find("a") for i in soup.findAll('tr') if 'birth' in i.find('a').attrs['href']]
    
    #tag에서 생일만 추출하기
    p = re.compile("\d{4}")
    birth = [p.findall(i.attrs['href'])[0] for i in birth]
    
    temp['생일']=birth
    
    if i ==0:
        result = temp
    else:
        result = result.append(temp)
        result = result.reset_index(drop=True)
    print(i, "완료")
    
columns = ['선수'] + [i.text for i in soup.findAll("tr")[0].findAll("th")][4:-3] + ['타율','출루율','장타율','OPS','wOBA','wRC+','WAR+','WPA','생년']
result.columns = columns

driver.close()

#선수 이름을 슬라이싱하여 새로운 열로 추가
result['이름'] = result['선수'].map(lambda x:x[:x.find('22')])
#선수 포지션을 슬라이싱하여 새로운 열로 추가
result['포지션'] = result['선수'].map(lambda x:x[x.find('22')+3:])

# 투수교체나 대타 기용 과정에서 타석에 들어선 것으로 처리되는 투수들이 간혹 있음
# 이 투수들의 row를 삭제해주기 위한 과정
pitcher_index = result[result['포지션']=='P'].index
result.drop(pitcher_index, inplace=True)

주석을 달아놓아서 이해에 큰 어려움은 없겠지만, 좀 더 세분화하여 설명해보도록 하자. 참고로 웹 페이지에서 개발자도구(ctrl+shift+i)를 열면 그 페이지에 대한 html 정보를 얻을 수 있어서 과정을 따라오기에 더욱 편리할 것이다.


스탯티즈 사이트에 접근

url로 지정한 부분에서 ys, ye 부분을 주목해보자. ys는 검색하고자 하는 시즌 시작년도이고, ye는 시즌 종료년도이다. 필자는 2022시즌 데이터가 필요하므로 시작과 끝을 2022로 지정했다. sn은 한 페이지에 몇 명의 선수씩 불러올 것인지를 지정한다. pa는 몇 번째 선수부터 시작할지이다. 즉, sn=100, pa=i로 하고 반복문을 4번 돌렸으므로 1~100번째 선수가 크롤링되고, 101~200번째 선수가 크롤링 되는 식으로 이어지다 400번째 선수까지 크롤링될 것이다. 2022시즌 한 타석이라도 들어선 선수는 3xx명이므로 반복문을 4회로 설정했다.

for i in range(4):
    url = 'http://www.statiz.co.kr/stat.php?mid=stat&re=0&ys=2022&ye=2022&sn=100&pa={}'.format(i*100)


BeautifulSoup 객체로 만들기

get() 함수는 검색하고자 하는 url을 불러오는 역할을 한다.
implicitly_wait()는 웹페이지의 로딩을 기다려주는 역할을 하는데, 설정한 시간 동안 로드되지 않으면 에러를 일으킨다.
find_element(By.XPATH)는 xpath 경로를 사용하여 원하는 element를 가져오는 함수이다. 원하는 선수들의 성적이 적힌 테이블은 mytable이란 아이디에 기록이 되어있다. 그 중에서도 tbody라는 요소에 표 형식으로 기록이 되어있다. 즉, 선수들의 정보가 담긴 tbody란 표를 통째로 가져온다.
get_attribute()를 사용해서 element 안의 html 내용들을 모두 가져왔다.
이제 마지막으로 BeautifulSoup 객체에 이 내용들을 담아준다. 이로써 soup 객체가 되어 태그를 추출하기가 수월해진다.

    driver.get(url)
    driver.implicitly_wait(time_to_wait=5)
    html = driver.find_element(By.XPATH,'//*[@id="mytable"]/tbody').get_attribute("innerHTML")
    soup = BeautifulSoup(html, 'html.parser')


series 객체로 변경

findAll() 함수를 사용하여 tr태그들을 불러온다. tr 태그란 행을 뜻한다. 즉, 선수 개개인의 행을 가져온다는 의미이다. 여기서 text.strip() 함수를 사용하여 text만 저장하고 공백들은 없앤다.
분석을 용이하게 하기 위해 윗줄에서 만든 list를 series 객체로 변환해준다.

    temp = [i.text.strip() for i in soup.findAll("tr")] #tr태그에서 text만 저장하고 공백 제거
    temp = pd.Series(temp) #list 객체를 series 객체로 변경


불필요한 부분 제거 전처리

아무래도 웹사이트의 날것 데이터를 가져오다보니 문자열을 다듬는 전처리를 거쳐야 한다. 이제부턴 주석만으로 이해에 큰 어려움이 없을 것이라 따로 설명은 하지 않는다.

    #중간중간에 '순'이나 'WAR'로 시작하는 행들이 있는데 이를 제거해준다
    #그리고 index를 reset
    temp = temp[~temp.str.match("[순W]")].reset_index(drop=True)
    
    #띄어쓰기 기준으로 분류해서 데이터프레임으로 만들기
    temp = temp.apply(lambda x: pd.Series(x.split(' ')))
    
    #선수 팀 정보 이후 첫번째 기록과는 space 하나로 구분, 그 이후로는 space 두개로 구분이 되어 있음 
    #그래서 space 하나로 구분을 시키면, 빈 column들이 존재 하는데, 해당 column들 제거 
    temp = temp.replace('', np.nan).dropna(axis=1) 
    
    #WAR이 두 열이나 존재해서 처음 나오는 WAR열을 삭제. 1열에 있음
    temp = temp.drop(1,axis=1)
    
    #선수 이름 앞의 숫자 제거
    temp[0] = temp[0].str.replace("^\d+", "")


선수 생년 변수 추가

이제 선수들의 생년 정보도 가져온다. 나이를 하나의 변수로 추가하기 위해서이다. re.compile()은 정규표현식이다. 이 함수로 원하는 패턴을 저장해놓는다. 아래 코드에선 “\d{4}”를 입력했는데, 숫자 4자리를 찾아내겠다는 의미다. 즉, `p.findall(i.attrs[‘href’])를 하면 원하는 텍스트에서 숫자 4자리(생년) 정보만 추출할 수 있다.

    #선수들의 생일 정보가 담긴 tag들 가져오기
    birth = [i.find("a") for i in soup.findAll('tr') if 'birth' in i.find('a').attrs['href']]
    
    #tag에서 생년만 추출하기
    p = re.compile("\d{4}")
    birth = [p.findall(i.attrs['href'])[0] for i in birth]
    
    temp['생일']=birth


여러 웹페이지에서 추출한 것들을 합치기

조건문은 앞서 반복해서 로드한 웹페이지에서 추출한 것들을 하나로 합치는 과정이고, 그 밑은 생성된 최종 데이터프레임에 컬럼명을 설정해주는 작업이다. 그리고 driver.close를 하여 로드한 웹페이지를 종료함으로써 크롤링을 마친다!

    if i ==0:
        result = temp
    else:
        result = result.append(temp)
        result = result.reset_index(drop=True)
    print(i, "완료")
    
columns = ['선수'] + [i.text for i in soup.findAll("tr")[0].findAll("th")][4:-3] + ['타율','출루율','장타율','OPS','wOBA','wRC+','WAR+','WPA','생년']
result.columns = columns

driver.close()

아래는 만들어진 결과물이다.

result
선수 G 타석 타수 득점 안타 2타 3타 홈런 루타 ... 출루율 장타율 OPS wOBA wRC+ WAR+ WPA 생년 이름 포지션
0 이정후22키CF 142 627 553 85 193 36 10 23 318 ... .421 .575 .996 .441 182.5 9.23 6.72 1998 이정후 CF
1 피렐라22삼LF 141 630 561 102 192 33 4 28 317 ... .411 .565 .976 .434 169.3 7.40 4.20 1989 피렐라 LF
2 나성범22KRF 144 649 563 92 180 39 2 21 286 ... .402 .508 .910 .411 157.4 6.50 3.74 1989 나성범 RF
3 오지환22LSS 142 569 494 75 133 16 4 25 232 ... .357 .470 .827 .372 138.6 5.77 2.56 1990 오지환 SS
4 최정22S3B 121 505 414 80 110 21 0 26 209 ... .386 .505 .891 .400 145.4 5.15 3.72 1987 최정 3B
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
352 조세진22롯RF 39 88 86 6 16 3 0 0 19 ... .195 .221 .416 .192 5.7 -0.76 -0.64 2003 조세진 RF
353 유로결22한LF 30 68 60 5 8 0 0 0 8 ... .200 .133 .333 .164 -11.4 -0.82 -1.05 2000 유로결 LF
354 정보근22롯C 95 226 199 8 38 2 0 1 43 ... .250 .216 .466 .218 24.1 -0.91 -1.78 1999 정보근 C
355 박경수22K2B 100 194 166 13 20 3 0 3 32 ... .234 .193 .427 .213 21.9 -1.01 -2.16 1984 박경수 2B
356 김헌곤22삼CF 80 239 224 18 43 8 0 1 54 ... .224 .241 .465 .217 24.6 -1.58 -2.24 1988 김헌곤 CF

289 rows × 31 columns

#크롤링한 파일 저장
result.to_excel('statiz_crawl_2022.xlsx', index=False)