taptap关于王者荣耀评论的爬虫与数据可视化

背景

玩家评论可以为游戏的版本迭代提供重要参考,假如可以快速定位玩家的负面评价,则能够节约收集意见的时间成本。本项目通过文本挖掘方法,展示从数据采集到情感模型评价的全过程。

一、爬虫

TAPTAP评论数据通过JSON返回,使用python中的Requests库非常容易就可以提取里面的内容。下面这幅图是Fiddler抓包时看到的数据:

在这里插入图片描述

运行环境

请在python环境下运行,本次程序的开发环境为python 3.8.1

基本功能

这个程序根据你提供的游戏id,按评论最近更新时间,自动抓取每条完整的游戏评论和它的关联信息,存放到csv文件中。

  • 前置准备: 如果你将csv文件的保存路径设置在C盘,最好手动建立路径,否则可能会出现premission dennied,抓取的数据保存不成功
  • 爬虫运行: 最大抓取页数为990页,由于taptap的设置,单个游戏在990以后的评论数据无法访问。爬取过程中出错,重新运行程序,程序会自动在断点位置续爬
  • 爬虫结束: 达到上限,程序报页面无法打开,数据已在csv文件中保存

抓包获取游戏id

  1. 因为fiddler抓包的原理就是通过代理,所以被测终端需要和安装fiddler的电脑在同一个局域网中。

  2. 开启Fiddler的远程连接,Fiddler 主菜单 Tools -> Fiddler Options…-> Connections页签,选中Allowremote computers to connect,并记住端口号为8888,等会设置手机代理时需要。设置好后重启fiddler保证设置生效。设置如下:

20160216140525429

  1. 查看电脑IP,在CMD中输入ipconfig或者直接在开热点设备上查看连接设备的ip。

  2. 在手机侧设置代理

设置-WLAN,找到连接的wifi-选择修改网络-勾选高级选项,选择代理为手动,填入代理服务器为自己电脑IP,端口填入刚刚记住的8888。iPhone则可以在对应wifi的设置中直接配置HTTP代理。

  1. 开始抓包

在手机上对APP进行操作,从fiddler上即可看到对应的网络请求信息与游戏id。

王者荣耀

和平精英

原神

英雄联盟手游

人类跌落梦境

断点续传

建立断点txt文件,在因网络等原因中断时,重启程序,可以在断点处续爬,在中断时,已缓存的数据将保存至csv

def resume(self):
    """
    爬取出错时,将出错url的‘from’参数值保存至txt中,中断爬虫。再次运行爬虫程序后,从此页继续爬取
    :return: 本次续连url的‘from’参数值
    """
    start_from = 0
    if os.path.exists(STOP_POINT_FILE):
        with open(STOP_POINT_FILE, 'r') as f:
            start_from = int(f.readline())
    return start_from

爬虫休眠

文明爬虫,虽未发现反爬,但爬完每个页面后暂停0-2秒,减轻服务器负担

import random
import time
pause = random.uniform(0, 2)
time.sleep(pause)

编码转换

python中比较容易出现编码问题,在中文环境下更甚,评论里可能会有无法打印的字符,虽然不影响数据下载,但容易影响后续处理。先把数据进行gbk编码,丢弃无法识别的字符,再进行解码,最后将数据保存为utf-8格式,上面的问题就不存在啦~

 review['author'] = r.get('author').get('name').encode('gbk', 'ignore').decode('gbk')

其他信息

每页10条数据,每个游戏的评论最多可爬990页,超过990页,TAPTAP拒绝访问。

991

程序将采集到的数据存放至你指定路径的csv中。

爬虫完整代码

import requests
import os
import re
import random
import time
import csv

# 请求头
HEADERS = {'Host': 'api.taptapdada.com',
           'Connection': 'Keep-Alive',
           'Accept-Encoding': 'gzip',
           'User-Agent': 'okhttp/3.10.0'}
# 基础页面 每个页面有10条评论,'from'参数表示评论序号,从0开始,每+10翻页一次
BASE_URL = 'https://api.taptapdada.com/review/v1/by-app?sort=new&app_id={}' \
            '&X-UA=V%3D1%26PN%3DTapTap%26VN_CODE%3D593%26LOC%3DCN%26LANG%3Dzh_CN%26CH%3Ddefault' \
            '%26UID%3D8a5b2b39-ad33-40f3-8634-eef5dcba01e4%26VID%3D7595643&from={}'
# 保存断点的文件
STOP_POINT_FILE = 'stop_point.txt'


class TapSpiderByRequests:
    def __init__(self, csv_save_path, game_id):
        """
        获取断点,激活爬虫
        """
        # 获取断点
        self.start_from = self.resume()
        # 重置保存评论的列表
        self.reviews = []
        # 运行爬虫
        self.spider(csv_save_path, game_id)

    def spider(self, csv_save_path, game_id):
        """
        发送请求,验证访问状态
        :return: 网页返回的json数据
        """
        end_from = self.start_from + 300
        # 循环爬取30页
        for i in range(self.start_from, end_from+1, 10):
            url = BASE_URL.format(game_id, i)
            try:
                resp = requests.get(url, headers=HEADERS).json()
                resp = resp.get('data').get('list')
                self.parse_info(resp)
                print('=============已爬取第 %d 页=============' % int(i/10))

                # 等待0至2秒,爬下一页
                if i != end_from:
                    print('爬虫等待中...')
                    pause = random.uniform(0, 2)
                    time.sleep(pause)
                    print('等待完成,准备翻页。')
                # 顺利爬至末页,则保存断点
                else:
                    with open(STOP_POINT_FILE, 'w') as f:
                        f.write(str(i+10))

            # 出错,则中断爬虫,保存断点
            except Exception as error:
                with open(STOP_POINT_FILE, 'w') as f:
                    f.write(str(i))
                # 打印异常信息
                print('爬取第%i页出现异常,断点已保存,异常信息如下:' % int(i/10))
                raise error
                # 退出程序
                exit()

        # 将信息写入csv
        self.write_csv(csv_save_path, self.reviews)

    def parse_info(self, resp):
        """
        :param resp: 本页返回的json数据
        :return: 将本页评论信息追加至REVIEWS列表
        """
        for r in resp:
            review = {}
            # id
            review['id'] = r.get('id')
            # 昵称
            review['author'] = r.get('author').get('name').encode('gbk', 'ignore').decode('gbk')
            # 评论时间
            review['updated_time'] = r.get('updated_time')
            # 设备
            review['device'] = r.get('device').encode('gbk', 'ignore').decode('gbk')
            # 游玩时长(分钟)
            review['spent'] = r.get('spent')
            # 打分
            review['stars'] = r.get('score')
            # 评论内容
            content = r.get('contents').get('text').strip()
            review['contents'] = re.sub('<br />|&nbsp', '', content).encode('gbk', 'ignore').decode('gbk')
            # 支持度
            review['ups'] = r.get('ups')
            # 不支持度
            review['downs'] = r.get('downs')

            self.reviews.append(review)

    # 断点续传
    def resume(self):
        """
        爬取出错时,将出错url的‘from’参数值保存至txt中,中断爬虫。再次运行爬虫程序后,从此页继续爬取
        :return: 本次续连url的‘from’参数值
        """
        start_from = 0
        if os.path.exists(STOP_POINT_FILE):
            with open(STOP_POINT_FILE, 'r') as f:
                start_from = int(f.readline())
        return start_from

    # 追加写入csv
    def write_csv(self, full_path, reviews):
        """
        :param full_path: csv保存的完整路径
        :param reviews: 列表形式的评论信息
        """
        title = reviews[0].keys()
        path, file_name = os.path.split(full_path)
        if os.path.exists(full_path):
            with open(full_path, 'a+', newline='', encoding='utf-8-sig') as f:
                writer = csv.DictWriter(f, title)
                writer.writerows(reviews)
        else:
            try:
                os.mkdir(path)
            except Exception:
                print('路径已存在,或未获得建立路径的权限。请检查路径是否存在,或手动建立路径。')
            with open(full_path, 'a+', newline='', encoding='utf-8-sig') as f:
                writer = csv.DictWriter(f, title)
                writer.writeheader()
                writer.writerows(reviews)


if __name__ == '__main__':
    # csv保存路径
    csv_save_path = r'.\data\tap_reviews.csv'
    # 游戏id      王者荣耀:2301;和平精英:70056;原神:168332~更多可以抓包获取
    game_id = 2301
    # 循环爬取至990页
    for i in range(33):
        TapSpiderByRequests(csv_save_path, game_id)

爬虫数据文档

爬取数据

二、数据清洗

这一步主要为数据可视化服务,使用pandas库可以很方便地进行数据清洗。

新增列

评论热度

点赞数和点踩数的总和,进行归一化表示

data['heat'] = data['ups'] + data['downs']
data['heat'] = (data['heat'] - data['heat'].min()) / (data['heat'].max() - data['heat'].min())

评分

评论标星的2倍,标星范围1-5,评分范围2-10

data['score'] = data['stars']*2

评论净支持数

data['net_support'] = data['ups'] - data['downs']

转换

时间戳转换日期

为了让pyecharts识别出时间标签,需要进行日期转换

import time
data['updated_time'] = data['updated_time'].apply(lambda x: time.strftime('%Y-%m-%d', time.localtime(x)))

替换

替换游玩时间中的0值:

实际情况下,玩家不太可能在未游玩的情况下评论(或者说这些评论意义不大),将游玩时间0替换为缺失是合理的,当进行相关维度的可视化,这些缺失值将不会被考虑

data['spent'] = data['spent'].replace(0, np.nan)

删除

用正则表达式删除无意义字符

一些同学发评论比较喜欢用颜表情,但在爬虫过程中,gbk编码下无法全部显示,只能删掉意义不明的那另一半

import re
data['contents'] = data['contents'].apply(lambda x: re.sub('&[\w]+;', '', str(x)))
data['contents'] = data['contents'].apply(lambda x: re.sub('\(\s*\)', '', str(x)))

删除无意义的列数据

import pandas as pd
data.drop(['ups', 'downs'], axis=1, inplace=True)

清洗完整代码

# coding=gbk
import pandas as pd
import time
import re
import numpy as np


# 爬虫获取的数据的所在路径
csv_path = r'.\data\tap_reviews.csv'
# 清洗后的数据的保存路径
clean_path = r'.\data\tap_reviews-extend cleaned.csv'

# 读取数据
data = pd.read_csv(csv_path, header=0, index_col='id')

# # 查看前20条数据和列名
# print(data[:20])
# print(data.columns)

# 将评论时间由时间戳转日期
data['updated_time'] = data['updated_time'].apply(lambda x: time.strftime('%Y-%m-%d', time.localtime(x)))
# 评论净支持数
data['net_support'] = data['ups'] - data['downs']
# 评论热度
data['heat'] = data['ups'] + data['downs']
data['heat'] = (data['heat'] - data['heat'].min()) / (data['heat'].max() - data['heat'].min())
# 评分
data['score'] = data['stars']*2

# 将游玩时间为0的标注为缺失值
data['spent'] = data['spent'].replace(0, np.nan)

# 清除无意义字符
data['contents'] = data['contents'].apply(lambda x: re.sub('&[\w]+;', '', str(x)))
data['contents'] = data['contents'].apply(lambda x: re.sub('\(\s*\)', '', str(x)))
# 删除用不上的列
data.drop(['ups', 'downs'], axis=1, inplace=True)
# 保存数据,转换成utf-8编码
data.to_csv(clean_path, encoding='utf_8_sig')

清洗后数据文档

三、可视化

查看数据分布情况,通过对评论的长度分析,利用星级评论情感分布分析,一定程度上查看玩家对游戏整体的满意度。

从时间、设备、玩家印象维度可视化评论数据,这一步使用pyecharts库。pyecharts库是python生成Eharts图表的轮子,官方文档中就有 丰富的图表实例。颜值高,上手容易,入股不亏。

查看数据分布情况

查看评论长度分布

data['contents_length'] = data['contents'].apply(lambda x: len(str(x)))
len_se = data.groupby('contents_length')['contents_length'].count()
sns.distplot(len_se, bins=20, hist=True, kde=True, rug=True)
plt.title('taptap评论长度分布')
plt.show()
print('评论长度的9/10分位数:', data['contents_length'].quantile(0.9))

计算分位数发现,90%评论在193字内;密度图表明,评论长度集中在100字以内

查看评论情感分布

positive = len(data['stars'][data['stars'] >= 4])
total = len(data['stars'])
negative = total - positive
sns.distplot(data['stars'], bins=5, kde=False, norm_hist=True)
plt.title('taptap评论情感分布')
plt.show()
print('正面评价: %d,占总数的%.2f%%    负面评价: %d, 占总数的%.2f%%' %
      (positive, (positive/total*100), negative, (negative/total*100)))

正面评价: 2468,占总数的30.62% ;负面评价: 5592, 占总数的69.38%。

时间维度的考察

王者荣耀每日评分均值变化

image-20210512172003692

王者荣耀每日评分次数

image-20210429211906875

归因:04.08赛季更新

image-20210429212010527

设备维度的考察

评论活跃设备Top15

image-20210429212325016

评分较低设备Top10

image-20210429214528633

玩家印象

使用文本挖掘的预处理方法对TOP500支持度和热度达到0.5的评论进行处理,得到了玩家对这个游戏的关键评价

可视化完整代码

在线查看网页

四、后记

对游戏的评价星数并不能真实的反映每个玩家对游戏的感情态度,因此我们还需要对评论数据进行数据挖掘、建立LSTM模型并对模型进行评价。

参考文献

posted @ 2022-10-10 21:46  baixf白小飞  阅读(1104)  评论(2编辑  收藏  举报