본문 바로가기

Minding's Programming/Crawling

[Python/Selenium] Selenium으로 KBO 경기 일정 크롤링하기

728x90
반응형

https://minding-deep-learning.tistory.com/180

 

[Python/Selenium] (업데이트)Selenium으로 KBO 경기 일정 크롤링하기

2024.07.01 - [Minding's Programming/Knowledge] - [Python/Selenium] Selenium으로 KBO 경기 일정 크롤링하기 [Python/Selenium] Selenium으로 KBO 경기 일정 크롤링하기야구장 소개 홈페이지를 만드는 데 경기 일정도 한 페

minding-deep-learning.tistory.com

위 글은 본문의 코드를 보완한 코드이다.

 

야구장 소개 홈페이지를 만드는 데 경기 일정도 한 페이지에 보여주는 곳이 있으면 좋겠다고 생각이 들었다. KBO 홈페이지에 있는 경기 일정을 주기적으로 크롤링해 이 홈페이지에 노출시키고자 하는데, 일단 그 크롤러부터 제작해보았다.

 

처음에는 requests와 BeautifulSoup 등을 활용해 정적 크롤링을 시도하려고 했으나, 월 별로 데이터를 수집하려고 하니 Dropdown 형식으로 월 별 경기일정을 고르게 되어 있어 동적 크롤링인 Selenium을 선택하게 되었다.

 

1. Chrome 및 Selenium 설치

우선 나는 wsl2를 통해 linux 환경인 ubuntu를 사용하고 있었기 때문에, 해당 가상환경에 터미널을 통해 chrome을 설치해주었다.

wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo apt-get update
sudo apt install ./google-chrome-stable_current_amd64.deb

 

그 뒤에 pip 명령어를 통해 selenium도 설치해주었다.

pip install selenium

 

원래 같았으면 이 다음에 설치된 크롬 버전에 맞는 크롬 드라이버를 설치해줬어야 하는데...

이제는 그럴 필요가 없어졌다. selenium 업데이트로 인해 이젠 크롬 드라이버가 필요하지 않다고 한다!

 

사용할 라이브러리는 아래와 같다.

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.select import Select
import pandas as pd

 

 

2. driver로 해당 페이지 접근 및 데이터 수집

이제 selenium의 webdriver를 통해 해당 페이지에 접근하고, 원하는 데이터를 선택해 가져와야한다.

데이터를 수집할 링크는 아래와 같다.

https://www.koreabaseball.com/Schedule/Schedule.aspx

 

KBO 홈페이지

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

www.koreabaseball.com

url = "https://www.koreabaseball.com/Schedule/Schedule.aspx"

options = Options()

# jupyter lab과 VS code에서 실행했는데, 아래 옵션이 없으면 이상하게 에러가 나서 설정했다.
options.add_argument("--headless")
options.add_argument('--no-sandbox')

driver = webdriver.Chrome(options=options)
driver.get(url)

 

위 코드를 통해 웹페이지에 접근했다면, 원하는 데이터만 골라낼 차례다.

# 월 별 데이터 선택 가능한 dropdown 바의 ID
select = Select(driver.find_element(By.ID, "ddlMonth"))
select.select_by_value(month)

# 그리고 경기일정이 있는 부분
table = driver.find_element(By.CLASS_NAME, "tbl-type06")
schedule = table.text

먼저 select 메서드를 통해 월 별 데이터를 선택하는 dropdown 바에서 원하는 달(5월, 6월 등..)을 선택한다.

그 다음 아래 table 형식으로 되어있는 경기 일정을 By 메서드로 class 이름을 지정해 해당 데이터만 긁어온 뒤, schedule이라는 변수에 text형태로 저장해준다. 이렇게까지하면 raw 데이터 수집은 끝이다!

 

3. 데이터 활용하기 좋게 정제하기

"""
날짜 시간 경기 게임센터 하이라이트 TV 라디오 구장 비고
07.02(화) 18:30 롯데vs두산 프리뷰 KN-T 잠실 -
18:30 KIAvs삼성 프리뷰 SPO-T 대구 -
18:30 SSGvsNC 프리뷰 SS-T 창원 -
18:30 LGvs키움 프리뷰 SPO-2T 고척 -
18:30 KTvs한화 프리뷰 MS-T 대전 -
07.03(수) 18:30 롯데vs두산 프리뷰 KN-T 잠실 -
18:30 KIAvs삼성 프리뷰 SPO-T 대구 -
18:30 SSGvsNC 프리뷰 SS-T 창원 -
18:30 LGvs키움 프리뷰 SPO-2T 고척 -
18:30 KTvs한화 프리뷰 MS-T 대전 -
"""

schedule에 저장된 text를 보면 위와 같이 데이터가 수집된 것을 확인할 수 있다. 하지만 이대로는 활용이 어렵기 때문에, 나는 먼저 dataframe으로 변환해 준 뒤 약간의 핸들링을 거쳐 필요한 데이터만 남겨두기로 했다.

 

일단 해결해야 할 문제는 크게 세 가지였다.

  • 데이터에 날짜가 없는 경우: KBO 홈페이지에서는 날짜를 Groupby형식으로 하나로 묶어 보여주기 때문에, 크롤링 했을 때 맨 윗 경기를 제외하고는 날짜 데이터가 없었다.
  • 아직 진행 전인 경기의 경우: 아직 진행 전 경기의 경우 '하이라이트'칼럼에 아무것도 있지 않아 데이터가 한 칸씩 당겨져 나타나는 문제가 있었다.
  • 칼럼 수와 각 칼럼에 들어갈 데이터 수의 불일치 문제: 칼럼 수는 9개 고정인데 비해 위 두 가지의 이유로 데이터가 7가지 또는 8가지인 경우가 생겼다. AssertError가 발생할 수 있다.

 

# 각 줄 별로 나누어서 리스트에 저장
lines = schedule.strip().split('\n')
if lines[1] == '데이터가 없습니다.': # 해당 월 경기 데이터가 없을 경우
	return '0'
    
header = lines[0].split()
rows = []

for line in lines[1:]:
	if line.split()[0].endswith(')'): # '날짜' 칼럼 데이터 위치에 ')'로 끝날경우 날짜 데이터로 판단
		date = line.split(' ')[0] # 해당 날짜 저장(아래 데이터에 추가 위함)
		rows.append(line.split(' '))
	else:
		temp = line.split() # 임시 리스트에 저장한 뒤
		temp.insert(0, date) # 위에서 저장한 날짜 데이터 맨 앞에 추가
		rows.append(temp)

rows.insert(0, header) # 칼럼으로 지정해줄 데이터를 rows 리스트의 맨 앞에 넣어줌

df = pd.DataFrame(rows) # 칼럼을 따로 지정해주지 않은 상태로 df 생성 (AssertError 방지를 위해)

일단 strip()과 split()메서드를 통해 수집한 데이터를 각 행 별로 나누어 리스트에 저장해줬다. 혹시 경기 일정이 업데이트 되지 않은 달이 있을 수 있기 때문에 해당 경우에는 함수의 리턴값을 '0'으로 돌려주도록 했다.

 

칼럼으로 사용할 부분인 lines의 첫 번째 행은 header라는 변수에 미리 리스트 형태로 저장해주었다.

 

그리고 rows라는 리스트에 라인 두번째 줄(인덱스 1)부터 반복문이 돌며 '날짜' 칼럼에 들어갈 데이터가 없는 경우에는 맨 위에 있는 날짜가 해당 위치에 들어갈 수 있도록 했다. (insert를 활용하면 list의 원하는 위치에 데이터를 넣을 수 있다.) 여기서 첫 번째 문제는 해결이다.

 

이렇게까지 하면 칼럼명이 0,1,2... 이런식으로 지정되어 있고 첫 번째 행이 원래 지정하려고 했던 칼럼명이 되면서 데이터프레임이 생성된다. 데이터가 없는 부분은 None 값으로 자동으로 지정된다.

 

df = df.rename(columns=df.iloc[0]) # 첫 번째 행을 칼럼명으로 지정
df = df.drop(df.index[0])

if df['하이라이트'][1] != '하이라이트': # 아직 진행 전인 경기의 경우
	df['라디오'] = df['TV']
	df['TV'] = df['하이라이트']

df = df.fillna('-') # None 값을 '-'로 대체

df = df.drop(['게임센터', '하이라이트', '구장',], axis=1) # 필요없는 칼럼 삭제
df.rename(columns = {"라디오":"구장"}, inplace=True) # 라디오 칼럼명을 구장으로 변경

df.to_json('./app/game_schedule/{0}m_calender.json'.format(month), force_ascii = False, orient='records', indent=4)

driver.quit()

return '1'

우선 첫 번째 행을 칼럼명으로 만들어주기 위해 rename()메서드를 사용했다. 원래 있던 행은 drop()을 통해 삭제했다.

 

그런 뒤 두 번째 문제 해결을 위해 조건문을 설정해주었다. 진행한 경기일 경우 '하이라이트'라는 스트링이 수집되기 때문에, 해당 경우에 한정해서 데이터들을 한 칸씩 뒤로 복사를 통해 옮겨준다.

 

또한 None값들을 '-' 스트링으로 변환해준 뒤 활용하지 않는 칼럼들을 삭제해주었다.

'라디오'에서 '구장'으로 칼럼명을 변경하는 데에는 이유가 있는데, 보통 라디오 중계 정보를 KBO에서 제공하지 않아 빈 칸으로 데이터가 수집되기 때문에, '라디오' 칼럼에 구장명이 적힌다. 따라서 원래 있던 '구장' 칼럼을 없앤 뒤 '라디오' 칼럼의 이름을 변경하는 식으로 진행했다.

 

마지막으로 해당 데이터프레임을 json형태로 저장해주었다. (DB 저장에 용이하도록 하기 위해 json 사용) 저장할 때 꿀팁이 있다면 force_ascii=False를 하면 한글이 깨지지 않은 상태로 저장되며, orinet='records'는 DB 저장에 용이하도록 각 행별로 저장이된다. 또한 Indent=4로 지정해줄 경우 들여쓰기로 인해 json이 보기 좋게 저장된다.

 

이 경기 일정 크롤러를 CLASS 형태로 만들었을 때의 전체 코드는 아래와 같다.

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.select import Select
import pandas as pd

class GameCalCrawler:

    url = "https://www.koreabaseball.com/Schedule/Schedule.aspx"

    def crawling(self, month):
        # selenium 업데이트로 인해 이제 크롬드라이버가 필요없다!
        options = Options()

        options.add_argument("--headless")
        options.add_argument('--no-sandbox')

        driver = webdriver.Chrome(options=options)
        driver.get(self.url)

        select = Select(driver.find_element(By.ID, "ddlMonth"))
        select.select_by_value(month)
        table = driver.find_element(By.CLASS_NAME, "tbl-type06")
        schedule = table.text

        lines = schedule.strip().split('\n')
        if lines[1] == '데이터가 없습니다.': # 해당 월 경기 데이터가 없을 경우
            return '0'
        header = lines[0].split()
        rows = []

        for line in lines[1:]:
            if line.split()[0].endswith(')'): # '날짜' 칼럼 데이터 위치에 ')'로 끝날경우 날짜 데이터로 판단
                date = line.split(' ')[0] # 해당 날짜 저장(아래 데이터에 추가 위함)
                rows.append(line.split(' '))
            else:
                temp = line.split() # 임시 리스트에 저장한 뒤
                temp.insert(0, date) # 위에서 저장한 날짜 데이터 맨 앞에 추가
                rows.append(temp)

        rows.insert(0, header) # 칼럼으로 지정해줄 데이터를 rows 리스트의 맨 앞에 넣어줌

        df = pd.DataFrame(rows)
        df = df.rename(columns=df.iloc[0]) # 첫 번째 행을 칼럼명으로 지정
        df = df.drop(df.index[0])
        if df['하이라이트'][1] != '하이라이트':
            df['라디오'] = df['TV']
            df['TV'] = df['하이라이트']
        df = df.fillna('-')
        df = df.drop(['게임센터', '하이라이트', '구장',], axis=1)
        df.rename(columns = {"라디오":"구장"}, inplace=True)

        df.to_json('./app/game_schedule/{0}m_calender.json'.format(month), force_ascii = False, orient='records', indent=4)

        driver.quit()

        return '1'

if __name__ == "__main__":
    crawler = GameCalCrawler()
    df = crawler.crawling('07')
    # print(df)

기본적으로 월 별 데이터를 수집하는 데, 해당 월 데이터가 없을 경우 '0'의 리턴값을 주어 다음 월로 넘어가게 하고, 있을 경우 '1'을 주어 DB에 저장 및 홈페이지 노출 함수를 작동시키도록 설계했다.

728x90