[OPGG] 인턴 연계 과정 - 프로 리그 데이터 수집

Updated:

오피지지 인턴 과정

오피지지 데이터 분석가 과정 교육을 수료하고 우수 교육생으로 한 달간 인턴 생활을 하게 되었습니다.

회사의 실무를 보면서 개인적으로 진행했던 과정에 대해 정리하고자 합니다.

본 내용들은 실제 인턴 생활에서 맡았던 업무와 관련 있다고 생각한 부분을 개인적으로 진행한 것 입니다.

실제 맡은 업무와는 상이할 수 있으니 참고 바랍니다.

프로 리그 데이터 수집

여기서는 스크래핑을 이용해서 리그 오브 레전드 프로 리그의 데이터를 수집하고자 합니다.

현재 개인이 Riot API를 이용해서 프로 리그 데이터를 가져올 수 없기 때문에 스크래핑을 사용했습니다.

1. 스크래핑 사이트

스크래핑 사이트로는 QWER.GG를 사용했습니다.

한국 뿐 아니라 여러 리그의 경기 정보가 담겨있는 사이트입니다.

  • 홈페이지 상단에 LCK, LPL등 다양한 리그가 있습니다.

  • LCK를 클릭했을 때 모습입니다.

  • 연도별 롤드컵 결정전, 섬머, 스프링 등을 클릭하여 경기 결과 요약을 확인 할 수 있습니다.

  • 사진에는 보이지 않지만 경기 결과 하단에는 더보기 버튼이 있어 이전 경기 결과도 확인 가능합니다.

  • 경기를 클릭하면 다음과 같이 상세 정보가 나타납니다.

  • 세트별 정보, 룬 등의 상세 정보는 다시 클릭을 통해 확인이 가능합니다.

2. 수집 방법

우선 사이트의 특성 상 클릭을 해야 정보가 생기는 부분이 많아 selenium을 사용해야 합니다.

이를 이용해서 각 경기 결과의 정보를 가져와 보겠습니다.

[데이터 수집]

import numpy as np
import pandas as pd
from pandas.api.types import CategoricalDtype

import requests
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver import ActionChains

import time

import pytube

import warnings
warnings.filterwarnings("ignore")
# 원하는 형태의 url 만들기
want_season = "LCK 2021 Summer"

url_sp = want_season.split(" ")
league = url_sp.pop(0)
year = url_sp.pop(0)

if (url_sp[-1] == "Summer") | (url_sp[-1] == "Spring") | (url_sp[-1] == "Winter"):
    season = url_sp[-1]
else:
    season = "/".join(url_sp)

url = f"https://qwer.gg/leagues/{league}/{year}/{season}"
print(url)
https://qwer.gg/leagues/LCK/2021/Summer
  • 우선 url의 형태는 특정 리그, 연도, 토너먼트를 입력하여야 합니다.

  • 예시: https://qwer.gg/leagues/LCK/2021/summer

  • 여기서 롤드컵 결정전은 Regional-finals로 입력하여야 합니다.

  • 사실 if문을 안써도 상관없지만 다른 작업과 병행하면서 예외 처리로 넣어둔 것이니 무시해도 됩니다.

# 크롬 드라이버 옵션
options = webdriver.ChromeOptions()
options.add_argument('headless')
options.add_argument("--window-size=1920x1080")

# 크롬 드라이버 실행
browser = webdriver.Chrome("./chromedriver.exe", options= options)
browser.maximize_window()
browser.get(url)

# 더보기 클릭하여 모든 링크 찾기
while True:
    try:
        WebDriverWait(browser,5).until(
            EC.presence_of_element_located( 
                (By.XPATH, '//*[@id="root"]/div[3]/div[2]/div/div[4]/div/section[1]/section[1]/div[2]/div[2]/div/div[2]/button') )).click()
    except:
        break
        time.sleep(2)
        
# 링크 저장
soup = BeautifulSoup(browser.page_source, "lxml")
browser.quit()
soup2 = soup.find("div", attrs={"class":"Tournament__records"}).find("div", attrs={"class":"MatchItemList"})
  • 사이트에 접속하였다면 더보기 버튼을 계속 클릭하여 모든 경기 결과 요약 정보를 확인해야합니다.

  • 누르지 않는다면 해당 사이트에 다른 경기 정보는 없습니다.

  • 더 이상 경기가 없을 때까지 버튼을 클릭 후 페이지 정보를 soup에 저장하고 브라우저를 종료합니다.

  • 경기 결과 요약 정보가 있는 전체 테이블 정보를 soup2에 저장합니다.

  • 왜 해당 태그들이나 클래스를 썼는지는 직접 확인하면서 작업합니다.

# 경기별 url
url_lst = []
for i in soup2.find_all("a", attrs={"class":"MatchItem__link"}):
    url_lst.append("https://qwer.gg" + i["href"])
print(f"총 매치 수: {len(url_lst)}")
총 매치 수: 95
url_lst[:5]
['https://qwer.gg/matches/XSDD-fIWa/2021-06-09-BRO-vs-LSB',
 'https://qwer.gg/matches/IsULOWisZC/2021-06-09-T1-vs-HLE',
 'https://qwer.gg/matches/DlJQeVq38J/2021-06-10-KT-vs-NS',
 'https://qwer.gg/matches/FQOaDQ--GT/2021-06-10-GEN-vs-DRX',
 'https://qwer.gg/matches/DbzcjTdnct/2021-06-11-AF-vs-BRO']
  • LCK 2021 Summer 시즌에는 총 95개의 매치(set 아님)가 있었습니다.

  • 가장 첫 번째 경기는 프레딧 브리온과 리브샌드박스의 경기네요.

  • 각 경기 결과의 상세 정보 url은 url_lst에 저장하였습니다.

def match_url(want_season):
    # 원하는 형태의 url 만들기
    url_sp = want_season.split(" ")
    league = url_sp.pop(0)
    year = url_sp.pop(0)

    if (url_sp[-1] == "Summer") | (url_sp[-1] == "Spring") | (url_sp[-1] == "Winter"):
        season = url_sp[-1]
    else:
        season = "/".join(url_sp)

    url = f"https://qwer.gg/leagues/{league}/{year}/{season}"

    # 크롬 드라이버 옵션
    options = webdriver.ChromeOptions()
    options.add_argument('headless')
    options.add_argument("--window-size=1920x1080")

    # 크롬 드라이버 실행
    browser = webdriver.Chrome("./chromedriver.exe", options= options)
    browser.maximize_window()
    browser.get(url)

    # 더보기 클릭하여 모든 링크 찾기
    while True:
        try:
            WebDriverWait(browser,5).until(
                EC.presence_of_element_located( 
                    (By.XPATH, '//*[@id="root"]/div[3]/div[2]/div/div[4]/div/section[1]/section[1]/div[2]/div[2]/div/div[2]/button') )).click()
        except:
            break
            time.sleep(2)

    # 링크 저장
    soup = BeautifulSoup(browser.page_source, "lxml")
    browser.quit()
    soup2 = soup.find("div", attrs={"class":"Tournament__records"}).find("div", attrs={"class":"MatchItemList"})

    # 경기별 url
    url_lst = []
    for i in soup2.find_all("a", attrs={"class":"MatchItem__link"}):
        url_lst.append("https://qwer.gg" + i["href"])
        
    return url_lst
url_lst = match_url(want_season="LCK 2021 summer")
  • 좀 전까지의 모든 과정을 하나의 함수로 정의합니다.
# 경기 상세 정보 url
url = url_lst[0]

# 크롬 드라이버 옵션
options = webdriver.ChromeOptions()
options.add_argument('headless')
options.add_argument("--window-size=1920x1080")

# 크롬 드라이버 실행
browser = webdriver.Chrome("./chromedriver.exe", options= options)
browser.get(url)
browser.maximize_window()

# 채팅 닫기
browser.find_element_by_css_selector('#root > div.Chat.Chat--opened > div.Chat__window > div.Chat__header > svg').click()

# 빈 데이터 프레임
final_data = pd.DataFrame()

# 세트 선택
set_num = 1

while True:
    try:
        # 세트 클릭
        WebDriverWait(browser,3).until(
            EC.presence_of_element_located( 
                (By.XPATH, f"//*[contains(text(), 'SET {set_num}')]") )).click()

        time.sleep(2)

        # 챔피언 기본 정보
        soup3 = BeautifulSoup(browser.page_source, "lxml")
        temp = soup3.find("div", attrs={"class":"Tabs__contents"}).find_all("section")[0]

        data = pd.DataFrame()
        for s in range(10):
            info_lst = []

            # 챔피언
            info_lst.append(temp.find_all("li")[s].find("img")["alt"])
            # 레벨
            info_lst.append(temp.find_all("li")[s].find("span").get_text())
            # 스펠
            info_lst.append(temp.find_all("li")[s].find_all("img", attrs={"class":"SpellIcon__image"})[0]["alt"])
            info_lst.append(temp.find_all("li")[s].find_all("img", attrs={"class":"SpellIcon__image"})[1]["alt"])
            # 선수 이름
            info_lst.append(temp.find_all("li")[s].find("a", attrs={"class":"MatchHistoryDetailPlayer__summoner__link"}).get_text())
            # kda
            kda = temp.find_all("li")[s].find("div", attrs={"class":"MatchHistoryDetailPlayer__kda"}).find_all("span")[0].get_text()
            k,d,a = kda.split("/")
            info_lst.append(k), info_lst.append(d), info_lst.append(a)

            # 가한 데미지
            info_lst.append(temp.find_all("li")[s].find("span", attrs={"class":"MatchHistoryDetailPlayer__damage__desktop"}).get_text().replace(",",""))
            # 시야점수
            info_lst.append(temp.find_all("li")[s].find("div", attrs={"class":"MatchHistoryDetailPlayer__vs"}).get_text())
            # CS
            cs_lst = temp.find_all("li")[s].find("div", attrs={"class":"MatchHistoryDetailPlayer__cs"})["title"].split()
            mcs = int(cs_lst[1])
            jcs = int(cs_lst[3])
            cs = mcs + jcs
            info_lst.append(mcs), info_lst.append(jcs), info_lst.append(cs)

            # 아이템
            for i, info in enumerate(temp.find_all("li")[s].find_all("div", attrs={"class":"ItemIcon MatchHistoryDetailPlayer__items__elem"})):
                try:
                    info_lst.append(info.find('img')['alt'])
                except:
                    info_lst.append("None")

            # 장신구
            info_lst.append(temp.find_all("li")[s].find("div", attrs={"class":"ItemIcon MatchHistoryDetailPlayer__items__ward"}).find("img")["alt"])

            # 데이터 프레임화
            data = pd.concat([data, pd.DataFrame(info_lst).T], axis=0)


        # 밴
        ban_lst = []
        for i in range(2):
            ban_info = soup3.find_all("div", attrs={"class":"MatchHistoryDetailBansAndObjects__ban"})[i].find_all("a")
            for j in range(5):
                try:
                    ban_lst.append(ban_info[j].find("img")["alt"])
                except:
                    ban_lst.append("None")
        data["ban"] = ban_lst

        # 진영
        data["side"] = ["blue"]*5 + ["red"]*5

        # 결과
        blue_result = temp.find_all("div", attrs={"class":"MatchHistoryDetailOverall__player"})[0].get_text().split(" ")[0]
        red_result = temp.find_all("div", attrs={"class":"MatchHistoryDetailOverall__player"})[1].get_text().split(" ")[0]

        data["result"] = [blue_result]*5 + [red_result]*5

        # 오브젝트
        temp21 = temp.find_all("div", attrs={"class":"MatchHistoryDetailBansAndObjects__object"})[0]
        temp31 = temp21.find_all("span", attrs={"class":"ObjectIcon MatchHistoryDetailBansAndObjects__object__icon"})
        temp22 = temp.find_all("div", attrs={"class":"MatchHistoryDetailBansAndObjects__object"})[3]
        temp32 = temp22.find_all("span", attrs={"class":"ObjectIcon MatchHistoryDetailBansAndObjects__object__icon"})

        data["turret"] = [temp31[0].get_text()]*5 + [temp32[0].get_text()]*5
        data["inhibitor"] = [temp31[1].get_text()]*5 + [temp32[1].get_text()]*5
        data["baron"] = [temp31[2].get_text()]*5 + [temp32[2].get_text()]*5
        data["dragon"] = [temp31[3].get_text()]*5 + [temp32[3].get_text()]*5
        data["riftHerald"] = [temp31[4].get_text()]*5 + [temp32[4].get_text()]*5

        # 경기 시간, 날짜, 버전, url
        length_lst = temp.find("div", attrs={"class":"MatchHistoryDetail__duration"}).get_text().split(":")
        data["length"] = int(length_lst[0])*60 + int(length_lst[1])
        data["date"] = temp.find("div", attrs={"class":"GameDetail__date"}).get_text().split(" ")[0].replace(".","-")
        data["version"] = temp.find("div", attrs={"class":"GameDetail__date"}).get_text().split(" ")[-1]

        # 룬 정보 클릭
        WebDriverWait(browser,5).until(
            EC.presence_of_element_located( 
                (By.XPATH, f"//*[contains(text(), '룬 / 빌드')]") )).click()
        time.sleep(1.5)

        rune_df = pd.DataFrame()

        # 챔피언별 룬 페이지 클릭
        for i in ['blue', 'red']:
            for j in range(1,6):
                rune_stat_lst = []
                WebDriverWait(browser, 5).until(
                    EC.presence_of_element_located( 
                        (By.CSS_SELECTOR, f'#root > div.Match > div:nth-child(7) > div > div > div.Tabs__contents > section > div.MatchHistoryDetail.GameDetail__matchHistory > section:nth-child(4) > div > div.Tabs__contents > section > div > div.MatchHistoryDetailBuilds__teams > div.MatchHistoryDetailBuilds__teams__{i} > div > div:nth-child({j}) > a > div > img') )).click()
                time.sleep(1)

                soup2 = BeautifulSoup(browser.page_source, "lxml")

                # 메인룬, 서브룬
                rune_stat_lst.append(soup2.find_all("span", attrs={"class":"RuneTree__styleName"})[0].get_text())
                rune_stat_lst.append(soup2.find_all("span", attrs={"class":"RuneTree__styleName"})[1].get_text())

                # 세부 룬 정보
                for k in soup2.find_all("div", attrs={"class":"RuneTree__rune RuneIcon"}):
                    rune_stat_lst.append(k.find("img")["alt"])

                for k in soup2.find_all("div", attrs={"class":"StatPerkIcon RuneTree__perk"}):
                    rune_stat_lst.append(k.find("img")["alt"])

                rune_df = pd.concat([rune_df, pd.DataFrame(rune_stat_lst).T], axis=0)

        # 기본 정보, 룬 정보 결합
        set_data = pd.concat([data, rune_df], axis=1)

        # 최종 데이터
        final_data = pd.concat([final_data, set_data], axis=0)
        set_num += 1
    except:
        browser.find_elements_by_xpath("//*[contains(text(), '전체경기')]")[0].click()
        soup4 = BeautifulSoup(browser.page_source, "lxml")
        final_data["url"] = soup4.find("iframe", attrs={"class":"YoutubeVideo"})["src"]
        browser.quit()
        break
  • 경기 상세 정보 url에 접속했다면 팀별 챔피언, 밴 정보 등의 태그를 확인 후 수집합니다.

  • 이 과정에서 접속시 팝업창 같은 채팅창을 종료하는 코드를 추가했습니다.

  • 룬 페이지를 클릭하여 선수별 룬 정보도 수집합니다.

  • 이 과정을 모든 세트에 대해 반복하며 보통 2~5개 존재할 수 있으므로 while을 이용하여 작업합니다.

  • 직접 작업을 하면서 로딩 시간 등을 고려하여 적절히 sleep등을 활용해야 합니다.

  • 주의 사항으로는 밴의 경우 2021 기준 10개가 맞지만 징계 등으로 인해 밴이 없는 경우가 존재 합니다.

  • 또한 아이템도 6개 + 1개(장신구)가 항상 모두 있진 않을 것입니다.

  • 데이터 프레임으로 만드면서 이러한 예외 처리를 잘 해주어야 합니다.

print(final_data.shape)
(20, 43)
  • 데이터는 잘 20 x 43 형태로 구축되었습니다.
final_data.head(10)
0 1 2 3 4 5 6 7 8 9 ... 2 3 4 5 6 7 8 9 10 url
0 나르 18 점멸 순간이동 BRO Hoya 2 0 9 19967 46 ... 착취의 손아귀 철거 뼈 방패 과잉성장 피의 맛 굶주린 사냥꾼 5005 5008 5002 https://www.youtube.com/embed/rFE21sDpHjs
0 릴리아 16 강타 점멸 BRO UmTi 2 3 13 11555 55 ... 난입 빛의 망토 기민함 물 위를 걷는 자 외상 우주적 통찰력 5008 5008 5002 https://www.youtube.com/embed/rFE21sDpHjs
0 루시안 18 순간이동 점멸 BRO Lava 11 0 5 22265 71 ... 집중 공격 침착 전설: 핏빛 길 최후의 저항 비스킷 배달 시간 왜곡 물약 5005 5008 5003 https://www.youtube.com/embed/rFE21sDpHjs
0 칼리스타 17 회복 점멸 BRO Hena 3 1 10 9735 87 ... 칼날비 피의 맛 좀비 와드 굶주린 사냥꾼 비스킷 배달 우주적 통찰력 5005 5008 5002 https://www.youtube.com/embed/rFE21sDpHjs
0 그라가스 14 점화 점멸 BRO Delight 1 2 11 5349 111 ... 여진 생명의 샘 재생의 바람 불굴의 의지 비스킷 배달 우주적 통찰력 5007 5008 5002 https://www.youtube.com/embed/rFE21sDpHjs
0 리 신 18 순간이동 점멸 LSB Summit 3 2 1 13179 36 ... 정복자 승전보 전설: 강인함 최후의 저항 재생의 바람 소생 5005 5008 5002 https://www.youtube.com/embed/rFE21sDpHjs
0 올라프 14 강타 점멸 LSB Croco 1 6 4 8487 63 ... 정복자 승전보 전설: 민첩함 최후의 일격 마법의 신발 쾌속 접근 5008 5008 5003 https://www.youtube.com/embed/rFE21sDpHjs
0 빅토르 17 순간이동 점멸 LSB FATE 2 4 3 19753 32 ... 콩콩이 소환 마나순환 팔찌 절대 집중 주문 작열 비스킷 배달 시간 왜곡 물약 5005 5008 5002 https://www.youtube.com/embed/rFE21sDpHjs
0 15 회복 점멸 LSB Prince 0 2 3 10324 28 ... 기민한 발놀림 침착 전설: 핏빛 길 최후의 일격 비스킷 배달 시간 왜곡 물약 5008 5008 5002 https://www.youtube.com/embed/rFE21sDpHjs
0 카르마 11 점멸 탈진 LSB Effort 0 5 4 6655 111 ... 신비로운 유성 빛의 망토 절대 집중 주문 작열 비스킷 배달 우주적 통찰력 5008 5008 5002 https://www.youtube.com/embed/rFE21sDpHjs

10 rows × 43 columns

  • 프레딧 브리온과 리브 샌드박스의 경기 결과 정보입니다.

  • 1세트 당 10개의 챔피언이 있고 스펠, 아이템, KDA, 경기 영상 url등이 존재하나 컬럼명이 구분 되지 않습니다.

  • 처음 확인했을 때 20개의 row가 있었으니 두 팀의 결과는 2세트로 2:0 이었겠네요.

def league_data2(url):
    # 크롬 드라이버 옵션
    options = webdriver.ChromeOptions()
    options.add_argument('headless')
    options.add_argument("--window-size=1920x1080")

    # 크롬 드라이버 실행
    browser = webdriver.Chrome("./chromedriver.exe", options= options)
    browser.get(url)
    browser.maximize_window()

    # 채팅 닫기
    browser.find_element_by_css_selector('#root > div.Chat.Chat--opened > div.Chat__window > div.Chat__header > svg').click()

    # 빈 데이터 프레임
    final_data = pd.DataFrame()

    # 세트 선택
    set_num = 1

    while True:
        try:
            # 세트 클릭
            WebDriverWait(browser,3).until(
                EC.presence_of_element_located( 
                    (By.XPATH, f"//*[contains(text(), 'SET {set_num}')]") )).click()
            
            time.sleep(2)

            # 챔피언 기본 정보
            soup3 = BeautifulSoup(browser.page_source, "lxml")
            temp = soup3.find("div", attrs={"class":"Tabs__contents"}).find_all("section")[0]
            
            data = pd.DataFrame()
            for s in range(10):
                info_lst = []
                
                # 챔피언
                info_lst.append(temp.find_all("li")[s].find("img")["alt"])
                # 레벨
                info_lst.append(temp.find_all("li")[s].find("span").get_text())
                # 스펠
                info_lst.append(temp.find_all("li")[s].find_all("img", attrs={"class":"SpellIcon__image"})[0]["alt"])
                info_lst.append(temp.find_all("li")[s].find_all("img", attrs={"class":"SpellIcon__image"})[1]["alt"])
                # 선수 이름
                info_lst.append(temp.find_all("li")[s].find("a", attrs={"class":"MatchHistoryDetailPlayer__summoner__link"}).get_text())
                # kda
                kda = temp.find_all("li")[s].find("div", attrs={"class":"MatchHistoryDetailPlayer__kda"}).find_all("span")[0].get_text()
                k,d,a = kda.split("/")
                info_lst.append(k), info_lst.append(d), info_lst.append(a)
                
                # 가한 데미지
                info_lst.append(temp.find_all("li")[s].find("span", attrs={"class":"MatchHistoryDetailPlayer__damage__desktop"}).get_text().replace(",",""))
                # 시야점수
                info_lst.append(temp.find_all("li")[s].find("div", attrs={"class":"MatchHistoryDetailPlayer__vs"}).get_text())
                # CS
                cs_lst = temp.find_all("li")[s].find("div", attrs={"class":"MatchHistoryDetailPlayer__cs"})["title"].split()
                mcs = int(cs_lst[1])
                jcs = int(cs_lst[3])
                cs = mcs + jcs
                info_lst.append(mcs), info_lst.append(jcs), info_lst.append(cs)
                
                # 아이템
                for i, info in enumerate(temp.find_all("li")[s].find_all("div", attrs={"class":"ItemIcon MatchHistoryDetailPlayer__items__elem"})):
                    try:
                        info_lst.append(info.find('img')['alt'])
                    except:
                        info_lst.append("None")

                # 장신구
                info_lst.append(temp.find_all("li")[s].find("div", attrs={"class":"ItemIcon MatchHistoryDetailPlayer__items__ward"}).find("img")["alt"])
                
                # 데이터 프레임화
                data = pd.concat([data, pd.DataFrame(info_lst).T], axis=0)
                

            # 밴
            ban_lst = []
            for i in range(2):
                ban_info = soup3.find_all("div", attrs={"class":"MatchHistoryDetailBansAndObjects__ban"})[i].find_all("a")
                for j in range(5):
                    try:
                        ban_lst.append(ban_info[j].find("img")["alt"])
                    except:
                        ban_lst.append("None")
            data["ban"] = ban_lst

            # 진영
            data["side"] = ["blue"]*5 + ["red"]*5

            # 결과
            blue_result = temp.find_all("div", attrs={"class":"MatchHistoryDetailOverall__player"})[0].get_text().split(" ")[0]
            red_result = temp.find_all("div", attrs={"class":"MatchHistoryDetailOverall__player"})[1].get_text().split(" ")[0]

            data["result"] = [blue_result]*5 + [red_result]*5

            # 오브젝트
            temp21 = temp.find_all("div", attrs={"class":"MatchHistoryDetailBansAndObjects__object"})[0]
            temp31 = temp21.find_all("span", attrs={"class":"ObjectIcon MatchHistoryDetailBansAndObjects__object__icon"})
            temp22 = temp.find_all("div", attrs={"class":"MatchHistoryDetailBansAndObjects__object"})[3]
            temp32 = temp22.find_all("span", attrs={"class":"ObjectIcon MatchHistoryDetailBansAndObjects__object__icon"})

            data["turret"] = [temp31[0].get_text()]*5 + [temp32[0].get_text()]*5
            data["inhibitor"] = [temp31[1].get_text()]*5 + [temp32[1].get_text()]*5
            data["baron"] = [temp31[2].get_text()]*5 + [temp32[2].get_text()]*5
            data["dragon"] = [temp31[3].get_text()]*5 + [temp32[3].get_text()]*5
            data["riftHerald"] = [temp31[4].get_text()]*5 + [temp32[4].get_text()]*5

            # 경기 시간, 날짜, 버전, url
            length_lst = temp.find("div", attrs={"class":"MatchHistoryDetail__duration"}).get_text().split(":")
            data["length"] = int(length_lst[0])*60 + int(length_lst[1])
            data["date"] = temp.find("div", attrs={"class":"GameDetail__date"}).get_text().split(" ")[0].replace(".","-")
            data["version"] = temp.find("div", attrs={"class":"GameDetail__date"}).get_text().split(" ")[-1]

            # 룬 정보 클릭
            WebDriverWait(browser,5).until(
                EC.presence_of_element_located( 
                    (By.XPATH, f"//*[contains(text(), '룬 / 빌드')]") )).click()
            time.sleep(1.5)
            
            rune_df = pd.DataFrame()
            
            # 챔피언별 룬 페이지 클릭
            for i in ['blue', 'red']:
                for j in range(1,6):
                    rune_stat_lst = []
                    WebDriverWait(browser, 5).until(
                        EC.presence_of_element_located( 
                            (By.CSS_SELECTOR, f'#root > div.Match > div:nth-child(7) > div > div > div.Tabs__contents > section > div.MatchHistoryDetail.GameDetail__matchHistory > section:nth-child(4) > div > div.Tabs__contents > section > div > div.MatchHistoryDetailBuilds__teams > div.MatchHistoryDetailBuilds__teams__{i} > div > div:nth-child({j}) > a > div > img') )).click()
                    time.sleep(1)
                    
                    soup2 = BeautifulSoup(browser.page_source, "lxml")

                    # 메인룬, 서브룬
                    rune_stat_lst.append(soup2.find_all("span", attrs={"class":"RuneTree__styleName"})[0].get_text())
                    rune_stat_lst.append(soup2.find_all("span", attrs={"class":"RuneTree__styleName"})[1].get_text())

                    # 세부 룬 정보
                    for k in soup2.find_all("div", attrs={"class":"RuneTree__rune RuneIcon"}):
                        rune_stat_lst.append(k.find("img")["alt"])

                    for k in soup2.find_all("div", attrs={"class":"StatPerkIcon RuneTree__perk"}):
                        rune_stat_lst.append(k.find("img")["alt"])

                    rune_df = pd.concat([rune_df, pd.DataFrame(rune_stat_lst).T], axis=0)

            # 기본 정보, 룬 정보 결합
            set_data = pd.concat([data, rune_df], axis=1)

            # 최종 데이터
            final_data = pd.concat([final_data, set_data], axis=0)
            set_num += 1
        except:
            browser.find_elements_by_xpath("//*[contains(text(), '전체경기')]")[0].click()
            soup4 = BeautifulSoup(browser.page_source, "lxml")
            final_data["url"] = soup4.find("iframe", attrs={"class":"YoutubeVideo"})["src"]
            browser.quit()
            break
    
    return final_data
  • 우선 지금까지의 과정을 함수로서 정의합니다.
def data_export(season):
    # 모든 매치 URL
    url_lst = match_url(want_season = season)
    print("총 매치수:", len(url_lst))

    # 경기 정보 쌓기
    stack_data = pd.DataFrame()

    total_duration = 0
    for i, url in enumerate(url_lst):
        start_time = time.time()

        data = league_data2(url)
        stack_data = pd.concat([stack_data, data], axis=0)

        duration = round(time.time() - start_time, 0)
        total_duration += duration
        print(f"{i}번째 경기 수집 완료: {duration}초")

    print(f"총 수집 시간: {round(total_duration/60,0)}분")

    # 컬럼명 수정
    stack_data.columns = ["champion", "level", "spell1", "spell2", "summonerName", "K", "D", "A", "TotalDamage",
                           "vision_score", "minion_cs", "jungle_cs", "total_cs", 
                           "item1", "item2", "item3", "item4", "item5", "item6", "item7", 
                           "ban", "side", "result", "turret", "inhibitor", "baron", "dragon", "riftHerald",
                           "length", "date", "version", 
                           "main_rune", "sub_rune", "rune1", "rune2", "rune3", "rune4", "rune5", "rune6",
                           "stat1", "stat2", "stat3", "url"]
    
    return stack_data
  • 앞선 과정으로 특정 리그, 연도, 토너먼트의 모든 상세 경기 url을 수집하였습니다.

  • 그리고 하나의 상세 경기에 대해 결과 정보를 수집해보았습니다.

  • 이제는 이를 통합하여 전체 정보를 가져오면 됩니다.

  • 기타 컬럼명 등의 수정 작업도 함께 진행합니다.

season = "LCK 2021 Summer"
season2 = season.replace(" ", "_").replace("-", "_")
exec(f"{season2} = data_export(season)")
총 매치수: 95
0번째 경기 수집 완료: 43.0초
1번째 경기 수집 완료: 43.0초
2번째 경기 수집 완료: 61.0초
3번째 경기 수집 완료: 59.0초
4번째 경기 수집 완료: 61.0초
5번째 경기 수집 완료: 60.0초
6번째 경기 수집 완료: 58.0초
7번째 경기 수집 완료: 46.0초
8번째 경기 수집 완료: 45.0초
9번째 경기 수집 완료: 45.0초
10번째 경기 수집 완료: 62.0초
11번째 경기 수집 완료: 57.0초
12번째 경기 수집 완료: 46.0초
13번째 경기 수집 완료: 45.0초
14번째 경기 수집 완료: 45.0초
15번째 경기 수집 완료: 60.0초
16번째 경기 수집 완료: 43.0초
17번째 경기 수집 완료: 59.0초
18번째 경기 수집 완료: 43.0초
19번째 경기 수집 완료: 58.0초
20번째 경기 수집 완료: 44.0초
21번째 경기 수집 완료: 59.0초
22번째 경기 수집 완료: 60.0초
23번째 경기 수집 완료: 49.0초
24번째 경기 수집 완료: 43.0초
25번째 경기 수집 완료: 57.0초
26번째 경기 수집 완료: 48.0초
27번째 경기 수집 완료: 44.0초
28번째 경기 수집 완료: 45.0초
29번째 경기 수집 완료: 58.0초
30번째 경기 수집 완료: 58.0초
31번째 경기 수집 완료: 59.0초
32번째 경기 수집 완료: 44.0초
33번째 경기 수집 완료: 59.0초
34번째 경기 수집 완료: 59.0초
35번째 경기 수집 완료: 42.0초
36번째 경기 수집 완료: 62.0초
37번째 경기 수집 완료: 60.0초
38번째 경기 수집 완료: 58.0초
39번째 경기 수집 완료: 44.0초
40번째 경기 수집 완료: 46.0초
41번째 경기 수집 완료: 50.0초
42번째 경기 수집 완료: 60.0초
43번째 경기 수집 완료: 56.0초
44번째 경기 수집 완료: 41.0초
45번째 경기 수집 완료: 60.0초
46번째 경기 수집 완료: 60.0초
47번째 경기 수집 완료: 44.0초
48번째 경기 수집 완료: 44.0초
49번째 경기 수집 완료: 43.0초
50번째 경기 수집 완료: 42.0초
51번째 경기 수집 완료: 61.0초
52번째 경기 수집 완료: 45.0초
53번째 경기 수집 완료: 44.0초
54번째 경기 수집 완료: 58.0초
55번째 경기 수집 완료: 60.0초
56번째 경기 수집 완료: 60.0초
57번째 경기 수집 완료: 44.0초
58번째 경기 수집 완료: 60.0초
59번째 경기 수집 완료: 60.0초
60번째 경기 수집 완료: 44.0초
61번째 경기 수집 완료: 62.0초
62번째 경기 수집 완료: 41.0초
63번째 경기 수집 완료: 60.0초
64번째 경기 수집 완료: 59.0초
65번째 경기 수집 완료: 61.0초
66번째 경기 수집 완료: 61.0초
67번째 경기 수집 완료: 43.0초
68번째 경기 수집 완료: 60.0초
69번째 경기 수집 완료: 46.0초
70번째 경기 수집 완료: 59.0초
71번째 경기 수집 완료: 61.0초
72번째 경기 수집 완료: 60.0초
73번째 경기 수집 완료: 44.0초
74번째 경기 수집 완료: 44.0초
75번째 경기 수집 완료: 44.0초
76번째 경기 수집 완료: 61.0초
77번째 경기 수집 완료: 60.0초
78번째 경기 수집 완료: 42.0초
79번째 경기 수집 완료: 57.0초
80번째 경기 수집 완료: 41.0초
81번째 경기 수집 완료: 46.0초
82번째 경기 수집 완료: 43.0초
83번째 경기 수집 완료: 60.0초
84번째 경기 수집 완료: 59.0초
85번째 경기 수집 완료: 45.0초
86번째 경기 수집 완료: 44.0초
87번째 경기 수집 완료: 61.0초
88번째 경기 수집 완료: 44.0초
89번째 경기 수집 완료: 44.0초
90번째 경기 수집 완료: 75.0초
91번째 경기 수집 완료: 57.0초
92번째 경기 수집 완료: 61.0초
93번째 경기 수집 완료: 77.0초
94번째 경기 수집 완료: 76.0초
총 수집 시간: 84.0분
  • 보시다시피 시간이 매우 오래 걸립니다.

  • 아무래도 selnium으로 정보를 확인하는 점에서 감안해야할 부분입니다.

  • 다만 현재는 특정 시즌 전체 정보를 수집하기에 하나의 매치 단위로는 대략 1분 정도면 괜찮아 보입니다.

  • 필요에 따라 매치 단위 url을 넣는 방식으로 사용하면 쉽게 정보를 확인할 수 있겠네요.

  • 혹은 룬 정보 등을 포기한다거나 할 수도 있겠어요.

LCK_2021_Summer
champion level spell1 spell2 summonerName K D A TotalDamage vision_score ... rune1 rune2 rune3 rune4 rune5 rune6 stat1 stat2 stat3 url
0 나르 18 점멸 순간이동 BRO Hoya 2 0 9 19967 46 ... 착취의 손아귀 철거 뼈 방패 과잉성장 피의 맛 굶주린 사냥꾼 5005 5008 5002 https://www.youtube.com/embed/rFE21sDpHjs
0 릴리아 16 강타 점멸 BRO UmTi 2 3 13 11555 55 ... 난입 빛의 망토 기민함 물 위를 걷는 자 외상 우주적 통찰력 5008 5008 5002 https://www.youtube.com/embed/rFE21sDpHjs
0 루시안 18 순간이동 점멸 BRO Lava 11 0 5 22265 71 ... 집중 공격 침착 전설: 핏빛 길 최후의 저항 비스킷 배달 시간 왜곡 물약 5005 5008 5003 https://www.youtube.com/embed/rFE21sDpHjs
0 칼리스타 17 회복 점멸 BRO Hena 3 1 10 9735 87 ... 칼날비 피의 맛 좀비 와드 굶주린 사냥꾼 비스킷 배달 우주적 통찰력 5005 5008 5002 https://www.youtube.com/embed/rFE21sDpHjs
0 그라가스 14 점화 점멸 BRO Delight 1 2 11 5349 111 ... 여진 생명의 샘 재생의 바람 불굴의 의지 비스킷 배달 우주적 통찰력 5007 5008 5002 https://www.youtube.com/embed/rFE21sDpHjs
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
0 제이스 18 점멸 순간이동 DK Khan 5 3 4 24782 56 ... 난입 마나순환 팔찌 절대 집중 폭풍의 결집 마법의 신발 비스킷 배달 5008 5008 5003 https://www.youtube.com/embed/m5IY7xwPJW8
0 트런들 14 강타 점멸 DK Canyon 0 2 6 4040 50 ... 집중 공격 승전보 전설: 강인함 최후의 일격 마법의 신발 우주적 통찰력 5005 5008 5002 https://www.youtube.com/embed/m5IY7xwPJW8
0 르블랑 17 순간이동 점멸 DK ShowMaker 7 1 6 26242 95 ... 감전 피의 맛 좀비 와드 굶주린 사냥꾼 마나순환 팔찌 깨달음 5005 5008 5003 https://www.youtube.com/embed/m5IY7xwPJW8
0 애쉬 16 정화 점멸 DK Ghost 2 0 9 16316 82 ... 칼날비 피의 맛 좀비 와드 굶주린 사냥꾼 비스킷 배달 쾌속 접근 5005 5008 5002 https://www.youtube.com/embed/m5IY7xwPJW8
0 세라핀 12 탈진 점멸 DK BeryL 0 3 4 7069 103 ... 수호자 보호막 강타 뼈 방패 소생 비스킷 배달 우주적 통찰력 5008 5008 5002 https://www.youtube.com/embed/m5IY7xwPJW8

2440 rows × 43 columns

Leave a comment