1. 程式人生 > 實用技巧 >steam夏日促銷悄然開始,用Python爬取排行榜上的遊戲打折資訊

steam夏日促銷悄然開始,用Python爬取排行榜上的遊戲打折資訊

前言

本文的文字及圖片來源於網路,僅供學習、交流使用,不具有任何商業用途,版權歸原作者所有,如有問題請及時聯絡我們以作處理。

不知不覺,一年一度如火如荼的steam夏日促銷悄然開始了。每年通過大大小小的促銷,我的遊戲庫裡已經堆積滿還未下載過的遊戲。但所謂“買到就是賺到,G胖一定大虧”的想法日漸流行,指不定以後就靠它們發達了呢。

有時候滾動steam的排行榜看自己喜歡的遊戲的時候,未免會被右邊的價格影響到。久而久之我發現我所不想買的遊戲並不是因為它不好玩,而是它還沒打折。又或者有些心水未被別人挖掘,在排行榜隱祕的角落裡自怨自艾,等待“把玩”它的人出現~

於是我簡單的用python爬取了steam排行榜前10000個遊戲的資訊,其中有遊戲名,評價,價格,出版日期等,在更加簡潔的列表介面選取自己感興趣的遊戲之時,也可以進行進一步的資料分析。

廢話不多說,趕緊開始,不然被我拖更到促銷結束了就蹭不到熱度了。(本來也沒有熱度)

開始爬取

先說說這次爬蟲選用資料的優缺點:

第一,我發現了steam在顯示排行榜列表的時候後臺會進行一個查詢的申請,點開一看是一串json程式碼,而且在python進行request的時候不需要模擬瀏覽器進行填“headers”表的操作。通過訪問而得到的json程式碼大大簡化了迴圈複雜度,一次迴圈可以得到100個遊戲資訊。

第二,因為只需要遍歷所有json程式碼,時間上可以比進入每一個遊戲連結更加短。

第三,但就因為沒有進入每個遊戲的連結,所以像評論,簡介,開發商等資訊就沒有爬取。但爬取遊戲連結的爬蟲攻略網上也有很多,這裡就不弄斧了。

首先,進入官網的排行榜頁面,為了避免遊戲DLC、bundle等影響後期操作的型別出現,記得在右邊的過濾器裡只勾選遊戲類目。

通過後臺的XHR發現,頁面每次重新整理都只顯示前50個遊戲,當我們滾動頁面往下看時,網站會傳送一個神祕程式碼:

經過觀察,我發現程式碼會自動申請返回從start引數的數字開始,一共count引數的數字的遊戲資訊。比如,下面的圖顯示它申請了從第51個到100個總共50個遊戲的資訊。

雙擊上上圖的紅框連結,返回的頁面長這樣:

所謂json格式,其實就是在字典裡夾字典或者列表,目前許多大資料都是這樣儲存滴。所以在查詢的時候其實很方便,但是我在抽取資訊的時候還是會用到正則表示式,因為會方便很多。

知道這些之後,剩下的就可以用python一個個有用資訊抽取出來,組成一個新的Dataframe列表,以便之後儲存為csv格式。

# 匯入需要用到的庫
import requests
from bs4 import BeautifulSoup
import re
import json
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

我們嘗試用requests開啟上面json頁面的連結,並用json load解析。

這裡我更改了start和count 的引數,比較方便對照原網頁來看資訊是否一致。

url = 'https://store.steampowered.com/search/results/?query&start=0&count=100&dynamic_data=&sort_by=_ASC&category1=998&snr=1_7_7_globaltopsellers_7&filter=globaltopsellers&infinite=1'
content = requests.get(url).content
jsontext = json.loads(content)
soup = BeautifulSoup(jsontext['results_html'],'html.parser')

可以看看soup返回的結果,它顯示了json裡邊'results_html'返回的東西,因為前邊的內容我們已經不需要了,所有遊戲資訊都在這個鍵裡邊。

接著我們回到那個json頁面看看我們想要的東西都藏在哪:

遊戲名字藏在span的title class裡:

name = soup.find_all('span',class_ ='title')

出版日期藏在div的另一個class裡:

listdate = soup.find_all('div', class_ ='col search_released responsive_secondrow')

同樣的,可以用上面的方法找到遊戲的連結、ID,這裡就不贅述了。

評分和打分人數藏在span標籤裡,如果用字典查詢的話會比較麻煩,所以我們稍後使用正則表示式將它倆提取出來:

不幸運的是,有些遊戲因為還沒上架,所以沒有人評論,我們用正則表示式得到的資訊是亂碼。所以我們用函式來防止出現亂碼的可能性:

def get_reviewscore(review):
gamereview=[]
for i in range(len(review)):
try:
score = re.search('br>(\d\d)%',str(review[i]))[1]
except:
score = ''
gamereview.append(score)
return gamereview
###########################################
def get_reviewers(review):
reviewers=[]
for i in range(len(review)):
try:
ppl = (re.search('the\s(.*?)(\s)user',str(review[i]))[1])
except:
ppl = ''
reviewers.append(ppl)
return reviewers

如果看到這裡的讀者覺得很輕鬆,那我便可以繼續往下述說,因為爬取價格比評論更加麻煩。但僅限於麻煩,並沒有很高大上的操作;而我相信我並不是用聰明的方法爬取到想要的結果,因為對於這個體量的資料再優化的程式碼對於執行時間來說相差不大。反正結果都一樣,管它呢。

其實要找物品的最終價格(即免費遊戲,打折後或未打折的遊戲價格)非常簡單,因為他就藏在這裡:

預設後邊兩位為小數點後兩位,所以我們直接把這串數字揪出來併除以100:

def get_finalprice(price):
finalprice=[]
for i in range(len(price)):
pricelist = int(re.search('final(\W+?)(\d+)(\W)',str(price[i]))[2])/100
finalprice.append(pricelist)
return finalprice

但我們如果就想知道他的原價,以便之後做分析該怎麼辦呢?

先看一下steam排行榜上的價格有三種顯示方法:

第一種,帶有劃線價格的打折商品,在原始碼中長這樣:

第二種,免費的:

頭疼的是,免費的標識也有變體:

(連to的大小寫也有不一樣的……steam您用點心!)

不過Free還是老老實實在最前面,所以我們後邊只要找到Free就好啦。

第三種,原價顯示:

上面的圖片都是我在抽查的時候發現的規律與變形,為了避免後續幾千個遊戲有“烏合之眾”,我在程式碼裡只查詢這三種格式,如果有奇形怪狀的資料出現,直接一棍子打成“空值”:

def get_price(price):
oripricelist=[]
for i in range(len(price)):
try:
oripricelist.append(price[i].find_all(class_="col search_price responsive_secondrow")[0].text)
except:
oripricelist.append(price[i].find_all(class_="col search_price discounted responsive_secondrow")[0].text) ori_price=[]
for i in range(len(oripricelist)):
try:
search = re.search('Free',oripricelist[i])[0].replace('Free','0')
except:
if oripricelist[i]== '\n':
search=''
else:
try:
search = re.search('HK.*?(\d+\.\d+)\D',oripricelist[i])[1]
except:
search=''
ori_price.append(search)
return ori_price

定義完這些想要的資料之後,我們就開始跑迴圈了。

先把我們要的資料列命好名字:

def get_data(games=1000):
num_games = games
gamename=[]
gamereview=[]
gamereviewers=[]
gamerelease=[]
oriprice=[]
final_price=[]
appid=[]
website=[]

接著我們以每個連結查詢100個遊戲的步伐開始跑迴圈並將裡邊的資訊找出來,錄入上面的列表裡:

       page = np.arange(0,num_games,100)
for num in page:
url = 'https://store.steampowered.com/search/results/?query&start='+str(num)+'&count=100&dynamic_data=&sort_by=_ASC&category1=998&snr=1_7_7_globaltopsellers_7&filter=globaltopsellers&infinite=1'
print('the {} iteration: Trying to connect...'.format((num/100)+1))
content = requests.get(url).content
jsontext = json.loads(content)
soup = BeautifulSoup(jsontext['results_html'],'html.parser')
name = soup.find_all('span',class_ ='title')
review = soup.find_all('div', class_ ='col search_reviewscore responsive_secondrow')
listdate = soup.find_all('div', class_ ='col search_released responsive_secondrow')
price = soup.find_all('div', class_ = 'col search_price_discount_combined responsive_secondrow')
href = soup.find_all(class_='search_result_row ds_collapse_flag')
for i in name:
gamename.append(i.text)
getreview = get_reviewscore(review)
for i in getreview:
gamereview.append(i)
getreviewers = get_reviewers(review)
for i in getreviewers:
gamereviewers.append(i)
for i in listdate:
gamerelease.append(i.text)
getprice = get_price(price)
for i in getprice:
oriprice.append(i)
getfinalprice = get_finalprice(price)
for i in getfinalprice:
final_price.append(i)
for i in range(len(href)):
appid.append(eval(soup.find_all(class_='search_result_row ds_collapse_flag')[i].attrs['data-ds-appid']))
website.append(soup.find_all(class_='search_result_row ds_collapse_flag')[i].attrs['href'])
print('done')

我們在遍歷中每次訪問頁面、完成每次迴圈的時候都讓電腦列印一段字,以便出錯的時候能快速找出出錯的頁面。

接下來就將得到的資料塞進一個資料表裡:

    df = pd.DataFrame(data=[gamename,gamereview,gamereviewers,gamerelease,oriprice,final_price,appid,website]).T
df.columns = ['name','review_score','reviewers','release_date','ori_price','final_price','id','link']
return df #呼叫我們的函式:
df = get_data(10000) #這裡的數字代表爬取10000個遊戲

等待漫長的過程與欣賞成功的過程:

最後的資料集長這樣:

接下來只要儲存為csv格式,就可以開始分析資料了。但這已經不是爬蟲文章的內容,所以不會往下繼續分析啦。

總結與反思

我發現final_price也就是一開始提取的最終價格中,會有高於原價的現象。

比如CS:GO的最終價格並不是0,是因為它有一個升級包:

前1000個遊戲裡總共有3個這樣的錯誤:

實況足球2020 是demo版免費,而想體驗完整遊戲確實需要78港幣;

奇異人生1 是第一篇章免費,後邊的篇章需要23.8港幣。

這些程式碼跑起來雖然快,但得到的資訊依舊太少,如果要深入研究steam的資料還是需要有強大的耐心遍歷所有遊戲連結吶。

這次的爬蟲經歷其實也發現了steam一些錄入大資料庫的時候的小差錯,比如前面所提到的免費標識竟然有3種變體,但他們可能覺得問題不大。

終於整理結束,趕緊結尾:

這次的文章就到這裡,希望對大家有所幫助~!