본문 바로가기
파이썬

파이썬 - KBO 선수 기록 크롤링해서 엑셀로 저장하기

by Tiabet 2023. 2. 20.

작년에 학교에서 통계 수업시간에 팀프로젝트를 하면서 특정 KBO 타자들의 기록을 엑셀 파일로 저장할 일이 생겼었다. 당시 나는 데이터 분석 파트를 맡고 있어서 크롤링은 다른 분이 해주셨는데, 그걸 보면서 참 신기하다는 생각을 했었다. 다만 아쉬웠던 점이 있었다. 그때 분석 팀이 원했던 것은 타자의 1년치 성적을 한 파일에 쭉 나열해서 저장하는 것이었는데, 소통이 잘 안 됐던 건지 크롤링 상의 문제였던 건지 수집 팀이 4월, 5월, 6월 등 월별로 기록을 저장해서 분석 팀에 넘겨줬었다. 그래서 일일이 복사 붙여넣기를 통해 파일을 힘들게 합쳤던 기억이 난다.

 

그래서 이젠 내가 직접 크롤링을 해보고자 한다. 목표는 한 선수의 1년치 기록을 하나의 엑셀파일로 저장하는 것이다. 

 

크롤링을 공부한 책은 '데이터 과학 기반의 파이썬 빅데이터 분석' (2020) 이며, 많은 내용이 책을 참고했다.

http://www.yes24.com/Product/Goods/96652194

 

데이터 과학 기반의 파이썬 빅데이터 분석 - YES24

데이터 과학 방법론으로 배우는 파이썬 빅데이터 분석 프로젝트데이터 과학의 개념, 파이썬 기초, 데이터 크롤링 방법을 익힌 후 14개의 프로젝트를 ‘연구 목표 설정→데이터 수집→데이터 준

www.yes24.com

 

크롤링 가능 여부 확인

크롤링은 개인 혹은 기관이 웹사이트에 올려놓은 데이터를 쉽게 다루기 위해 긁어오는(crwal) 과정이다. 따라서 소유자가 데이터 사용을 금지할 수 있고, 크롤링이 금지된 정보를 가져오는 것은 법적, 윤리적 문제를 일으킬 수 있다. 

 

크롤링 가능 여부를 확인하는 방법은 간단하다. 크롤링하고자 하는 웹사이트의 메인페이지 주소 뒤에 /robots.txt를 붙이기만 하면 된다.

 

나는 KBO의 기록실을 이용할 것이므로, KBO 공식 홈페이지에 들어가 보았다.
https://www.koreabaseball.com/

 

KBO 홈페이지

KBO, 한국야구위원회, 프로야구, KBO 리그, 퓨처스리그, 프로야구순위, 프로야구 일정

www.koreabaseball.com

이 뒤에 robots.txt 를 타이핑하고 엔터를 누르면 아래와 같은 사이트가 나온다.

 

KBO 사이트에 robots.txt 를 해본결과

robots.txt 는 웹사이트의 소유자가 사용자들에게 크롤링이 가능한 범위와 불가능한 범위를 명시적으로 표시해놓은 텍스트파일이다. KBO는 Common, Help, Member, ws 탭이 아니면 모두 크롤링이 가능한 것으로 보인다.

 

사실 처음에는 Netflix를 크롤링해보고 싶었는데, 아래와 같이 떠서 포기했다.

넷플릭스의 robots.txt

맨 위를 확인해보면 개인 유저에 대해선 모든 페이지에 대해 크롤링을 불허하고 있다. 넷플릭스에 있는 특정 장르 영화 목록을 통째로 크롤링해보면 재밌을 것 같은데 아쉬웠다.

 

https://www.koreabaseball.com/Record/Player/PitcherDetail/Basic.aspx?playerId=77637 

 

KBO 홈페이지

KBO, 한국야구위원회, 프로야구, KBO 리그, 퓨처스리그, 프로야구순위, 프로야구 일정

www.koreabaseball.com

내가 크롤링한 데이터의 탭은 /Record 이기 때문에 크롤링 가능함을 알 수 있다.

 

크롤링하기전 HTML 확인

내가 한창 야구를 볼 때 좋아했던 기아의 양현종 선수의 스탯을 크롤링해보기로 했다. KBO는 친절하게도 일자별 기록, 상황별 기록, 경기별 기록 등을 상세하게 알려주고 있다.

 

크롤링을 하려면 웹페이지의 html 구조를 봐야한다. Ctrl + U 키를 눌러서 html을 확인해보자.

 

웹페이지를 구성하고 있는 자바스크립트 코드들이 뜬다. 자바스크립트는 전혀 모르기 때문에 세세한 의미는 알지 못 한다. 중요한 부분, 즉 성적이 기록된 부분을 찾아서 넘어가보자.

538 번째 줄부터 성적이 기록된 코드들이 나온다. 성적이 기록된 칸을 자세히 살펴보면, tbody-tr-td 순으로 블록이 구성되어 있음을 볼 수 있다. tbody는 그 달의 성적, tr은 그 날의 성적, td는 539번째 줄에 나와있는 대로 평균자책점, 타자수, .... 등이 기록되어 있음을 알 수 있다. 그 앞에 날짜, 상대팀, 선발-구원 유무, 승패 등도 기록되어 있음을 확인할 수 있다.

 

html의 구조를 확인했으니 코드를 짤 수 있다. 

 

크롤링 파이썬 코드

크롤링에 많이 쓰이는 패키지 중 하나인 bs4의 BeautifulSoup을 사용해서 크롤링을 진행해보았다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from bs4 import BeautifulSoup
import urllib.request
 
result = []
 
Stats_url='https://www.koreabaseball.com/Record/Player/PitcherDetail/Daily.aspx?playerId=77637'
html = urllib.request.urlopen(Stats_url)
 
print(html)
 
soupStats=BeautifulSoup(html,'html.parser')
 
print(soupStats.prettify())
 
cs

결과를 저장할 result 리스트를 미리 선언해놓았다. 그리고 정보를 추출하고자 하는 url을 문자열로 저장한 뒤 urllib의 request 함수를 이용하여 urlopen을 해서 html 코드를 불러올 수 있었다. 그리고 BeautifulSoup 함수의 html.parser로 html의 구조를 나누고, prettify 함수로 이를 구체적으로 확인할 수 있었다. 

 

 

1
2
3
4
tag_tbody=soupStats.find_all('tbody')
 
print(tag_tbody)
 
cs

 

tbody 블록이 매달 자료들을 갖고 있으므로, 이 tbody 블록들을 모두 찾아주는 find_all 함수를 사용했다. 이렇게 하면 기록을 월별로 묶어서 리스트로 저장되었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import datetime
format = '%Y.%m.%d'
 
for tbody in tag_tbody:
    for stat in tbody.find_all('tr'):
        stat_td=stat.find_all('td')
        stat_date="2022."+stat_td[0].string
        dt_datetime = datetime.datetime.strptime(stat_date,format)#경기날짜
        stat_opposite=stat_td[1].string #상대
        stat_winloss=stat_td[3].string #승패
        stat_INN=stat_td[6].string #이닝
        stat_H=stat_td[7].string #안타
        stat_HR=stat_td[8].string #홈런
        stat_BB=stat_td[9].string #볼넷
        stat_HBP=stat_td[10].string #사구
        stat_SO=stat_td[11].string #삼진
        stat_R=stat_td[12].string #실점
        stat_ERA=stat_td[14].string #시즌평균자책점
        result.append([dt_datetime]+[stat_opposite]+[stat_winloss]+[stat_INN]+
                  [stat_H]+[stat_HR]+[stat_BB]+[stat_HBP]+[stat_SO]+[stat_R]+[stat_ERA])
cs

tag_tbody가 월별로 기록을 갖고 있는 리스트 이므로, 이 안에서 일별 기록만 뽑아내기 위해 for문을 한 번 사용했다. 그러면 일별 기록에서 필요한 정보들만을 뽑아낼 수 있게 된다. 이 과정에서 역시 다시한번 find_all('tr') 함수와 for문을 사용했고, stat_td에 다시 find_all('td') 함수를 통해 일별 기록을 리스트형으로 저장했다. 경기 날짜는 포맷을 datetime으로 고쳐주었고, 나머지는 모두 string 형으로 자료를 저장했다. 마지막으로 앞서 생성한 result에 append 해주었다. 참고로 필요없는 사소한 성적은 몇 개 뺐다.

 

 

1
2
3
4
5
6
7
import pandas as pd
 
df = pd.DataFrame(result,columns=('날짜''상대''승패','이닝','안타','홈런','볼넷','사구','삼진','실점','시즌평균자책점'))
df.to_csv('./양현종.csv',encoding='cp949',mode='w',index=False)
 
result=[] #result 초기화
 
cs

이제 pandas를 통해 이를 dataframe으로 고쳐주고, 알맞은 열 이름을 써준 뒤 csv 파일로 저장했다. 재사용을 위해 result는  마지막에 초기화 해주었다.

 

결과 확인

양현종 선수의 2022 시즌 기록표

내가 원하는대로 1년치 기록이 모두 저장되었다. 다시 말하지만 작년 팀플 땐 월별로밖에 없어서 일일이 복붙해서 합쳤었다. 다만 그때는 다른 데이터랑 합치는 일이 있어서 그랬던 것이 아닌가 싶기도 하다.

 

한 선수만 하고 넘어가면 아쉬워서 몇 명 더 해봤다. url만 바꿔주면 돼서 아주 간단하게 할 수 있었다.

김광현 선수의 2022 시즌 기록표

타자도 result 에 저장하는 방식만 바꿔주면 원하는 대로 자료를 저장할 수 있었다. 코드는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from bs4 import BeautifulSoup
import urllib.request
import datetime
import pandas as pd
 
result = []
 
Stats_url='https://www.koreabaseball.com/Record/Player/HitterDetail/Daily.aspx?playerId=62947'
html = urllib.request.urlopen(Stats_url)
soupStats=BeautifulSoup(html,'html.parser')
 
tag_tbody=soupStats.find_all('tbody')
 
format = '%Y.%m.%d'
 
for tbody in tag_tbody:
    for stat in tbody.find_all('tr'):
        stat_td=stat.find_all('td')
        stat_date="2022."+stat_td[0].string
        dt_datetime = datetime.datetime.strptime(stat_date,format)#경기날짜
        stat_opposite=stat_td[1].string #상대
        stat_PA=stat_td[3].string #타격
        stat_AB=stat_td[4].string #타수
        stat_R=stat_td[5].string #득점
        stat_H=stat_td[6].string #안타
        stat_2B=stat_td[7].string #2루타
        stat_3B=stat_td[8].string #3루타
        stat_HR=stat_td[9].string #홈런
        stat_RBI=stat_td[10].string #타점
        stat_SB=stat_td[11].string #도루
        stat_BB=stat_td[13].string #볼넷
        stat_HBP=stat_td[14].string #사구
        stat_SO=stat_td[15].string #삼진
        stat_GDP=stat_td[16].string #병살타
        stat_AVG=stat_td[17].string #시즌타율
        result.append([dt_datetime]+[stat_opposite]+[stat_PA]+[stat_AB]+
                  [stat_R]+[stat_H]+[stat_2B]+[stat_3B]+[stat_HR]+[stat_RBI]+
                      [stat_SB]+[stat_BB]+[stat_HBP]+[stat_SO]+[stat_GDP]+[stat_AVG])
        
df = pd.DataFrame(result,columns=('날짜''상대''타격','타수','득점','안타','2루타',
                                  '3루타','홈런','타점','도루','볼넷','사구','삼진','병살타','시즌타율'))
df.to_csv('./나성범.csv',encoding='cp949',mode='w',index=False)
 
result=[] #result 초기화
cs

나성범 선수의 2022 시즌 기록표
이정후 선수의 2022 시즌 기록표

 

결론

이렇게 파이썬을 통해 선수들의 기록을 크롤링해보았다. 공부한 내용에 따르면 Selenium 이라는 패키지로 동적 웹크롤링을 진행할 수도 있는데, 아직 적절한 예시를 찾지 못 했다.

 

크롤링에서 제일 중요한 부분은 파이썬보단 자바스크립트 html 코드를 해석하는 일 같다. 물론 친절하게도 html에 tbody 와 tr, td 이런 식으로 어느정도 형식이 다 통일되어 있는 것 같다. 디테일한 부분만 수정해가면서 앞으로 크롤링을 진행해보면 될 것 같다.