본문 바로가기
  • 실행력이 모든걸 결정한다
Crawling

[크롤링, 예제 3] 게임 포럼 사이트에서 정보 긁어오기 (+그래프)

by 김코더 김주역 2020. 10. 31.
반응형

본인이 가끔씩 즐겨하는 스팀 게임 'Geometry Dash' 라는 점프 게임이 있다.

그리고 이 게임의 포럼 사이트 pointercrate.com 에서는 관리자들이 선정한 가장 어려운맵 top 150을 확인할 수 있다.

이 150개의 맵 정보들을 위 사이트에서 크롤링하여 여러 재밌는 통계를 내보며 python의 matplotlib으로 그래프까지 그려보는 시간을 가질 것이다.

 

 

 

0. 크롤링 계획 세우기

가장 어려운 150개의 레벨마다 맵 이름, 플레이 타임(맵 길이), 클리어 인원, 클리어 시 얻는 점수 데이터들을 추출한다.

 

1. 크롤링할 페이지 접속

/demonlist로 접속했다. (pointercrate.com/demonlist)

 

 

 

 

pointercrate.com/demonlist/

 

 

2. 크롤링 가능 여부 확인(필수)

 

Disallow에 /demonlist가 없으므로 불법x

 

 

 

3. 크롤링할 대상 결정

 

본인이 표시해놓은 빨간 상자 영역을 위 사이트내에서 클릭해주면 상세 정보를 확인할 수 있다.

 

 

 

 

대표로 가장 어려운 맵 Top1인 'Tartarus' 로 들어가보자

 

맵 이름, 플레이 타임과 클리어(100% 달성) 시 얻을 수 있는 유저 점수를 볼 수 있다.

 

 

 

 

그리고 아래로 좀더 내려보면 세계에서 몇 명이 이 맵을 클리어 했는지 알 수 있다.

3번째 줄의 'out of which 6 are 100%' 에서 '6' 을 추출하는 방법도 있고 테이블에서 Progress가 100%인 행의 수를 계산하는 방법도 있을 것인데, 본인은 후자의 방법으로 했다.

 

 

 

 

4. 크롤링할 데이터가 있는 태그 파악

<맵 링크 확인>

demonlist/ 의 뒤에 맵의 순위가 붙는다.

그래서 위의 'Tartarus' 를 클릭하면 demonlist/1 로 이동한 것이다.

 

<원하는 데이터가 있는 태그 파악>

맵 이름, 플레이 타임, 클리어 시 얻는 점수

 

 

 

클리어 여부

 

 

 

 

5. 태그 규칙 파악

 

맵 이름 : class가 'underlined' 인 <div> 태그 안에 있는 <h1> 태그에 text 형식으로 존재

플레이 타임(맵 길이), 클리어 시 얻는 점수 : id가 'level-info'인 <span> 태그 안에 있는 <span> 태그에 존재

-> 가끔가다 몇몇 정보가 누락 되어있는 맵도 있기 때문에, 원하는 데이터가 n번째 <span> 태그에 있다는 규칙을 세워서는 안된다. 

 

 

 

 

대신, 각각 'Level length', '100%' 라는 문자열이 포함된 텍스트를 찾았을 때, 원하는 데이터를 추출할 수 있게 하였다.

각 텍스트마다 ':' 를 기준으로 split한 배열을 만들어서, 원하는 기준으로 원하는 데이터를 쉽게 추출할 수 있었다.

 

 

 

 

클리어 인원 수 : <tbody> 태그 안의 <tr> 태그 안의 2번째 <td> 태그 안에 유저의 진행도가 % 형식으로 존재 하는데, 진행도가 100% (클리어) 인 인원을 세면 된다.

 

6. 코딩

 

81번째 줄부터 있는 plt은 그래프를 그리기 위한 matplotlib.pyplot 모듈을 plt라는 이름으로 사용하겠다는 의미이며, 맵 길이 정보는 맵마다 누락된 경우가 있어서 눈금표시를 하지 않았다.

클리어 인원 수와 점수 정보는 확실히 150개 모두 나와 있는데 맵 길이 정보는 그렇지 않기 때문에 보기 불편할 수 있음을 고려한 것이다.

 

import sys
import urllib.request
import urllib.parse
import matplotlib.pyplot as plt
import numpy as np
from bs4 import BeautifulSoup
seconds=0 #모든 맵의 플레이 타임의 합
seconds_length=0
score=0 #모든 맵의 점수의 합
score_length=0
top=150
victors_sum=0 #모든 맵의 클리어 인원 수의 합
victors_list=[]
playtime_list=[]
score_list=[]

#[mapname, value, rank]
most_victors=['null',0,-1]
least_victors=['null',sys.maxsize,-1]
longest=['null',0,-1]
shortest=['null',sys.maxsize,-1]

for rank in range(1,top+1):
    print("Analyzing Rank "+str(rank)+'...') # 크롤링 진행 과정을 콘솔창에서 확인하기 위한 용도
    with urllib.request.urlopen('https://pointercrate.com/demonlist/'+str(rank)+'/') as response:
        html = response.read()
        soup = BeautifulSoup(html,'html.parser')

        #해당 맵의 이름 데이터 추출
        mapname = soup.select_one('div.underlined h1')

        #해당 맵의 유저 기록 데이터 추출
        records = soup.select('tbody tr td:nth-child(2)')
        victors=0
        for record in records:
            if record.text=='100%': # 100% = 클리어
                victors+=1
        victors_sum+=victors
        victors_list.append(victors)
        if(victors>most_victors[1]): #최댓값 갱신
            most_victors[0]=mapname.text
            most_victors[1]=victors
            most_victors[2]=rank
        if(victors<least_victors[1]): #최솟값 갱신
            least_victors[0]=mapname.text
            least_victors[1]=victors
            least_victors[2]=rank

        #해당 맵의 플레이 타임,클리어 점수 정보 추출
        infotexts = soup.select('#level-info span')
        for infotext in infotexts:
            splited_elements = str(infotext.text).split(':')
            if splited_elements[0].find('Level length')!=-1: #플레이 타임 정보 추출
                sec=int(splited_elements[1][1:-1])*60+int(splited_elements[2][:-1])
                seconds+=sec
                playtime_list.append(sec)
                if(sec>longest[1]): #최댓값 갱신
                    longest[0]=mapname.text
                    longest[1]=sec
                    longest[2]=rank
                if(sec<shortest[1]): #최솟값 갱신
                    shortest[0]=mapname.text
                    shortest[1]=sec
                    shortest[2]=rank
                seconds_length+=1
            elif splited_elements[0].find('100%')!=-1: #클리어 점수 정보 추출
                score+=float(splited_elements[1])
                score_list.append(float(splited_elements[1]))
                score_length+=1

#크롤링 결과 출력
print('\n<Top '+str(top)+' Maps Analyzer>')
print('\n[Longest Map] #'+ str(longest[2]) + ' '+ longest[0]+' - '+str(int(longest[1]/60))+'m '+str(longest[1]%60)+'s')
print('[Shortest Map] #'+str(shortest[2]) + ' '+ shortest[0]+' - '+str(int(shortest[1]/60))+'m '+str(shortest[1]%60)+'s')
print('[Average Playtime] '+str(int(int(seconds/seconds_length)/60))+'m '+str(int(seconds/seconds_length)%60)+'s')
print('[Most Victors] #'+ str(most_victors[2]) + ' '+ most_victors[0]+' - '+str(most_victors[1]) +' Victors')
print('[Least Victors] #'+ str(least_victors[2]) + ' '+ least_victors[0]+ ' - '+str(least_victors[1]) +' Victors')
print('[Average Victors] '+str(round(victors_sum/top,1))+' Victors')
print('[Total Score] '+str(round(score,2))+'\n')

#맵 난이도 순위와 클리어 인원 수의 관계를 그래프로 시각화
plt.figure(1)
plt.plot(np.arange(1,top+1),victors_list)
plt.xlabel('Rank')
plt.ylabel('Victors')

#맵 난이도 순위와 플레이 타임의 관계를 그래프로 시각화
plt.figure(2)
plt.plot(np.arange(1,len(playtime_list)+1), playtime_list,'r')
plt.xticks([],[]) # 눈금 표시 안하기
plt.xlabel('Rank')
plt.ylabel('Playtime')

#맵 난이도 순위와 점수의 관계를 그래프로 시각화
plt.figure(3)
plt.plot(np.arange(1,top+1),score_list,'g')
plt.xlabel('Rank')
plt.ylabel('Score')

plt.show()

 

7. 실행

cmd 대신 pycharm 에서 실행하였다. pycharm이 모듈 설치가 잘 되었다.

 

 

 

가장 긴 맵 : 51위 - Freedom08 - 4분 20초

가장 짧은 맵 : 17위 - Kowareta - 48초

평균 맵 길이 : 1분 51초

(맵 길이 정보가 누락된 맵도 있기 때문에 위 3개는 정확하지 않을 수 있음)

 

클리어 인원 수가 가장 많은 맵 : 138위 - Bloodbath - 1039명

클리어 인원 수가 가장 적은 맵 : 37위 - Omicrom - 1명

평균 클리어 인원 수 - 49.9명

150개의 맵을 모두 클리어 했을 시 얻는 점수 : 6429.94점

 

 

1. 맵 난이도 순위와 클리어 인원 수의 관계를 그래프로 시각화

맵의 난이도가 낮을 수록 클리어 인원 수가 많은 경향이 조금은 보인다.

 

 

 

2. 맵 난이도 순위와 플레이 타임의 관계를 그래프로 시각화

크게 관계는 없는 듯 하다.

그래도 플레이 타임이 높은 표본이 왼쪽에 조금 더 뭉쳐있는 것을 보아, 순위가 높은(어려운) 맵일 수록 플레이 타임이 긴 경향이 살짝은 있는 것 같다.

 

 

 

3. 맵 난이도 순위와 클리어 시 얻는 유저 점수와의 관계를 그래프로 시각화

깔끔한 곡선이 나왔다.

난이도가 높은 맵을 클리어하는 유저가 점수를 얻기 훨씬 유리하다는 사실을 알 수 있다.

 

 

 

이렇게 웹 데이터에서 정보를 추출하여, 추출한 정보를 기반으로 통계를 내고 그래프로 보기 좋게 시각화까지 하는 시간을 가져 보았다. 빅데이터들이 많아지고 복잡해질수록 이러한 시각화는 더욱 필수 사항이다.

 

wikidocs.net/92071

 

위키독스

온라인 책을 제작 공유하는 플랫폼 서비스

wikidocs.net

Matplotlib에 대한 wikidocs 문서이다.

레이블, 색상, 영역, 눈금, 그래프 종류 등등 여러 상황에 맞는 그래프를 그리고 싶을 때, 위 문서를 참조하면 매우 도움이 될 것이다.

반응형

댓글