
简介面向Python课程设计与期末大作业场景这套基于hanlp的Steam评论情感分析可视化项目完整实现了评论爬取、文本清洗、情感极性判断与可视化展示的闭环。项目基于Steam评论接口抓取真实数据配合hanlp自然语言处理能力进行情感倾向分析最终以图表形式直观呈现结果代码注释详细、模块划分清晰新手也能快速读懂并部署运行。资源包共53个文件包含多个Python源码、CSV/TXT数据文件、爬虫运行效果截图、数据挖掘分析报告Word文档以及答辩PPT和配置文件等整体压缩包大小25.8MB目录结构便于按功能模块检索使用。目前已有264人学习下载。项目不仅能直接作为高分的课程设计或期末大作业还可更换游戏评论数据并扩展分析对象在数据采集、文本处理和可视化展示方面具备良好的参考与复用价值。1. 它不是泛泛的项目模板而是一套能把期末大作业直接交差的 Steam 评论情感分析流水线这套基于 HanLP 的 Steam 评论爬取、情感分析与可视化 Python 源码是热门课设题目里少有的完整包。它拿《赛博朋克 2077》当样本爬 Steam 官方评论接口拿数据再用 HanLP 做中文分词和情感打分最后输出 HTML 图表、CSV 结果和一份能直接改的答辩报告。对正在找期末大作业的人来说这份资源的意义不只是省事而是数据采集、清洗、建模、可视化每个环节都有对应文件新手按 requirements.txt 装完依赖就能进入复现。文章后面我会把文件分工、接口参数、HanLP 的两种分析方案和踩过的坑都拆开讲一遍。2. 项目结构与数据主链路从 review.json 到 2077.csv每个文件都有明确分工2.1 文件盘点脚本、数据、产物三类各管什么拿到压缩包之后别急着跑代码先把根目录过一遍。2077CommentAnalyze-master 里的文件可以分成三类第一类是脚本也就是 main.py、sentiment_analysis.py、sentiment_analysis2.py第二类是数据包括 review.json、2077origin.csv、2077.csv、reviews_over.txt第三类是辅助文件dict.txt、stopwords.txt、html 目录、材料目录、PPT 目录。表里面的对应关系是这样的文件角色实际用途main.py爬虫入口请求 Steam 评论接口把回包落成 JSON 和原始 CSVsentiment_analysis.py情感分析主脚本基于 HanLP 分词加 dict.txt 词典打分产出 2077.csvsentiment_analysis2.py第二版分析脚本用预训练分类器做对比实验结果列稍有不同review.json原始缓存Steam 接口返回的评论数组没有这只剩爬虫可以重新生成2077origin.csv清洗前数据从 JSON 转出来的规整表字段包括作者、语言、是否好评2077.csv分析结果每一条评论带 sentiment_score 和情感标签dict.txt情感词典每行一个情感词和对应的权重分数stopwords.txt停用词表过滤“的、了、我”这类无意义词html可视化输出pyecharts 渲染出来的图表页直接浏览器打开材料、PPT答辩素材docx 分析报告和演示文稿可替换数据复用一眼看下来这个项目的设计思路是“缓存优先”爬虫把原始回复先存成 review.json后面所有处理都从这个文件开始这样就算接口反爬导致断点重跑也不需要重新把全部评论拉一遍。2.2 主链路四个模块连成一条可替换的流水线整个项目的数据流是一条单向链路爬虫先把评论写进 review.json数据转换脚本把 JSON 摊平成 2077origin.csv情感分析脚本读 CSV逐条跑 HanLP 分词和词典匹配把分数写进 2077.csv可视化脚本再读结果 CSV生成 html 目录下的图表。这里要注意 sentiment_analysis.py 和 sentiment_analysis2.py 的关系。前者是词典打分法也就是把 dict.txt 里的词和权重累加后者走的是 HanLP 预训练情感分类器。两版输出的列不完全一样但最终落点都是 2077.csv。课程设计里两种方法都跑一遍反而能形成“词典基线模型对比”的分析结构答辩时更有说头。如果 review.json 里不是数组而是接口原包转 CSV 的代码要按实际结构取值。常见做法是这样import json import csv with open(review.json, r, encodingutf-8) as f: data json.load(f) # 接口回包有时套了一层 query_summary取不到 reviews 就换个取法 reviews data if isinstance(data, list) else data.get(reviews, []) with open(2077origin.csv, w, encodingutf-8-sig, newline) as f: writer csv.writer(f) writer.writerow([recommendationid, author, language, voted_up, timestamp_created, content]) for r in reviews: writer.writerow([ r.get(recommendationid), r.get(author, {}).get(steamid), r.get(language), r.get(voted_up), r.get(timestamp_created), r.get(review, ).replace(\n, ) ])这里三个细节容易踩坑第一CSV 文件编码一定用 utf-8-sigExcel 直接打开才不会把中文读成乱码第二author 字段是嵌套结构要走.get(author, {}).get(steamid)写作二级键时不能直接 r.get 到底第三评论里的换行符要替换成空格否则 CSV 里一行记录会被拆成多行。文件里 2077origin.csv 就是从原始评论转出来的标准表后面情感分析只依赖该表不会再回头翻 JSON。review.json 转 CSV 成功后检查一下行数Steam 对应《赛博朋克 2077》的接口响应里如果num_per_page100一趟跑下来能拿到几百条到几千条评论数量大会被接口限流所以爬虫部分的节奏控制决定了这个转换脚本能拿到多少原料。3. Steam 评论接口爬取游标、语言、限流三处最容易翻车3.1 接口地址与核心参数Steam 的评论数据不是从 HTML 页面里硬解析出来的而是走官方评论接口这也是这个项目能稳定爬到大量评论的原因。接口地址格式是https://store.steampowered.com/appreviews/1091500其中 1091500 是《赛博朋克 2077》的 appid如果换游戏去商店地址栏里找对应的数字即可。关键参数如下参数含义建议值json返回 JSON 而非 HTML1cursor分页游标第一页传 *每页回包里的 cursor 覆盖num_per_page每页条数100接口上限language评论语言all 或 schinesepurchase_type是否只取购买用户的评论allday_range时间范围不限制就传 9223372036854775807全部评论language 这个参数尤其要说一下。all会把英文、俄文、中文混杂在一起HanLP 对中文分词很利索对英文只能按空格硬切所以情感词典里如果没有英文词英文评论得分基本都是 0。如果只想拿中文评论把languageschinese填进去爬下来的数据会更贴合中文情感分析流程。3.2 带游标翻页和断点续传的爬虫写法这个项目里的 main.py 核心思路是先给接口发送请求拿到响应后把 cursor 取出来供下一页使用。我一般会这样写爬虫循环import json import time import requests APPID 1091500 URL fhttps://store.steampowered.com/appreviews/{APPID} HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 } def fetch_reviews(cursor*, num_per_page100): params { json: 1, cursor: cursor, num_per_page: num_per_page, language: all, purchase_type: all, day_range: 9223372036854775807, } resp requests.get(URL, paramsparams, headersHEADERS, timeout15) resp.raise_for_status() return resp.json() all_reviews [] cursor * total 0 while True: payload fetch_reviews(cursor) if payload.get(success) ! 1: break total payload.get(query_summary, {}).get(total_reviews, 0) reviews payload.get(reviews, []) if not reviews: break all_reviews.extend(reviews) cursor payload.get(cursor, ) # 抓到总数或者游标不动就结束防止死循环 if len(all_reviews) total or not cursor: break time.sleep(2) with open(review.json, w, encodingutf-8) as f: json.dump(all_reviews, f, ensure_asciiFalse, indent2) print(collected, len(all_reviews))这段代码里值得注意的参数有三个。第一个是 cursor它由服务端返回不能自己随便乱造中途停止后要拿上次的游标续传。第二个是 timeout15Steam 接口响应不稳定没有超时控制会让爬虫挂死。第三个是 sleep(2)两秒间隔看起来保守实际上对短平快的课程设计更友好跑一页分析一眼再继续觉得太慢就把 sleep 降到 1但别直接删掉不然容易被临时限流。评论数据结构里还有两个能直接复用的字段voted_up代表该条评论是否推荐这个命中可以作为情感分析的对照标签timestamp_created是 Unix 时间戳如果后面想按月份拆分评论趋势这个字段可以直接用 datetime 转换。3.3 拿不到数据时先看日志再决定重试策略项目里带了日志.zip说明作者在开发时是把运行过程记录下来的。如果跑 main.py 没有任何数据先不要改爬虫逻辑而是调日志看是哪个环节拦住了。常见情况是第一次请求就失败这时单独打印一次响应内容确认返回的是 JSON 还是被重定向到登录页。如果单次请求成功但翻到第二页就报错大概率是频率问题。我的做法是设置一个简单的失败重试重试三次且每次间隔递增def get_with_retry(params, retries3): for i in range(retries): try: resp requests.get(URL, paramsparams, headersHEADERS, timeout15) if resp.status_code 200: return resp.json() except requests.RequestException as e: print(fattempt {i 1} failed: {e}) time.sleep(2 i * 2) return None这里的 retries 和递增 sleep 是为了应对接口偶发抖动不能写成无限重试否则程序会卡在一个死循环里。拿到 review.json 之后后续步骤全部离线跑爬虫只需要跑一次即可。4. HanLP 情感分析实操词典打分和预训练模型两条线都跑通4.1 依赖安装与模型加载项目采用 HanLP 作为中文处理核心requirements.txt 里锁定的几个库至少包含 hanlp、requests、pandas、pyecharts。安装依赖用常规方式pip install -r requirements.txtHanLP 的模型加载很有特点它跟 jieba 不一样不是一装就能用而是首次调用hanlp.load()时把模型文件拉到本地缓存。项目脚本常用的分词模型是PKU_NAME_MERGED_SIX_MONTHS_CONVSEG加载方式如下import hanlp tokenizer hanlp.load(PKU_NAME_MERGED_SIX_MONTHS_CONVSEG) print(tokenizer(这游戏优化太差了玩了十分钟就崩溃))本地没有模型时这条语句会显示下载进度耐心等它跑完即可。模型加载成功后分词器把中文句子切成词语列表后面进行情感词匹配的前提是这些词语能被正确识别出来。4.2 dict.txt 加 stopwords.txt 的词典打分法sentiment_analysis.py 采用词典累加思路。dict.txt 里每行是一个情感词和权重分数stopwords.txt 是无意义词表。完整流程分两步先读词典和停用词再对每条评论分词、过滤、打分。def load_sentiment_dict(path): sentiment {} with open(path, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue parts line.split() if len(parts) 2: sentiment[parts[0]] float(parts[1]) return sentiment def load_stopwords(path): stopwords set() with open(path, r, encodingutf-8) as f: for line in f: stopwords.add(line.strip()) return stopwords字典文件里词的权重有正有负比如“垃圾”可能是负数而“好玩”是正数。如果 dict.txt 实际使用的是逗号分隔而不是空格把 split 的参数改成逗号即可整体逻辑不变。接下来是打分函数这个函数的逻辑是每条评论的最终倾向由分词后所有命中词的权重累加决定def review_score(text, tokenizer, sentiment, stopwords): tokens tokenizer(text) tokens [t for t in tokens if t not in stopwords and len(t) 1] score 0.0 for token in tokens: if token in sentiment: score sentiment[token] return score停用词过滤为什么要保留长度大于 1 的词汇原因很好理解中文里单个字的情感判断经常不可靠而且这个项目的数据里混着部分英文单词单字母过滤掉能减少无匹配的噪音。dict.txt 里如果已经有相关词单个字的词条也会被分数覆盖但一般情况下多字词更稳定。把上面函数套到 2077origin.csv 的 content 列上逐行计算再按分数打标签def score_to_label(score): if score 0.5: return positive if score -0.5: return negative return neutral这里直接按 score 是否大于零判断会有一个问题很多评论是中性表达比如“游戏流程大约 30 小时”既不算正面也不算负面。我把阈值拉宽到 0.5project 里的默认逻辑可能用 0 做边界但课程设计里保留 neutral 类会让图表更接近真实分布。4.3 第二版HanLP 预训练分类器如果只跑词典打分答辩时很容易被老师问“这个字典覆盖不到的网络词怎么办”。sentiment_analysis2.py 存在的意义就是提供一个能对比的模型方案。HanLP 预训练分类器的加载方式是import hanlp classifier hanlp.load(hanlp.pretrained.classifiers.SENTIMENT_CTL_ELECTRA_SMALL) result classifier(游戏剧情很精彩但 bug 真的多) print(result)这个分类器输出通常是概率分布可以直接看哪一类得分高也可以把结果映射成“正面/负面”。它的优点是不需要人工维护词典缺点是对超长评论会截断所以长评精度不一定比词典法好。在实际课程设计报告里最合理的做法是两条线都跑然后对比两种方法在同样数据上的标签一致性。如果一致性低于百分之七八十你的分析结论里可以写“因为词典法覆盖了预训练模型忽略的长文本语义片段”。4.4 结果合并输出到 2077.csv情感分析的输出文件是 2077.csv它与 2077origin.csv 一一对应只是多出两列。输出代码的逻辑大致如下import csv with open(2077origin.csv, r, encodingutf-8-sig) as f: reader list(csv.DictReader(f)) with open(2077.csv, w, encodingutf-8-sig, newline) as f: writer csv.writer(f) writer.writerow([content, voted_up, sentiment_score, sentiment_label]) for row in reader: text row[content] score review_score(text, tokenizer, sentiment, stopwords) label score_to_label(score) writer.writerow([text, row[voted_up], score, label])这一步输出的 sentiment_label 就是后面可视化要用的核心字段。如果爬到的数据量大逐条调用分词器会比较慢可以先建一个 dict 做评论文本去重再去掉重复的计算能省下不少时间。5. 常见问题与排查三次翻车记录从模型缺失到全零得分5.1 现象一HanLP 加载时报没有模型文件现象第一次运行 sentiment_analysis.py终端里出现类似 cannot find model 或 no such file or directory 的报错有时候还会卡在下载进度条一动不动。原因HanLP 2.x 的模型名和 1.x 不一样项目脚本里写的模型名如果和当前安装的 hanlp 版本不匹配就会触发找不到模型。另外模型下载实际是异步的网络波动会导致模型文件写入一半。不要理解为“代码有问题”这在 HanLP 用户里也是高频翻车点。解决先执行 pip show hanlp 确认版本再到 HanLP 的预训练模型列表里找到当前版本支持的模型名替换脚本中的字符串。如果模型文件是断点损坏的删掉本地缓存目录后重新加载等日志里出现模型就绪提示再继续。本人写这个项目时就换过一次模型名之后固定用 PKU_NAME_MERGED_SIX_MONTHS_CONVSEG 的情况下没有再出过问题。5.2 现象二Steam 接口返回 success 0 或抓不到评论现象main.py 跑完 debug 日志review.json 里是空列表或者打印响应内容时看到 {success: 0}。原因success 为 0 多数是请求被拒绝或参数错误。尤其频繁请求不加间隔时接口会返回空结果而不是报错。另一个常见诱因是 appid 填错比如把游戏商店地址里的数字当成别的字段做了拼接。解决把 Headers 里的 User-Agent 改成浏览器风格加上 Accept-Language 字段每次请求之间 sleep 2 秒单独打印一遍响应结果确认 total_reviews 大于零再跑整个循环。只要你确认 appid 是 1091500 且语言参数合法success 通常能正常返回。5.3 现象三情感分析得分全部为零现象2077.csv 里 sentiment_score 列所有值都是 0正负标签分布严重失衡。原因要么 dict.txt 加载逻辑没有读到内容要么词典里的词根本切不出来。比如“不好”在分词结果里被切成“不好”和“不”“好”而词典里没有“不好”这一条得分就会归零。另一个隐蔽原因是停用词列表误删了情感词把“喜欢”放到了 stopwords.txt 里会导致很多积极表达被直接滤掉。解决在情感分析函数里打印前 20 条评论的分词结果肉眼确认高频词是否被正确切分再打印 dict.txt 命中的条目数确认权重确实被读入。如果切词粒度不对最直接的办法是把该复合词及其权重追加进 dict.txt。我还习惯跑一条样本评论输出中间过程比如 “这游戏真好玩” 分词后只剩“好玩”命中分数就变成正数这就说明链路通了。6. 进阶玩法自定义情感阈值与人工抽样复核换游戏评估时也不会全盘翻车6.1 调整 dict.txt 权重和阈值让结果贴合目标游戏这个项目默认的 dict.txt 是针对《赛博朋克 2077》场景打磨过的里面有很多游戏相关词条。如果你把它改成另一个游戏的课程设计首先要做的就是扩充词典。我的习惯是保留原词典不动新增一个 custom_dict.txt两个文件在加载时合并def merge_dict(base_path, extra_path): merged load_sentiment_dict(base_path) extra load_sentiment_dict(extra_path) merged.update(extra) return merged这里用 update 而不是逐行追加是因为merged.update(extra)可以直接覆盖同名词条优先使用自定义文件里的权重。对某个游戏的特定词语比如“抽卡”“武器手感”“平衡性差”放进 custom_dict.txt 就能让打分结果更贴近该游戏玩家舆论的实际倾向。阈值调整同样重要。原脚本默认可能用 0 分作为正负边界但评分分布是右偏的大量评论锁定在 0 分附近。我建议把标签划分改成def adaptive_label(score, pos_threshold0.5, neg_threshold-0.5): if score pos_threshold: return positive if score neg_threshold: return negative return neutralpos_threshold 和 neg_threshold 是根据实际数据分布定的不要先写死。跑一遍全量数据把分数分布拉出来看正负样本量再回填阈值这样得出的结论更有说服力答辩时也能回答“为什么这么多中性评论”的问题。6.2 人工抽样复核让图表结论可信可视化图哪怕画得再好看也逃不开“结果是否符合直觉”的拷问。我完成 2077.csv 后都会加一个抽样审核步骤随机抽 15 条 negative 和 10 条 positive把评论原文打出来人工读一遍import random import pandas as pd df pd.read_csv(2077.csv) sample pd.concat([ df[df.sentiment_label negative].sample(15, random_state42), df[df.sentiment_label positive].sample(10, random_state42) ]) for _, row in sample.iterrows(): print(f{row.sentiment_label} | {row.sentiment_score:.2f} | {row.content[:60]})人工读一遍后你会发现词典漏掉了不少反讽表达比如“优化得太好了”这种句子词典打分很可能偏正但实际意思完全相反。这时在 dict.txt 里补权重并非长久之计因为反讽句没有固定词汇。更好的做法是在最终报告里加一个 limitation 段落写明“基于词典的方案无法处理反讽后续可引入预训练模型做对抗验证”这句话反而比硬调词典更显专业。从那以后我每次换数据集做情感分析都会先跑一遍这个抽样复核再决定展示哪个版。这个习惯帮我挡了不少次数据翻车希望这套资源和方法也能帮到你。本文还有配套的精品资源点击获取