【一起学Python】STEAM游戏评测爬虫

时间:2022-05-07
本文章向大家介绍【一起学Python】STEAM游戏评测爬虫,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

别催更,越催越懒得写。催更只接受赞赏…可惜我的微信还没有赞赏的功能…

今天刚接的需求&新鲜的代码…

有个大佬昨天跟我说

来给我爬一下Steam的游戏评测吧,我要这个数据,这个数据,还有这个数据。效率我不管,存储方式我不管,数据分析我不管,你爬好了跟我说。

于是就有了今天的文章。

闲话少叙,我挑核心的部分来记录今天的工作。 主线任务:给定某STEAM平台游戏,抓取其评测相关信息(包括但不限于upvote/downvote、昵称、时间、评论等) 支线任务:抓取评价用户的游戏库存 隐藏任务:对用户评论进行情感语义分析,并对比其推荐/不推荐分析语义和评价的相关性

这篇文章里我们的目标是完成主线和隐藏任务,支线任务之后再写一篇。

第一步,确定需求和入口

需求前面已经给定了,那么确定我们抓取的入口,也就是网页链接。 以最近颇具争议的游戏 H1Z1 为例。打开其STEAM商店页面: http://store.steampowered.com/app/433850/ 在页面最下方找到“浏览所有评测”,获取入口链接: http://steamcommunity.com/app/433850/reviews/?browsefilter=toprated&snr=15_reviews

第二步,使用Python模拟请求,获得页面源码

使用firebug(或者Chrome的F12)抓网络请求。

发现只有三个请求,下面三个都Google Analytics的统计脚本,也就是说我们要抓取的内容必然在第一个请求里。 使用Python构造网络请求,并打印源码。

import requests
url = ‘http://steamcommunity.com/app/433850/reviews/?browsefilter=toprated&snr=15_reviews‘ 
html = requests.get(url).text
print(html)

打印出来的源代码太长了,就不再展示了。

第三步,parse&extract 从页面源码中提取内容

获取到页面源码后,我们需要从繁杂的源代码中提取出我们需要的内容。我们可以通过审查元素迅速定位内容所在的标签。

定位到比较清晰的标签后,推荐通过BeautifulSoup直接进行提取。 当然这里有一个小小的隐藏知识,如果你直接查看这个请求的HTML的话,会发现里面并没有直接展示出评测内容。也就是说评测内容其实是在页面加载的过程中由JS渲染完成的。

在有些网站的处理中,会将JS和需要渲染的内容分为两次请求发送。 这次的处理没有那么复杂,如果有人根本没发现JS渲染这一步而直接去解析页面源码的话,也是没有问题的。 下面我们使用BeautifulSoup进行相应的标签定位和解析,我就不赘述过程了。只要定位到相应标签,然后直接使用soup.find()就可以了。

from bs4 import BeautifulSoup

soup = BeautifulSoup(html, 'html.parser')  # 如果装了lxml,推荐把解析器改为lxml
reviews = soup.find_all('div', {'class': 'apphub_Card'})
for review in reviews:
    nick = review.find('div', {'class': 'apphub_CardContentAuthorName'})
    title = review.find('div', {'class': 'title'}).text
    hour = review.find('div', {'class': 'hours'}).text.split(' ')[0]
    link = nick.find('a').attrs['href']
    comment = review.find('div', {'class': 'apphub_CardTextContent'}).text
    print(nick.text, title, hour, link, )
    print(comment.split('n')[3].strip('t'))

这样我们就能将需要的信息提取并一一打印出来了。但是这时候我们又发现了另一个问题,为什么这边打印出来的全都是英文,而且跟我们在网页上看到的评测也不一样啊。原因其实是我们浏览器存了cookies,而且默认的语言是简体中文和英语,所以会把中文评测放在前面。 那么如何让我们的程序和浏览器输出结果一致呢?——添加headers。只需要添加一行就可以了。

import requests

url = 'http://steamcommunity.com/app/433850/reviews/?browsefilter=toprated&snr=15_reviews'headers = {    'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3'}
html = requests.get(url, headers=headers).text

第四步,the more, the better

这个时候我们发现,当我们使用浏览器时,只要滚动到页面底部,就会加载出另外10条评测。

那么如何用Python代码来实现这些额外内容的抓取呢? 本着空穴不来风的态度,我们要坚信,我们自己的电脑本地肯定不会凭空变出内容来的,那么这个下拉加载的过程中肯定发生了新的网络请求。打开Firebug的网络面板,点选保持。查看请求的情况。

很显然,这就是每次下拉加载的新内容所在的请求。我们只要继续模拟这个请求就可以了。 分析它的具体参数。

根据参数名,我们可以大概猜测出其中大部分参数的含义。 其中比较重要的几个是: offset 偏移值,固定为10,也就是每次获取新的10条。 其余几个2,是控制页数的,也就是需要翻页的话,只要把2相应地换成3 4 5…就可以了。 至于appHubSubSection、browsefilter、filterLanguage和l这几个,我是在之后的尝试中发现的其真正含义。这里暂时先不管了。

接下来通过我们刚才的发现,尝试抓取50条评测。对于参数有两种处理办法,一种是用字典作为请求参数payload,另一种是直接拼接URL,这次我们选择后者。

import requests
from bs4 
import BeautifulSoup

headers = {    'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3'}for i in range(1, 6):
    url = 'http://steamcommunity.com/app/433850/homecontent/?userreviewsoffset=' + str(10 * (i - 1)) + '&p=' + str(i) + '&workshopitemspage=' + str(i) + '&readytouseitemspage=' + str(i) + '&mtxitemspage=' + str(i) + '&itemspage=' + str(i) + '&screenshotspage=' + str(i) + '&videospage=' + str(i) + '&artpage=' + str(i) + '&allguidepage=' + str(i) + '&webguidepage=' + str(i) + '&integratedguidepage=' + str(i) + '&discussionspage=' + str(i) + '&numperpage=10&browsefilter=toprated&browsefilter=toprated&appid=433850&appHubSubSection=10&l=schinese&filterLanguage=default&searchText=&forceanon=1'
    html = requests.get(url, headers=headers).text
    soup = BeautifulSoup(html, 'html.parser')  # 如果装了lxml,推荐把解析器改为lxml
    reviews = soup.find_all('div', {'class': 'apphub_Card'})    
    for review in reviews:
        nick = review.find('div', {'class': 'apphub_CardContentAuthorName'})
        title = review.find('div', {'class': 'title'}).text
        hour = review.find('div', {'class': 'hours'}).text.split(' ')[0]
        link = nick.find('a').attrs['href']
        comment = review.find('div', {'class': 'apphub_CardTextContent'}).text
        print(nick.text, title, hour, link, )
        print(comment.split('n')[3].strip('t'))

至此我们就可以随心所欲地通过控制页数来控制抓取数量了。 当然了,在我给大佬的最终实现里,是通过while True加跳出break的方法来抓取所有评测的。鉴于评测可能非常非常多,大家一般也用不到,少量抓取还是直接自己控制参数吧~

第五步,save and load

之前写代码的过程中,我们都是直接在控制台打印内容。总不能让大佬到控制台手动复制粘贴吧,还是要把结果存起来的。

我之前其实很喜欢把结果通过xlwt库存到Excel文件里,但是有些时候会出错,性能也不够好。后面发现了一种更简单直接的操作,那就是通过在txt文件中添加制表符分隔,在粘贴进excel时实现自动分列。

现在直接添加写入文件的相关代码就可以了。

file = open('steam.txt', 'w+', encoding='utf-8')
file.write() # balabala
file.close()

第六步,做一点简单的情感分析

只有数据不好玩呀。现在流行搞数据分析。我们也来玩一玩。 http://bosonnlp.com/ boson提供免费的语义接口试用。自行注册之后获取API_TOKEN。其中今天用到的情感分析接口的文档如下: http://docs.bosonnlp.com/sentiment.html

这篇文章的内容够多了…就不再赘述详情了。之后还会写文章来谈。直接给代码了。

import requests
import json
def sen_from_text(text):
    SENTIMENT_URL = 'http://api.bosonnlp.com/sentiment/analysis'
    h = {'X-Token': 'balabala'} # your token
    data = json.dumps(text)
    resp = requests.post(SENTIMENT_URL, headers=h, data=data.encode('utf-8'))
    resp = json.loads(resp.text)    # print(resp)
    front = float(resp[0][0])    
    return front

返回结果中的数字越接近0,负面情绪越高;越接近于1,证明情绪越高。 还是测试前50条的评论。

可以发现,推荐的评论情绪偏于证明。而不推荐的评论中,虽然有少量的异常值,但是可以看到评论中存在明显的正面性语言。其他大部分数值是符合的。

最后附上此次文章的全部代码。

import requests
from bs4 import BeautifulSoup
import json
def sen_from_text(text):
    SENTIMENT_URL = 'http://api.bosonnlp.com/sentiment/analysis'
    h = {'X-Token': 'balbala'} # your token
    data = json.dumps(text)
    resp = requests.post(SENTIMENT_URL, headers=h, data=data.encode('utf-8'))
    resp = json.loads(resp.text)    # print(resp)
    front = float(resp[0][0])    
    return front

headers = {    'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3'}
file = open('steam.txt', 'w+', encoding='utf-8')
for i in range(1, 6):
    url = 'http://steamcommunity.com/app/433850/homecontent/?userreviewsoffset=' + str(10 * (i - 1)) + '&p=' + str(
        i) + '&workshopitemspage=' + str(i) + '&readytouseitemspage=' + str(i) + '&mtxitemspage=' + str(
        i) + '&itemspage=' + str(i) + '&screenshotspage=' + str(i) + '&videospage=' + str(i) + '&artpage=' + str(
        i) + '&allguidepage=' + str(i) + '&webguidepage=' + str(i) + '&integratedguidepage=' + str(
        i) + '&discussionspage=' + str(
        i) + '&numperpage=10&browsefilter=toprated&browsefilter=toprated&appid=433850&appHubSubSection=10&l=schinese&filterLanguage=default&searchText=&forceanon=1'
    html = requests.get(url, headers=headers).text
    soup = BeautifulSoup(html, 'html.parser')  # 如果装了lxml,推荐把解析器改为lxml
    reviews = soup.find_all('div', {'class': 'apphub_Card'})    
    for review in reviews:
        nick = review.find('div', {'class': 'apphub_CardContentAuthorName'})
        title = review.find('div', {'class': 'title'}).text
        hour = review.find('div', {'class': 'hours'}).text.split(' ')[0]
        link = nick.find('a').attrs['href']
        comment = review.find('div', {'class': 'apphub_CardTextContent'}).text.split('n')[3].strip('t')
        sen = sen_from_text(comment)        # print(nick.text, title, hour, link, )

        file.write(
            nick.text + 't' + title + 't' + str(sen) + 't' + hour + 't' + link + 't' + comment + 'n')
file.close()

今天这篇写的十分草率。没有校稿,中间也漏掉了许多细节。第一次用Markdown排版代码也是各种问题……大家有问题的话可以在后台留言交流。

就爬虫本身我自己已知的都有很多问题,比如访问频率限制反爬,一些异常内容导致的页面解析失败,一些emoji字符导致的编码失败,等等。大部分问题我已经解决了。有关STEAM应该还会写一到两篇文章,会继续丰富内容,重构代码,解决问题的。

撒花。