본인이 가끔씩 즐겨하는 스팀 게임 'Geometry Dash' 라는 점프 게임이 있다.
그리고 이 게임의 포럼 사이트 pointercrate.com 에서는 관리자들이 선정한 가장 어려운맵 top 150을 확인할 수 있다.
이 150개의 맵 정보들을 위 사이트에서 크롤링하여 여러 재밌는 통계를 내보며 python의 matplotlib으로 그래프까지 그려보는 시간을 가질 것이다.
0. 크롤링 계획 세우기
가장 어려운 150개의 레벨마다 맵 이름, 플레이 타임(맵 길이), 클리어 인원, 클리어 시 얻는 점수 데이터들을 추출한다.
1. 크롤링할 페이지 접속
/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. 맵 난이도 순위와 클리어 시 얻는 유저 점수와의 관계를 그래프로 시각화
깔끔한 곡선이 나왔다.
난이도가 높은 맵을 클리어하는 유저가 점수를 얻기 훨씬 유리하다는 사실을 알 수 있다.
이렇게 웹 데이터에서 정보를 추출하여, 추출한 정보를 기반으로 통계를 내고 그래프로 보기 좋게 시각화까지 하는 시간을 가져 보았다. 빅데이터들이 많아지고 복잡해질수록 이러한 시각화는 더욱 필수 사항이다.
Matplotlib에 대한 wikidocs 문서이다.
레이블, 색상, 영역, 눈금, 그래프 종류 등등 여러 상황에 맞는 그래프를 그리고 싶을 때, 위 문서를 참조하면 매우 도움이 될 것이다.
'Crawling' 카테고리의 다른 글
[크롤링, 보충] 웹 요소 선택 정리 (0) | 2020.11.03 |
---|---|
[크롤링, 예제 4] Youtube - 필터를 적용하여 영상 목록 보기 (0) | 2020.11.02 |
[크롤링, 예제 2] 레벨 분포 구하기 (0) | 2020.10.26 |
[크롤링, 예제 1-3] Selenium - 새 창 안띄우고 크롤링 하기 (0) | 2020.10.23 |
[크롤링, 예제 1-2] 예제 1 - 셀레니움(Selenium) 이용 (0) | 2020.10.23 |
댓글