别催更,越催越懒得写。催更只接受赞赏…可惜我的微信还没有赞赏的功能…
今天刚接的需求&新鲜的代码…
有个大佬昨天跟我说
来给我爬一下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')