本文爬取了豆瓣遊戲網站上所有可見的遊戲評分數據進行分析,全文包括以下幾個部分:
數據獲取
數據總覽
遊戲類型分析
遊戲平臺分析
遊戲名稱分析
高分遊戲匯總
代碼匯總
對代碼不感興趣的可以直接跳過第一部分。此外,鑑於經常有讀者問到代碼,所以這一次除了後臺回復「遊戲」可以獲取代碼和數據文件外,也把全部用到的代碼匯總到文章末尾,供參考。
全文數據獲取及分析均基於python3.6完成。
數據來自豆瓣遊戲,網址:
https://www.douban.com/game/explore
頁面內一條遊戲數據展示如下,顯示出來的一條評論是遊戲的點讚數最多的評論,我們分析需要的數據包括遊戲名稱、遊戲類型、遊戲平臺、遊戲評分、遊戲評價人數及最熱評價。
瀏覽器中按F12打開開發者工具,選擇NetWork-XHR,頁面拉倒底部點顯示更多,可以看到獲取到的數據文件。
右鍵打開後看到是遊戲的信息,通過改變網址中more後面的數字,可以獲取更多數據。但嘗試之後發現,每次可以獲取20條數據,more後面的數字最大可以設置為500,超過500後獲取不到數據,也就是說最多能獲取10000條數據,但底部total欄位顯示總的遊戲數據有52049條。
所以為了獲取更多數據,我們分類型爬取數據,每次選中一個類型,重複上述過程,可以得到數據,觀察後發現每個類型下的遊戲數據都不超過10000條,這樣每個類型的數據都能全部獲取,最後把所有數據拼到一起即可。
以動作類遊戲為例,動作類遊戲第二頁數據對應的網址如下
https://www.douban.com/j/ilmen/game/search?genres=1&platforms=&q=&sort=rating&more=2
多嘗試幾次之後能看出規律:genres後面是遊戲類型,動作類型對應的genres = 1,platforms後面是平臺類型,q後面的是遊戲名稱關鍵字,sort後面是排序方式,默認是按評分排序,more後面是頁碼。
所以我們需要知道每個遊戲類型對應的數字,可以在開發者工具中選Element,用小箭頭進行定位,快速獲取所有遊戲類型對應的數字:
定位後發現,每種類型包含在一個class內,動作類型對應的數字在values裡。
依次點開每個class,獲取每個類型對應的數字,整理如下:
{1:"動作",5:"角色扮演",41:"橫版過關",4:"冒險",48:"射擊",32:"第一人稱射擊",2:"策略",18:"益智",7:"模擬",3:"體育",6:"競速",9:"格鬥",37:"亂鬥/清版",12:"即時戰略",19:"音樂/旋律"}
之後就可以用python中的request+json包循環獲取數據了,代碼附在最後。爬到的一條遊戲數據樣式如下:
需要的數據包括:
n_ratings:評分人數
platforms:平臺
rating:評分
star:星級
title:遊戲名稱
content:最熱評論
遊戲類型因為已經我們已經分類型爬取,所以每次爬完之後用代碼加上對應的類型即可,但能看到一個遊戲可能對應多種類型,或者在多個平臺上同時發布,所以在後面的分析中需要處理,其他欄位分析中用不到。
最終爬下來數據有31574條,還是沒拿到所有5萬條,這已經是最大可見數據條數了,數據樣式如下:
genres是網站上給的類型,type是爬取過程中加上去的類型,後文的分析都用type。
簡單統計後發現,31574條數據中有17751條數據都是沒有評分的,此外,由於一種遊戲可能屬於多個類別,所以一部分遊戲是重複出現的,只有type不同。
首先對數據整體做一個統計描述,對於沒有評分的數據採用兩種方法處理,視為評分為0和刪除數據。
視為評分為0時
刪除沒有評分的數據
兩幅圖的5個變量(從左往右、從上至下)均為:星級、評分、評分人數、遊戲名稱長度、遊戲發行平臺數,加入遊戲名稱長度和發行平臺數是想探究遊戲名稱的長度以及發行的平臺數是否和遊戲評分有一定的關係。
從這兩幅分布圖能得到一些結論:
星級(star)和評分(rating)是嚴格單調關係的,星級是分級靠檔後的評分,所以之後的分析中只考慮星級不考慮評分。
兩種評分處理方法下,各變量的分布基本不變,大部分遊戲評分集中在7.5左右,評分跟發行平臺數、遊戲名稱長度關係不是非常明顯。
各類型遊戲數
豆瓣給出的遊戲共有15類,各類型遊戲數統計如下:
動作、冒險、角色扮演、益智、策略類的遊戲數量排到前5,第一人稱射擊、即時戰略、音樂、亂鬥/清版、射擊類排到後5。
豆瓣把第一人稱射擊、射擊分成兩類,亂鬥跟冒險、動作的區別似乎不是很大?這裡不是很理解。不過射擊類的遊戲模式太過單一,數量確實比較少,高質量的射擊遊戲不外乎這兩年火起來的吃雞遊戲,還有很早之前的CS系列。
而動作、冒險、角色扮演類的遊戲從世界觀設定、劇情設計上都可以有很多新意,時不時會有一些讓人眼前一亮的新作品,也很容易做成一個系列。
各類型遊戲平均評分人數
評分人數最多的是第一人稱射擊類遊戲,冒險、即時戰略類也較多。
各類型遊戲均分
各類型遊戲的均分如下,無評分視為0時,由於各種遊戲數量的差別,導致遊戲數較少的類型平均分更高,但刪掉無評分數據後,各種類型的評分基本是持平的,在7.5分上下波動。
總體來看,各種類型遊戲的平均質量基本是一樣的,只是各種類型的用戶基數差別大一些。
遊戲類型關聯分析
之前提到,一個遊戲可能屬於多個類別,比如仙劍同時屬於角色扮演和冒險。對所有的遊戲類別進行交叉分析,統計同時屬於兩個類別的遊戲個數,結果如下:
可以看到,動作+冒險,角色扮演+動作/冒險、橫版過關+動作/冒險、益智+冒險 組合的遊戲都是非常多的,動作、冒險大概是萬能類別了。
音樂旋律、競速類的遊戲跟其他類別交叉幾乎沒有,這兩種類型遊戲形式比較單一,大部分都是拼手速操作,各種遊戲同質性比較高。俠盜飛車是其中少有的融合了動作冒險的經典競速類遊戲,但當年玩罪惡都市的快感似乎跟競速沒啥關係。
遊戲平臺類型非常多,整體分為手機、電腦、遊戲機三類,遊戲機大部分是任天堂(Wii,GB)、索尼(PS)、微軟(Xbox)的產品。
之前提到,一款遊戲可能同時在多個平臺上發布,這給分析過程帶來了一定難度,觀察後發現,豆瓣的平臺分布是越靠前的平臺越大眾化,所以對於有多個平臺的遊戲,取第一個平臺,視為他的主要發布平臺進行分析。
各平臺遊戲數
鑑於平臺數太多,我們把所有遊戲數目小於100的平臺匯總,記為「其他」,各個平臺遊戲數分布如下:
PC遊戲數超過總數的50%,除此外,大部分遊戲在iphone,PS2,PS3上,沒有Android的原因在於豆瓣上對於遊戲平臺把iphone放在Android前面,大部分手遊是在這兩個作業系統上同時發布的,之前的處理方法導致Android數目非常少歸到了「其他」中去。
各平臺均分
刪除無評分數據遊戲後,各平臺均分基本一致。其中均分最高的GB是任天堂1989年推出的Game Boy 遊戲機,GBA是任天堂2001年推出的Game Boy Advanced遊戲機。你可能沒有用過這兩款設備,但當中的經典遊戲你一定玩過。
各平臺平均評分人數
各平臺評分總人數來說,PC佔據絕對優勢,但平均人均數來看,人數最多的是PS4和Nintendo Switch。
一個有意思的問題是,遊戲都是怎樣命名的呢?有沒有什麼規律?
爬取下來的遊戲名稱中大部分同時包含中文、英文,這裡我們只分析中文,將所有遊戲名稱拼到一起用正則提取其中的中文,去掉長度為1的詞,和詞頻小於10的詞,對剩下的高頻詞按詞頻做詞雲如下:
詞語能反映出遊戲的世界觀,大部分的遊戲會用到戰爭、戰士、傳奇、聯盟、幻想這樣一些虛構的有奇幻色彩的詞語,同時也不乏三國、火影等等一些源於歷史、動漫、小說、電影作品的詞。還有一些開門見山直接說明遊戲形式的詞語,比如迷宮、格鬥、大戰、足球等等。
對遊戲的整體分析只是統計分析的需要,但對一個遊戲迷來說,只需要告訴他哪些遊戲好就ok了,不好的遊戲並不關注,我們提取所有遊戲中評分超過9.5的部分,遊戲類型分布如下:
考慮到評分人數太少時,評分結果不一定具有代表性,所以我們只選擇其中評分人數超過100的部分,共84款遊戲匯總如下,看看有沒有你玩過or你想玩的呢?
爬蟲代碼
# -*- coding: utf-8 -*-
import urllib
import requests
from fake_useragent import UserAgent
import json
import pandas as pd
import time
import datetime
import os
# 發送get請求
""
genres : 遊戲類別
n_ratings: 評分人數
platforms: 平臺
rating : 評分
content : 最熱評論
star : 星數
title : 遊戲名稱
""
def getDoubanGame(genres):
id_all1 = {1:"動作",5:"角色扮演",41:"橫版過關",4:"冒險",48:"射擊",32:"第一人稱射擊",
2:"策略",18:"益智",7:"模擬",3:"體育",6:"競速",9:"格鬥",37:"亂鬥/清版",12:"即時戰略",
19:"音樂/旋律"}
comment_api = 'https://www.douban.com/j/ilmen/game/search?genres={}&platforms=&q=&sort=rating&more={}'
headers = { "User-Agent": UserAgent(verify_ssl=False).random}
response_comment = requests.get(comment_api.format(genres,1),headers = headers)
json_comment = response_comment.text
json_comment = json.loads(json_comment)
col = ['name','star','rating','platforms','n_ratings','genres','content']
dataall = pd.DataFrame()
num = json_comment['total']
print('{}類別共{}個遊戲,開始爬取!'.format(id_all1[genres],num))
i = 0
while i < num:
if i == 0:
s = 1
else:
s = json_comment['more']
response_comment = requests.get(comment_api.format(genres,s),headers = headers)
json_comment = response_comment.text
json_comment = json.loads(json_comment)
n = len(json_comment['games'])
datas = pd.DataFrame(index = range(n),columns = col)
for j in range(n):
datas.loc[j,'name'] = json_comment['games'][j]['title']
datas.loc[j,'star'] = json_comment['games'][j]['star']
datas.loc[j,'rating'] = json_comment['games'][j]['rating']
datas.loc[j,'platforms'] = json_comment['games'][j]['platforms']
datas.loc[j,'n_ratings'] = json_comment['games'][j]['n_ratings']
datas.loc[j,'genres'] = json_comment['games'][j]['genres']
datas.loc[j,'content'] = json_comment['games'][j]['review']['content']
i += 1
dataall = pd.concat([dataall,datas],axis = 0)
print('已完成 {}% !'.format(round(i/num*100,2)))
time.sleep(0.5)
dataall = dataall.reset_index(drop = True)
dataall['type'] = id_all1[genres]
return dataall
id_all = {"動作":1,"角色扮演" :5,"橫版過關" :41,"冒險" :4,"射擊": 48,"第一人稱射擊":32,
"策略":2,"益智":18,"模擬":7,"體育":3,"競速":6,"格鬥":9,"亂鬥/清版":37,"即時戰略":12,"音樂/旋律":19}
id_all1 = {1:"動作",5:"角色扮演",41:"橫版過關",4:"冒險",48:"射擊",32:"第一人稱射擊",
2:"策略",18:"益智",7:"模擬",3:"體育",6:"競速",9:"格鬥",37:"亂鬥/清版",12:"即時戰略",
19:"音樂/旋律"}
for i in list(id_all.values()):
dataall = getDoubanGame(i)
filename = '遊戲類別_' + id_all1[i] +'.xlsx'
filename = filename.replace('/','')
dataall.to_excel(filename)
數據總覽
dataall['n_platforms'] = dataall.platforms.apply(lambda x:len(str(x).split('/')))
dataall['platform'] = dataall.platforms.apply(lambda x:str(x).split('/')[0].strip())
sns.pairplot(dataall,diag_kind = 'kde')
plt.show()
data2 = dataall.dropna()
sns.pairplot(data2,diag_kind = 'kde')
plt.show()
遊戲類型分析
""
各類型遊戲分析
""
# 各類型遊戲數
num = len(dataall.name.unique())
result = dataall.groupby('type').count()['name'].reset_index().sort_values('name',ascending = False)
attr = list(result.type)
v = list(np.round(result.name/num,3))
pie = Pie()
style = Style()
pie_style = style.add(
label_pos="center",
is_label_show=True,
label_text_color=None,
is_legend_show = False
)
pie.add("",[attr[0],"其他"],[v[0],1-v[0]],radius=[18, 24],center = [10,20],**pie_style)
pie.add("",[attr[1],"其他"],[v[1],1-v[1]],radius=[18, 24],center = [30,20],**pie_style)
pie.add("",[attr[2],"其他"],[v[2],1-v[2]],radius=[18, 24],center = [50,20],**pie_style)
pie.add("",[attr[3],"其他"],[v[3],1-v[3]],radius=[18, 24],center = [70,20],**pie_style)
pie.add("",[attr[4],"其他"],[v[4],1-v[4]],radius=[18, 24],center = [90,20],**pie_style)
pie.add("",[attr[5],"其他"],[v[5],1-v[5]],radius=[18, 24],center = [10,50],**pie_style)
pie.add("",[attr[6],"其他"],[v[6],1-v[6]],radius=[18, 24],center = [30,50],**pie_style)
pie.add("",[attr[7],"其他"],[v[7],1-v[7]],radius=[18, 24],center = [50,50],**pie_style)
pie.add("",[attr[8],"其他"],[v[8],1-v[8]],radius=[18, 24],center = [70,50],**pie_style)
pie.add("",[attr[9],"其他"],[v[9],1-v[9]],radius=[18, 24],center = [90,50],**pie_style)
pie.add("",[attr[10],"其他"],[v[10],1-v[10]],radius=[18, 24],center = [10,80],**pie_style)
pie.add("",[attr[11],"其他"],[v[11],1-v[11]],radius=[18, 24],center = [30,80],**pie_style)
pie.add("",[attr[12],"其他"],[v[12],1-v[12]],radius=[18, 24],center = [50,80],**pie_style)
pie.add("",[attr[13],"其他"],[v[13],1-v[13]],radius=[18, 24],center = [70,80],**pie_style)
pie.add("",[attr[14],"其他"],[v[14],1-v[4]],radius=[18, 24],center = [90,80],**pie_style)
pie.render('各類型遊戲數.html')
# 各類型遊戲均分
c_schema= [( "亂鬥/清版",10),
( "體育",10),
( "冒險",10),
( "動作",10),
( "即時戰略",10),
( "射擊",10),
( "格鬥",10),
( "模擬",10),
( "橫版過關",10),
( "益智",10),
( "競速",10),
( "第一人稱射擊",10),
( "策略",10),
( "角色扮演",10),
( "音樂/旋律",10)]
result1 = dataall.rating.fillna(0).groupby(dataall.type).mean().reset_index()
result2 = dataall.rating.dropna().groupby(dataall.dropna().type).mean().reset_index()
v1 = [result1.rating.apply(lambda x:round(x,1)).tolist()]
v2 = [result2.rating.apply(lambda x:round(x,1)).tolist()]
radar = Radar()
radar.config(c_schema)
radar.add("遊戲均分(無評分視為0)", v1, is_splitline=True, is_axisline_show=True,is_label_show = True)
radar.add("遊戲均分(刪除無評分)", v2, label_color=["#4e79a7"],item_color="#f9713c",is_label_show = True)
radar.render('各類型遊戲評分.html')
# 各類型遊戲評分人數
result1 = dataall.n_ratings.fillna(0).groupby(dataall.type).mean().reset_index()
result2 = dataall.n_ratings.dropna().groupby(dataall.dropna().type).mean().reset_index()
attr = result1.type.tolist()
v1 = np.round(result1.n_ratings.tolist(),1)
v2 = np.round(result2.n_ratings.tolist(),1)
line = Line()
#line.add(x_axis = attr,y_axis = xaxis_type = 'category')
line.add("包含無評分", attr, v1, mark_point=["max"],is_label_show = True)
line.add("不包含無評分", attr, v2, is_smooth=True, mark_point=["max"],is_label_show = True,xaxis_rotate = 30)
line.render('各類型遊戲-評分人數.html')
遊戲平臺分析
""
遊戲平臺分析
""
# 各平臺遊戲數
num = len(dataall.name.unique())
result = dataall.groupby('platform').count()['name'].reset_index()
name = result.platform.tolist()
value = result.name.tolist()
wordcloud = WordCloud(width=1300, height=620)
wordcloud.add("", name, value, word_size_range=[20, 120])
wordcloud.render('遊戲平臺.html')
platforms = result.loc[result.name >100,'platform'].tolist()
dataall.platform = dataall.platform.apply(lambda x:x if x in platforms else '其他')
result = dataall.groupby('platform').count()['name'].reset_index().sort_values('name',ascending = False).reset_index(drop = True)
attr = result.platform
v1 = result.name
pie = Pie('各平臺遊戲數',title_pos = 'center',title_text_size = 20)
pie.add(
"",
attr,
v1,
radius=[40, 75],center = [50,60],
label_text_color=None,is_legend_show = False,
is_label_show=True
)
pie.render('各平臺遊戲數.html')
# 各平臺遊戲評分人數
result1 = dataall.n_ratings.fillna(0).groupby(dataall.platform).mean().reset_index()
result2 = dataall.n_ratings.dropna().groupby(dataall.dropna().platform).mean().reset_index()
attr = result1.platform.tolist()
v1 = np.round(result1.n_ratings.tolist(),1)
v2 = np.round(result2.n_ratings.tolist(),1)
line = Line()
#line.add(x_axis = attr,y_axis = xaxis_type = 'category')
line.add("包含無評分", attr, v1, mark_point=["max"],is_label_show = True)
line.add("不包含無評分", attr, v2, is_smooth=True, mark_point=["max"],is_label_show = True,xaxis_rotate = 70)
line.render('各平臺遊戲-評分人數.html')
# 各平臺遊戲均分
result1 = dataall.rating.fillna(0).groupby(dataall.platform).mean().reset_index()
result2 = dataall.rating.dropna().groupby(dataall.dropna().platform).mean().reset_index()
attr = result1.platform.tolist()
v1 = np.round(result1.rating.tolist(),1)
v2 = np.round(result2.rating.tolist(),1)
line = Line()
#line.add(x_axis = attr,y_axis = xaxis_type = 'category')
line.add("包含無評分", attr, v1, mark_point=["max"],is_label_show = True)
line.add("不包含無評分", attr, v2, is_smooth=True, mark_point=["max"],is_label_show = True,xaxis_rotate = 70)
line.render('各平臺遊戲-均分.html')
遊戲名稱分析
"""
標題分析
"""
import re
stopwords = open('中文停用詞表(比較全面,有1208個停用詞).txt','r').read()
stopwords = stopwords.split('\n')
texts = ''.join(dataall.name.tolist())
texts =''.join(re.findall(r'[\u4e00-\u9fa5]',texts))
result = jieba.cut(texts,cut_all=False)
allwords = [word for word in result if len(word)>1 and word not in stopwords]
result = pd.DataFrame(allwords)
result.columns =['word']
res = result.word.groupby(result.word).count()
res.index.name = 'text'
res = res.reset_index()
res = res.loc[res.word >= 10].reset_index(drop = True)
name = res.text.tolist()
value = res.word.tolist()
wordcloud = WordCloud(width=1300, height=620)
wordcloud.add("", name, value, word_size_range=[10, 80])
wordcloud.render('遊戲名稱高頻詞.html')
高分遊戲匯總
"""
高rating遊戲分析
"""
data1 = dataall.loc[dataall.rating>=9.5]
result = data1.groupby('type').count()['name'].reset_index().sort_values('name',ascending = False).reset_index(drop = True)
attr = result.type
v1 = result.name
pie = Pie('9.5分以上遊戲',title_pos = 'center',title_text_size = 20)
pie.add(
"",
attr,
v1,
radius=[40, 75],center = [50,60],
label_text_color=None,is_legend_show = False,
is_label_show=True
)
pie.render('高評分遊戲-分類型.html')
data1['title'] = data1.name.apply(lambda x:str(x).split(':')[0].split(' ')[0])
result = data1.loc[data1.n_ratings >= 100,['name','genres','content','platforms','rating','n_ratings']].drop_duplicates().reset_index(drop = True)
result = result.sort_values(by = ['n_ratings','genres'],ascending = False).reset_index(drop = True)
result.to_excel('評分9.5以上遊戲.xlsx')