
简介TapTap游戏评论文本挖掘项目是一份面向计算机相关专业学生的大三期末大作业完整源码与文档资料围绕游戏评论的采集、清洗、可视化和情感分析展开可作为课程设计、期末大作业或Python项目实战练习使用适合需要完整流程参考的初学者。整个资源包共13个文件、约66.45MB核心包含3个ipynb分析笔记分别对应评论爬取、数据清洗与pyecharts可视化另有3个md说明文档、2个py脚本、2个csv数据文件、pkl和model模型文件目录清晰能从原始评论数据一直复现到图表与情感结论。项目经导师指导并获得99分评审代码完整确保可运行对新手友好文档会解释各模块设计思路便于理解APP评论爬虫、数据清洗、可视化展示和情感分析等环节的重点与坑点也可迁移到其他游戏或平台评论场景。目前已有147人学习下载适合正在完成期末大作业或希望系统练习Python文本挖掘的学习者作为参考。1. 为什么拿 TapTap 评论做文本挖掘样本本身的三个反直觉特征把电商评论的情感分析套路直接搬到游戏社区第一轮就会翻车——这是 TapTap 评论与淘宝、京东评论最大的不同打 5 星的玩家可能写“卡池歪了卸了”打 1 星的玩家可能写“笑死这游戏垃圾得挺欢乐”。星级与文本情绪的错位率比电商高得多纯用评分当作标注反而会把模型教坏。与此同时游戏评论里有大量“水评论”“玩梗”“缩写黑话”长度短到不足 10 个字的评论可能占了三成常规自然语言处理流程里的分词、停用词和情感词典都显得水土不服。所以做 TapTap 游戏评论的文本挖掘核心不是“跑通一个爬虫”而是围绕游戏文本特性做一套可落地的链路APP 接口爬虫拿到原始评论数据清洗把噪音压下去pyecharts 把结果可视化情感分析负责给每个玩家评论打情绪分。这套流程适合想用真实游戏社区数据做文本挖掘的算法工程师、独立游戏运营和数据分析师。下面按我实际会做的顺序从爬虫一路推到可视化与验证。2. APP 爬虫的落地方式抓包驱动requests 直接请求评论接口2.1 为什么不用网页版评论接口TapTap 的网页版页面是服务端渲染加异步加载的混合结构直接用 requests 拉 HTML 再解析能在首屏拿到少量评论但一翻页就会遇到风控参数校验。更关键的是网页版的评论接口与移动端接口字段不一致安装时间、点赞数这类维度往往缺失。常见做法是用抓包工具观察手机端浏览评论时的网络请求找到真实的评论列表接口。这个接口返回的 JSON 结构非常稳定字段包括评论 ID、玩家昵称、评分rating、评论内容、设备标识、点赞数、回复数、创建时间等。相比网页版 HTML直接从 JSON 里取字段解析成本低也方便后续转 pandas 处理。抓包时重点记录两个信息完整的 URL 参数和请求头里的固定 Header。TapTap 的风控主要校验 Header 中的x-legacy-app-id、X-UA以及设备指纹相关字段这些值不是固定的以你自己设备抓包的结果为准。如果请求头里缺了这些接口大概率返回空列表或直接 HTTP 403。2.2 最小可用的评论爬虫代码以下代码是请求评论接口的最小骨架假设你已经通过抓包拿到了真实的接口路径和 Header 字段。核心逻辑是循环翻页把每页 JSON 里的列表数据追加到全局列表最后输出为 JSONLines 格式。import requests import json import time HEADERS { User-Agent: Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36, x-legacy-app-id: 你的抓包值, X-UA: 你的抓包值, } def fetch_reviews(app_id: str, max_pages: int 50): results [] for page in range(1, max_pages 1): # 以企业微信抓包结果为准不同版本接口参数名有差异 url https://www.taptap.cn/webapiv2/review/v1/list-by-app params { app_id: app_id, page: page, limit: 20, sort: new, } resp requests.get(url, paramsparams, headersHEADERS, timeout10) if resp.status_code ! 200: print(f第 {page} 页请求失败: {resp.status_code}) break data resp.json() reviews data.get(data, {}).get(list, []) if not reviews: break results.extend(reviews) time.sleep(1.5) # 控制请求频率避免触发风控 return results reviews fetch_reviews(app_id你的游戏ID) with open(data/raw_reviews.jsonl, w, encodingutf-8) as f: for item in reviews: f.write(json.dumps(item, ensure_asciiFalse) \n) print(f共抓取 {len(reviews)} 条评论)这段代码里需要特别说明的参数有三个。第一是sort参数new表示按时间倒序抓历史评论建议用new而hot会优先返回高热评论适合做舆情分析但会丢失长尾样本。第二是limitTapTap 接口一般限制单页最大 20 条调大不会报错但响应会变慢。第三是time.sleep(1.5)触发风控后不只是限流还会让接口返回验证码页面所以频率控制是爬虫能否跑完全量的关键。2.3 抓包之后第一件事确认字段语义拿到原始 JSON 后不要急着写清洗逻辑先把单条评论的字段打出来看一遍。不同版本的 App 返回的字段名会有差异常见的是rating是 1 到 5 的整数、edited_at是时间戳、developer_reply是开发者回复对象。有些字段是嵌套结构比如user下有namestat下有like_count和reply_count。我习惯先把字段铺平成表格再决定清洗策略。下面是一个字段映射示例实际字段名以抓包结果为准原始字段含义清洗后字段名保留原因id评论唯一标识review_id去重主键rating玩家星级1-5star监督信号contents.text评论文本content核心分析对象user.name玩家昵称username辅助识别水军stat.like_count点赞数likes舆情权重edited_at发布时间戳ts时间趋势分析字段都确认之后再进入数据清洗环节。这一步如果跳过了后面的情感分析和可视化都会建立在不完整或错位的字段上返工成本很高。3. 游戏评论区数据清洗的硬规则正则、去重和坏样本过滤3.1 游戏评论为何比电商评论脏电商评论的文本相对规矩游戏评论却有大量“复读机”内容。TapTap 上低质量评论通常分三类一是纯表情或纯标点比如“666666”“哈哈哈哈哈”二是复制粘贴的官方活动文案一个活动能让几千条评论完全相同三是包含大量 用户、抽奖口令、QQ 群广告的评论。这些样本如果进入情感分析训练集会让模型学到“哈哈”就是正面的错误映射。所以清洗的目标不是做得越干净越好而是保留下对情感判断有意义的文本内容。过分激进的清洗比如把所有短句都删掉会把玩家的真实态度也删没了“好玩”“拉胯”这种两字评价虽然短但恰恰是情感极性最强的样本。3.2 清洗规则的优先级排序数据清洗规则应该从“低成本高收益”的开始依次处理。下面这张表是我处理游戏评论区时固定使用的规则顺序规则处理方式示例去 URL正则匹配 http/https 链接并删除“详情见 https://t.cn/x” → “详情见”去 用户匹配 后跟非空字符直到空格或标点“策划 来聊聊” → “来聊聊”去表情符号匹配 Emoji 的 Unicode 范围并删除“好玩” → “好玩”压缩重复字符连续 3 个以上相同字符压缩到 1-2 个“哈哈哈哈哈” → “哈哈”繁体转简体使用 OpenCC 的 t2s 转换“這遊戲真好玩” → “这游戏真好玩”去除空白字符合并多余空格和换行多行文本合并为单行短文本过滤按分词后 token 数量删减“666”、空文本直接丢弃以下代码对应实现这些规则代码里尤其要注意表情匹配的 Unicode 范围Python 的re模块处理 Emoji 需要专门的正则import re import pandas as pd def clean_comment(text: str) - str: if not text or not isinstance(text, str): return # 去 URL text re.sub(rhttp\S|https\S, , text) # 去 用户 text re.sub(r[^\s:,。], , text) # 去 Emoji 和特殊符号 emoji_pattern re.compile( [\U0001F300-\U0001FAFF \U00002600-\U000027BF \U0001F900-\U0001F9FF \U00002000-\U0000206F], flagsre.UNICODE ) text emoji_pattern.sub(, text) # 压缩重复字符 text re.sub(r(.)\1{2,}, r\1\1, text) # 合并空白 text re.sub(r\s, , text).strip() return text代码里比较值得说明的是压缩重复字符正则(.)\1{2,}。玩家表达强烈情感时会故意拉长字像“气死了了了了了”如果不去重分词时“了”会被当作多个独立词影响词频统计和情感词典匹配。压缩到两个字符保留“了了”这种适度延长感不会改变原意。3.3 用 pandas 做批量清洗和坏样本过滤清洗是批量操作pandas 在这里是最顺手的工具。把原始 JSONL 读入 DataFrame逐列应用清洗函数同时做用户级去重和内容级去重import pandas as pd from opencc import OpenCC df pd.read_json(data/raw_reviews.jsonl, linesTrue) df[content_raw] df[contents].apply(lambda x: x.get(text, )) df[content_clean] df[content_raw].apply(clean_comment) # 繁体转简体 cc OpenCC(t2s) df[content_clean] df[content_clean].apply(cc.convert) # 内容去重多个用户发相同文本时只保留点赞数最高的一条 df df.sort_values(likes, ascendingFalse) df df.drop_duplicates(subset[content_clean], keepfirst) # 短文本过滤按词数过滤 import jieba def token_count(text: str) - int: return len([w for w in jieba.lcut(text) if w.strip()]) df[token_cnt] df[content_clean].apply(token_count) df df[df[token_cnt] 2] # 过短或清洗后为空直接删除 df df[df[content_clean].str.len() 2] df.to_csv(data/cleaned_reviews.csv, indexFalse, encodingutf-8-sig) print(f清洗后保留 {len(df)} 条评论)这段批量清洗里最容易被忽略的是drop_duplicates的likes排序。游戏评论区经常出现同一个梗被复制几百条的情况比如“今天出货了”这种抽卡玄学回复。直接去重会留下最早的那条而不是热度最高的那条后续做词频统计和可视化时热梗的权重会被低估。先按点赞数降序排列再保留首个是文本挖掘里常用的“热度去重”策略。清洗完的数据要人工抽查至少 200 条看看误删比例是否过高。如果清洗规则过猛通常会看到大量文本被截成残句这个时候要放宽过滤条件而不是继续加规则。游戏评论挖掘的清洗目标是把脏文本压到总样本的 10% 以下而不是追求 0 噪音过度清洗会损失真实的态度表达。4. 情感分析的两条路线自定义情感词典 机器学习对照4.1 为什么先做情感词典而不是直接上 BERT做完数据清洗后的下一步就是情感分析。很多项目一上来就打算用预训练模型做情感分类但游戏评论场景有个现实问题标注数据少。TapTap 评论没有现成的“正面/负面/中性”标签你手上唯一靠谱的弱监督信号是玩家的星级评分。用星级当标签数据量立刻就有几万条但正如第一章说的星级与文本情感存在大量错位直接训练深度学习模型的收敛效果不稳定。我一般会先做一条词典法的基线——依赖情感词典对评论文本进行情感打分。这条路的好处是零标注成本、结果可解释坏处是准确率上限受词典覆盖率限制。游戏玩家爱用黑话“绝了”“破防”“防沉迷”这类词在通用情感词典里要么没有、要么极性标注和游戏语境完全相反。所以必须先构建一个小规模的游戏领域情感词典再配合通用词典使用。4.2 基于情感词典和规则的情感打分实现构建规则的时候要处理三个要素情感词、否定词、程度副词。以下代码构建了一个轻量级的情感打分器语法简单可以直接在 Jupyter 里跑import jieba import re # 模拟一个游戏领域情感词典实际使用可扩充到几千词 pos_words {好玩: 2, 好评: 2, 神作: 3, 良心: 2, 爽: 1, 爱了: 2, 保底: 1, 出货: 1, 上头: 1} neg_words {垃圾: -3, 拉胯: -2, 骗氪: -2, 逼氪: -3, 煎熬: -2, 卸载: -2, 闪退: -3, 破防: -2, 无语: -1, 防沉迷: -2} negators {不, 没, 别, 无, 莫, 非} boosters {非常: 1.5, 特别: 1.8, 太: 1.6, 极度: 2.0, 有点: 0.7, 稍微: 0.5, 挺: 1.2} def sentiment_score(text: str) - float: words jieba.lcut(text) score 0.0 negate False boost 1.0 for w in words: if w in negators: negate True continue if w in boosters: boost boosters[w] continue if w in pos_words: score pos_words[w] * boost * (-1 if negate else 1) elif w in neg_words: score neg_words[w] * boost * (-1 if negate else 1) negate False boost 1.0 return score df[sentiment_score] df[content_clean].apply(sentiment_score) df[sentiment_label] pd.cut(df[sentiment_score], bins[-10, -1, 1, 10], labels[负面, 中性, 正面])这段代码的逻辑理解起来很简单遍历分词结果遇到否定词就翻转当前极性遇到程度副词就临时加权遇到情感词就把权重累加。pd.cut把连续的情感分数切成三档用于后续准确率评估。这里有两点容易踩坑第一negate和boost作用到下一个情感词后要立即重置否则“不是很垃圾”会被误判成强负面第二情感词典要按“词性领域”来组织像“保底”这个词在原神语境里是中性偏正面在有些游戏里却是吐槽这种必须结合具体游戏来定义。4.3 用 TF-IDF 加机器学习做对照实验词典法给出基线后再用机器学习方法做对照才有意义。注意这里不是简单比准确率而是在“星级标签错位”的前提下观察两种方法对哪些样本的预测不一致这些不一致样本就是后续人工分析的对象。特征用 TF-IDF模型用朴素贝叶斯两步就能跑通from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.pipeline import make_pipeline from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report # 用星级构造弱标签4-5星为正面1-2星为负面3星丢弃或视任务保留 df_labeled df[df[star].isin([1, 2, 4, 5])].copy() df_labeled[label] df_labeled[star].apply(lambda x: 1 if x 4 else 0) X_train, X_test, y_train, y_test train_test_split( df_labeled[content_clean], df_labeled[label], test_size0.2, random_state42, stratifydf_labeled[label] ) model make_pipeline( TfidfVectorizer(ngram_range(1, 2), max_features5000), MultinomialNB() ) model.fit(X_train, y_train) print(classification_report(y_test, model.predict(X_test)))机器学习对照实验的合理预期是准确率约 70% 到 78%比词典法高但很难超过 85%因为弱标签里本来就有一成多的错位样本。当你看到大量“正面新闻被预测成负面”时不要急着调参先抽 50 条预测错误的样本看看是不是反讽、玩梗、中英混搭这些文本。如果错误集中在这几类说明问题不在模型而在文本特征——可以往 TF-IDF 特征里加入游戏专属词或者直接用星级与情感分数的分歧度来筛选训练样本。多模态情感分析在游戏评论上也有实践空间把“星级 评论文本”当成两种模态用不一致样本单独建模能明显提高反讽检测的响应速度。5. pyecharts 可视化词云、趋势折线和多图 Grid 布局5.1 可视化之前的数据聚合pyecharts 做可视化的核心是把 DataFrame 转成 pyecharts 需要的数据格式。不要直接在图表代码里写过滤逻辑而是预先用 pandas 做 groupby 聚合。这里通常会产出四张图评分分布柱状图、评论量按日趋势折线图、情感占比饼图、高频词词云。四张图组合放在一个页面上对应热词里经常被搜索的“pyecharts grid 多个”问题实际用Page或Grid都能做多图合并但两者有区别Grid适合有明确坐标位置的多个图表Page则是纵向堆叠适合数据报告类的展示。import pandas as pd from pyecharts.charts import Bar, Line, Pie, WordCloud, Grid, Page from pyecharts import options as opts df pd.read_csv(data/cleaned_reviews.csv, encodingutf-8-sig) # 评分分布数据 star_counts df[star].value_counts().sort_index() bar ( Bar(init_optsopts.InitOpts(width800px, height400px, themelight)) .add_xaxis([f{s}星 for s in star_counts.index]) .add_yaxis(评论数, star_counts.tolist()) .set_global_opts(title_optsopts.TitleOpts(titleTapTap 评分分布)) ) # 情感分布饼图 pie ( Pie() .add(, [list(z) for z in df[sentiment_label].value_counts().items()]) .set_global_opts(title_optsopts.TitleOpts(title情感占比)) .set_series_opts(label_optsopts.LabelOpts(formatter{b}: {d}%)) ) # 按周聚合的情感均分与评论量 df[week] pd.to_datetime(df[ts], units).dt.to_period(W).astype(str) trend df.groupby(week).agg(avg_sentiment(sentiment_score, mean), cnt(content_clean, count)).reset_index() line ( Line() .add_xaxis(trend[week].tolist()) .add_yaxis(每周情感均分, trend[avg_sentiment].round(2).tolist(), is_smoothTrue) .set_global_opts(title_optsopts.TitleOpts(title情感均分周趋势), yaxis_optsopts.AxisOpts(min_-2, max_2)) ) grid ( Grid(init_optsopts.InitOpts(width1200px, height800px)) .add(bar, grid_optsopts.GridOpts(pos_top8%, pos_bottom60%)) .add(line, grid_optsopts.GridOpts(pos_top55%, pos_bottom8%)) ) grid.render(output/overview_grid.html) page Page(layoutPage.DraggablePageLayout) page.add(bar, pie, line) page.render(output/overview_page.html)这段代码里值得关注的是pos_top和pos_bottom这两个参数。Grid里要避免两个图表互相覆盖必须手动分配纵向位置。第一个图占上部pos_bottom60%第二个图占下部pos_top55%它们之间留下的 5% 空隙是坐标轴标签的空间如果填得太满两个图表的横轴文字会叠在一起。此外Page.DraggablePageLayout会生成一个可拖拽调整布局的 HTML你可以把四张图拖到自己想要的位置后保存布局配置更适合交付给运营同学看。5.2 词云必须处理的“噪音词”陷阱词云是文本挖掘项目里最出效果但也最容易翻车的一张图。直接用分词后的全部词语画词云会出现满屏“游戏”“我们”“一个”这种无意义词。wordcloud 库虽然支持停用词参数但 pyecharts 的 WordCloud 类没有内置停用词表需要手动在聚合时过滤。import jieba from collections import Counter stopwords set(open(data/stopwords.txt, encodingutf-8).read().split()) texts df[content_clean].tolist() word_list [w for t in texts for w in jieba.lcut(t) if w.strip()] word_counts Counter(w for w in word_list if w not in stopwords and len(w) 1) # 取前 100 个词给词云使用 top_words word_counts.most_common(100) wc ( WordCloud() .add(, top_words, word_size_range[15, 100], shapediamond) .set_global_opts(title_optsopts.TitleOpts(title高频词词云)) ) wc.render(output/wordcloud.html)需要处理两类词一是在所有游戏评论里都高频出现的通用词如“游戏”、“玩家”、“感觉”可以加入停用词表二是这个项目里特有的干扰词——如果你抓取的页面包含“攻略”这种引导性文本它们也会霸占词频榜单。只看词云的高频词排名不看上下文语境很容易误判这个游戏的真实口碑。这一步建议和第四章的情感词典联动把命中 pos_words 和 neg_words 的高频词分别做两张词云对比正负向的高频表达分布比单张总词云信息量高得多。6. 进阶验证技巧用“星评与情感分歧样本”反向优化情感词典情感分析做完后最该做的一件事不是看总体准确率而是把星级标签和情感打分极端不一致的样本单独拎出来这一部分才是整个项目的增量价值所在。定义分歧样本星级为 4 或 5 但情感分低于 -1 的以及星级为 1 或 2 但情感分高于 1 的。这些样本在 TapTap 里通常有三类反讽式好评、玩梗式差评、错位评价比如表达对某次更新不满但总体给高分。# 找出分歧样本 df[star_group] df[star].apply(lambda x: 高星 if x 4 else (低星 if x 2 else 中星)) df[sentiment_group] df[sentiment_score].apply(lambda x: 正面 if x 1 else (负面 if x -1 else 中性)) divergence df[(df[star_group] 高星) (df[sentiment_group] 负面) | (df[star_group] 低星) (df[sentiment_group] 正面)] print(f分歧样本数: {len(divergence)}占比 {len(divergence)/len(df)*100:.1f}%) divergence[[content_clean, star, sentiment_score]].head(20)拿到分歧样本后做两件事第一人工浏览前 50 条看哪些词频繁出现在“典型反讽”里比如“真好玩啊笑”“良心游戏反话”“快跑”这类把这些词以及它们的上下文一并加入一个sarcasm_words集合在情感打分循环里对命中样本做降权处理。第二统计分歧样本中出现次数最多的情感词如果某个词在分歧样本中多次出现说明词典对该词的极性定义有误应调整它的基础权重比如“保底”在抽卡类游戏里争议极大可以考虑直接从情感词典中移除。如果你想让整个 pipeline 更工程化可以把这个分歧样本筛选做成一个定期运行的脚本每周拉取新评论清洗后自动跑一遍情感打分把新增的分歧样本推到一个 review 列表由分析师或运营确认后回填词典。这样情感词典就不是一次性构建的死数据而是一个随新梗、新活动持续更新的活资产。整个项目的源码和文档说明也应围绕这条链路来组织crawler/、cleaning/、analysis/、visualize/四个模块各司其职其中cleaning/目录里额外放一份stopwords.txt和一个domain_dict.py这两个文件是后续迭代时修改频率最高的地方。最后给两个实战中排在第一优先级的经验如果爬虫跑到一半开始返回空列表先检查是不是接口版本升级导致x-legacy-app-id失效重新抓包替换即可如果清洗后文本大量出现空值优先检查表情符号的 Unicode 范围是否覆盖了你用的 Python 版本——Python 3.7 和 3.11 对部分扩展 Emoji 的正则匹配行为不同这是最容易在依赖环境迁移时踩中的坑。本文还有配套的精品资源点击获取