尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Steam评论情感分析:Python爬虫+hanlp分词+pyecharts可视化

发布时间:2026/9/28 15:53:29

资讯中心
01
ARTICLE

Steam评论情感分析:Python爬虫+hanlp分词+pyecharts可视化

Steam评论情感分析:Python爬虫+hanlp分词+pyecharts可视化
简介基于哈工大语言平台的蒸汽平台评论爬取情感分析可视化源码是一份可直接运行的课程设计与期末大作业方案面向需要完成数据分析类项目的学习者。项目完整覆盖评论采集、文本清洗、情感判断与图表展示等环节从接口请求到数据预处理再到情感模型调用与可视化输出过程一气呵成代码注释详细部署门槛低新手也能轻松读懂并二次改造。压缩包共53个文件、大小约26MB除核心代码外还配有csv数据集、停用词表、需求说明文档、演示文稿及运行截图目录结构清晰易查便于按需查阅与快速定位。已有264人学习浏览这套项目既适用于期末验收与课程设计展示也能帮助学习者理解实际NLP情感分析流程并学会如何将分析结果用图表直观表达。1. 把steam几千条中文评论变成可视化口碑报表hanlp、情感分析与Python爬虫一条链路打通steam游戏页面下面几千条中文评论想判断这游戏是好评如潮还是差评如潮靠人肉翻页能翻到手发麻。用Python写个爬虫把评论拉下来配hanlp做中文分词和情感打分再用pyecharts把结果渲染成词云和趋势图整条链路大概150行源码就能跑通。这个方案适合游戏开发者做口碑监控、游戏自媒体写选题也适合刚把requests和pandas摸熟、想往NLP方向迈一步的Python新人。有个反直觉的点hanlp不是装完叫两声就能告诉你好评差评它负责分词和词性标注情感判定要自己接词典或微调模型。这篇博文会把整套链路按能复现的标准拆开顺手把参数和踩过的坑也写出来。2. 先跑通评论数据管道steam评论区不是没人管但能给你开一扇窗2.1 从steam返回的JSON里把评论正文和voted_up挑出来steam官方其实提供了一个非正式的评论接口地址是store.steampowered.com/appreviews/游戏appid不需要玩家API Key就能直接访问。国内网上很多教程还在教解析HTML页面那是十年前的路子现在直接拿到JSON响应体字段齐全、结构稳定。这个JSON里最核心的部分是reviews数组和最外层的cursor字段。reviews数组里一条评论对应一个字典其中review字段是评论文本recommendationid是评论的唯一IDvoted_up表示玩家是否点击了“推荐”author.playtime_forever是游戏时长单位是分钟。常见做法是先写一个只拉一页的小函数把返回结构看清楚再进入翻页逻辑。我一般会这样写单页请求import requests APP_ID 730 # CS2的appid换成别的游戏在这里改 URL https://store.steampowered.com/appreviews/{}/.format(APP_ID) def fetch_one_page(app_id, cursor*, languageschinese): params { json: 1, language: language, cursor: cursor, day_range: 90, filter: recent, purchase_type: all, } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 } resp requests.get(URL, paramsparams, headersheaders, timeout15) resp.raise_for_status() data resp.json() return data[reviews], data.get(cursor, *)第一行resp.raise_for_status()会把403、429之类的HTTP异常直接抛出来避免你拿回一页错误页面还在强行解析。cursor参数在第一次请求时固定传*表示从最新评论开始取之后每次翻页都要用上一次返回的cursor值替换它。day_range90是只取最近90天的评论filterrecent表示按评论时间从新到旧排序这两个参数直接决定你拿到的数据窗口。languageschinese表示要简体中文评论这个字段对steam中国区玩家非常关键不传的话默认返回英文评论。这个函数返回的每条评论里还有timestamp_created单位是Unix时间戳秒后面画时间趋势图要把这个字段转成日期。字段名别记错写成timestamp会让pandas转日期时认不出列。你可以先打印第一条评论看看结构确认数据真的是中文再往下走。2.2 用requests把任意游戏的评论落成CSV翻页靠cursor不靠页码steam评论接口的翻页是很多爬虫新手的第一道坎它不认page1、page2这种页码参数只认cursor。每次请求返回的JSON里最外层会带一个cursor字段你要把它原封不动地传给下一轮请求——这就是游标翻页机制cursor记录的是评论集当前位置的服务器快照不是页码。如果你在翻页过程中改了filter、language或者day_range里面任何一个参数服务端生成的游标就对不上翻着翻着就会拿到重复数据。我一般会把翻页循环封装成独立函数同时加一个上限保护避免接口异常时无限跑下去import time import csv def crawl_reviews(app_id, max_pages5): cursor * rows [] for page in range(max_pages): reviews, cursor fetch_one_page(app_id, cursorcursor) for r in reviews: rows.append({ review_id: r[recommendationid], review_text: r[review], voted_up: r[voted_up], timestamp: r[timestamp_created], playtime_hours: round(r[author][playtime_forever] / 60, 1), }) print(page {}: got {}, next cursor {}.format(page 1, len(reviews), cursor)) if not reviews: break time.sleep(1.5) with open(steam_reviews.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesrows[0].keys() if rows else []) writer.writeheader() writer.writerows(rows) return rowstime.sleep(1.5)是给steam接口的喘息窗口间隔压到0.1秒连续拉几百页很容易触发限流。encodingutf-8-sig是CSV能被Excel正常打开中文的关键写成纯utf-8的话Excel里全是乱码。max_pages这个上限值在实际使用中最好从命令行参数传进来这样换游戏爬数据时不用改源码。2.3 评论数据落库为什么我选SQLite而不是CSV只跑一次实验CSV够用但后面做情感分析和可视化时要反复读取、去重、合并字段CSV会非常磨人。我习惯从第一步就把数据写进SQLite建表时提前给情感分析结果留好字段这样每次新算出的情感分只用UPDATE写回不用整库重灌。建表和写入用一段很薄的代码就够了import sqlite3 def init_db(db_pathsteam_comments.db): conn sqlite3.connect(db_path) conn.execute( CREATE TABLE IF NOT EXISTS reviews ( review_id TEXT PRIMARY KEY, game_id INTEGER, review_text TEXT, voted_up BOOLEAN, timestamp INTEGER, playtime_hours REAL, sentiment_score REAL, sentiment_label TEXT, created_at TEXT DEFAULT (datetime(now)) ) ) return conn def save_reviews(conn, app_id, rows): conn.executemany( INSERT OR IGNORE INTO reviews (review_id, game_id, review_text, voted_up, timestamp, playtime_hours) VALUES (?, ?, ?, ?, ?, ?), [(r[review_id], app_id, r[review_text], r[voted_up], r[timestamp], r[playtime_hours]) for r in rows] ) conn.commit()INSERT OR IGNORE依赖review_id主键去重第二次爬同一个游戏不会重复插入。这个习惯能救大命——后面调情感分析脚本时要反复迭代每次跑完只需UPDATE新算出的字段不用从头爬一遍。表里的game_id和timestamp字段是为后面按游戏过滤和画时间趋势准备的想爬多个游戏做对比的话一定要留着。第一次跑通管道后你手上应该有几条到几千条不等的评论。先用pandas看一眼DataFrame的行数和review_id的重复率我遇到过因为cursor参数传错导致第一页数据重复到第三页的情况这种脏数据会让后面的情感统计全盘出错。3. hanlp分词接情感打分正向负向不能只靠“好玩”“垃圾”3.1 hanlp装法和模型引入装纯Python版还是Java版hanlp在Python生态里有两代用法。第一代是早期基于Java的版本pip install hanlp会把jpype也带进来启动时要拉起一个JVM还要在环境变量里指定JVM路径很多人第一次跑就死在JVM内存上。现在主流的做法是直接用hanlp 2.x的纯Python版底层是PyTorch装完即用模型文件首次加载后会缓存到本地目录。装好之后先验证一下加载是否正常再决定用哪个模型。我会先跑一个最小的分词模型确认环境没问题再上联合模型import hanlp # 联合模型分词/词性/命名实体/依存句法一次出 HanLP hanlp.load(hanlp.pretrained.mtl.CLOSE_TOK_POS_NER_SRL_DEP_SDP_CON_ELECTRA_SMALL_ZH) doc HanLP(这游戏手感真好就是偶尔闪退) print(doc[tok]) print(doc[pos]) # 输出形如: # [这, 游戏, 手感, 真, 好, , 就是, 偶尔, 闪退] # [DT, NN, NN, RB, VA, PU, CS, AD, VA]CLOSE_TOK_POS_NER_SRL_DEP_SDP_CON_ELECTRA_SMALL_ZH是一组联合模型的名字它把中文分词、词性标注、命名实体、依存句法、语义角色都算出来返回的doc里可以直接取tok和pos。第一次加载会下载几百MB的模型文件网络不通时会卡住解决办法放在第5章。如果你只想拿词性标注来做情感词典匹配也可以单独加载分词和词性模型体积更小、速度更快tok hanlp.load(hanlp.pretrained.tok.COARSE_ELECTRA_SMALL_ZH) pos hanlp.load(hanlp.pretrained.pos.CTB9_POS_ELECTRA_SMALL) tokens tok(这游戏手感真好就是偶尔闪退) tags pos(tokens)单任务模型的优点是加载快、内存占用小缺点是要自己把分词结果传给pos模型代码多几步。联合模型的优点是一行出全部结果但对老电脑不太友好。我自己的习惯是做steam评论这种短文本时直接用联合模型因为口语化的“手感真好”“好是好就是卡”这种句子分词正确率肉眼可见地更高后面情感打分全靠正确的词边界。3.2 用hanlp做分词和词性标注然后打情感分hanlp本身不直接给出一句话是好评还是差评它做的是分词、词性、依存这些基础能力。情感判定这个活常见做法是拿分词结果接一个情感词典打分词典里每个词配一个极性权重最后把整条评论的分值加起来。这样做的好处是逻辑透明跑一次就能知道是哪个词拉的分数效果不够还能回去改词典。我先把打分函数写出来POSITIVE_WORDS {好, 不错, 舒服, 爽, 神, 优秀, 推荐, 喜欢, 值得, 好玩, 惊喜, 良心, 满意} NEGATIVE_WORDS {差, 垃圾, 坑, 烂, 卡, 闪退, 后悔, 无语, 失望, 骗, 糟糕, 影响, 难受, 不值得} NEGATION_WORDS {不, 没, 别, 无, 非} def sentiment_score_from_tokens(tokens): n len(tokens) flipped [False] * n for i, tok in enumerate(tokens): if tok in NEGATION_WORDS: for j in range(i 1, min(i 3, n)): flipped[j] True score 0 for i, tok in enumerate(tokens): w 0 if tok in POSITIVE_WORDS: w 1 elif tok in NEGATIVE_WORDS: w -1 if w 0: continue if flipped[i]: w -w score w return score def analyze_comment(text, hanlp_instance): doc hanlp_instance(text) score sentiment_score_from_tokens(doc[tok]) label positive if score 0 else (negative if score 0 else neutral) return score, label打分逻辑里最关键的是否定词翻转。steam评论里“不推荐”“不行”“没意思”都是典型负面表达但“推荐”“行”“有意思”单独看是正面词如果没有翻转逻辑整条评论会被打成正面。flipped数组的处理方式是碰到否定词把它后面两个位置的词标记为需翻转之后再统一计算这样“不推荐”就会把“推荐”的1变成-1。这里有个必须强调的点hanlp返回的doc对象不是普通Python字典直接取doc[tok]是能用的但别在doc上瞎调其他方法。想整体看结果时用doc.to_dict()。如果你加载的是单任务模型分词结果是普通字符串列表直接喂给打分函数同样没问题。3.3 情感词典的扩充和否定词处理汉语句子里的“不推荐”千万别看反steam评论里的高频词和新闻语料差很多拿CS2评论跑一遍top词除了“游戏”“手感”“优化”“挂”“队友”还有不少电竞圈黑话。这些词得单独加进词典里否则情感分析会大面积变成中性。我一般先用一段代码把高频词拉出来人工过一遍再扩充词典from collections import Counter def print_top_words(df, top_n60): counter Counter() for text in df[review_text].dropna(): tokens HanLP(text)[tok] counter.update(t for t in tokens if len(t) 1) for word, freq in counter.most_common(top_n): print(word, freq)跑完这一步把明显带褒贬的词摘出来写进lexicon.txt每行一个词加一个极性标签用load函数读进来这样词典可以脱离代码维护def load_lexicon(pathlexicon.txt): pos_words, neg_words set(), set() with open(path, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) 2: continue word, polarity parts[0], parts[1].lower() if polarity in {pos, 1, 正面}: pos_words.add(word) elif polarity in {neg, -1, 负面}: neg_words.add(word) return pos_words, neg_words为什么不用现成的通用情感词典大连理工情感本体库确实全但它是通用领域语料“手感”“优化”“闪退”这些steam评论里的关键词一概不覆盖。把领域词加进去情感分析的准确率会明显提升。这不是黑匣子模型能给你的解释性别嫌这一步土它最见功夫。否定词的翻转范围也要单独调翻转窗口太大会误伤无辜。比如“不贵而且好玩”“好玩”显然还是正面但翻转窗口设为3时“而且”后面那个“好玩”也被翻转整句就反了。一般我设2到3配合实际例子去试跑几条评论看结果对不对再固定下来。4. 可视化词云、情感分布、时间折线一张大屏看完4.1 把情感分聚合成正/负/中性并画饼图情感分析跑完SQLite里的sentiment_score和sentiment_label已经写好了接下来直接查库出结果。可视化我推荐pyecharts因为它用几行代码就能拼出一个可视化大屏风格的HTML页面浏览器直接打开不用起Flask服务这对快速分析steam评论这种一次性任务非常合适。先看最基本的聚合饼图import sqlite3 import pandas as pd from pyecharts.charts import Pie from pyecharts import options as opts conn sqlite3.connect(steam_comments.db) df pd.read_sql(SELECT * FROM reviews WHERE game_id730, conn) label_counts df[sentiment_label].value_counts() pie ( Pie() .add( series_name评论占比, data_pair[list(x) for x in label_counts.items()], radius[35%, 60%], label_optsopts.LabelOpts(formatter{b}: {c} ({d}%)), ) .set_global_opts(title_optsopts.TitleOpts(titlesteam评论情感分布)) ) pie.render(sentiment_pie.html)label_opts里的{d}%是pyecharts自带的百分号格式器不用自己算占比。radius传两个值是把普通饼图改成环形视觉上比实心饼图干净适合贴进分析报告。set_global_opts里还可以加legend_opts把图例放到右下角配色用默认的series_palette就行。4.2 词云图卖出安利和火葬场的词一眼抓出来词云是steam评论分析里最有冲击力的输出它把玩家反复提的“手感”“闪退”“优化”“匹配”这些词用字号大小展示出来比表格直观太多。词频统计要建立在分词结果上不能直接按空格分开因为中文评论里没有空格。from collections import Counter from pyecharts.charts import WordCloud STOP_WORDS {游戏, 这个, 真的, 可以, 还是, 就是, 觉得, 一个, 什么, 没有, 不过} def build_word_freq(df, top_n80): counter Counter() for text in df[review_text].dropna(): tokens HanLP(text)[tok] counter.update(t for t in tokens if len(t) 1 and t not in STOP_WORDS) return counter.most_common(top_n) freq build_word_freq(df) wc ( WordCloud() .add( , freq, word_size_range[12, 80], shapecircle, font_familyMicrosoft YaHei, ) .set_global_opts(title_optsopts.TitleOpts(title评论高频词)) ) wc.render(comment_wordcloud.html)停用词这一步不能省否则词云里全是“游戏”“这个”这种没有信息量的词。word_size_range控制字号下限和上限词频越高的词字号越大。font_family参数务必带上pyecharts的WordCloud组件默认字体大概率不支持中文不带这个参数渲染出来全是方块这个问题在Windows和Linux上都遇到过。4.3 时间维度上的情感变化评论口碑不是一条直线把每条评论的timestamp转成日期按天聚合并算平均情感分就能看到这个游戏的口碑走势。这个图对游戏开发者极有价值某次版本更新导致情感分断崖下跌从视觉上会非常扎眼。df[date] pd.to_datetime(df[timestamp], units).dt.date daily df.groupby(date).agg(avg_score(sentiment_score, mean), count(review_id, count)) daily daily[daily[count] 3] # 评论太少的天平均分没有意义 from pyecharts.charts import Line line ( Line() .add_xaxis([str(d) for d in daily.index]) .add_yaxis( 平均情感分, [round(v, 2) for v in daily[avg_score]], is_smoothTrue, markpoint_optsopts.MarkPointOpts( data[opts.MarkPointItem(type_min), opts.MarkPointItem(type_max)] ), ) .set_global_opts( title_optsopts.TitleOpts(title每日平均情感分变化), yaxis_optsopts.AxisOpts(min_-1, max_1), ) ) line.render(sentiment_trend.html)按天聚合时滤掉样本数小于3的天因为当天只有一两条评论时平均数会被极端值带飞画出来全是一根根刺看不出趋势。is_smooth打开后曲线会做平滑处理视觉上更接近“口碑走势”而不是散点连线。y轴范围固定-1到1是配合打分体系的如果词典权重让分数超出这个范围记得改成数据驱动的min和max否则曲线会顶到边界。到这里三张图都渲染成独立HTML最后拼成一个可视化大屏页面from pyecharts.charts import Page page Page(layoutPage.SimplePageLayout) page.add(pie, wc, line) page.render(steam_dashboard.html)Page.SimplePageLayout会把三张图按流式布局拼到一页里浏览器滚动就能看完所有图表这就是常说的一页看板。如果报告里要塞PDF也可以分别render出图片用matplotlib拼但我个人觉得直接给HTML链接更方便。5. steam评论爬虫避坑指南hanlp加载翻页编码一团乱麻逐个捋5.1 hanlp模型加载失败别在import之后才求救现象hanlp.load执行后一直卡住不动或者长时间下载后抛OSError提示找不到模型文件。原因hanlp 2.x的模型默认从远端下载到~/.hanlp目录国内网络环境下载几百MB的文件经常断在半路留下一个损坏的缓存目录下次加载就直接失败。解决先确认~/.hanlp目录的状态。如果里面有文件但加载报错删掉整个~/.hanlp后重新执行load如果网络实在不行找一个能下载的时段提前把模型拉到本地然后加载时写本地路径import os os.environ[HANLP_HOME] /data/hanlp_models HanLP hanlp.load(/data/hanlp_models/close_tok_pos_ner_srl_dep_sdp_con_electra_small_zh)HANLP_HOME指向的目录要和模型实际解压出来的结构一致别再套一层用不到的子目录。另外老教程里大量出现的from hanlp import HanLP在2.x里已经不建议用直接import hanlp然后hanlp.load()即可。看到jpype报错的用户基本都是落进旧版本坑了。5.2 爬虫翻页死循环cursor是快照不是页码现象爬了好几百页一看数据和第一页一模一样或者cursor始终不变。原因steam的cursor是基于服务端生成的评论快照的如果你在翻页过程中改了filter、day_range、language或者两次请求间隔太长导致快照过期游标就会失效。接口要么返回重复数据要么停在原地。解决翻页循环里除了cursor其他参数一个都不许变。具体代码里加一个安全阀连续3页没有新增记录就停止def crawl_reviews_safe(app_id, max_pages5): cursor * seen_ids set() stable_pages 0 for page in range(max_pages): reviews, cursor fetch_one_page(app_id, cursorcursor) new_count sum(1 for r in reviews if r[recommendationid] not in seen_ids) if new_count 0: stable_pages 1 if stable_pages 3: print(连续3页无新增停止) break else: stable_pages 0 for r in reviews: seen_ids.add(r[recommendationid]) time.sleep(1.5) return list(seen_ids)用recommendationid去重是最稳的判断方式比依赖cursor是否变化可靠得多。连续3页没有新评论基本可以确定已经翻到底了再跑下去只是浪费时间。5.3 情感分析结果全是中性词典覆盖率和分词粗细问题现象跑了200条评论正负加起来不到10条剩下全是neutral。原因情感词典太小steam评论的核心词一个没覆盖或者hanlp分词把“不推荐”切成“不/推荐”后翻转逻辑没有正确触发。解决先把评论里的高频词拉出来手动过一遍。具体方法是跑print_top_words打印top 50到100把“手感”“优化”“闪退”“掉线”“挂”“队友”这些词按褒贬加入词典再重跑分析。更关键的是不要把neutral当成失败它本来就是未知棋盘的占位词典补到位之后neutral比例自然会降下来。5.4 中文乱码utf-8-sig能救CSV但救不了你的终端现象CSV用Excel打开全是乱码IDLE或终端里print中文也乱。原因CSV用utf-8写入但Excel的默认读取方式是ANSI中文全乱Windows终端的代码页是GBK读到utf-8的中文自然也是乱码。解决CSV写文件时encoding参数用utf-8-sig而不是utf-8这是带BOM的UTF-8Excel识别得很好。终端乱码时在Windows下先执行chcp 65001把代码页切到UTF-8或者干脆把所有print语句里的中文改成英文标点和ASCII内容。我自己的代码里print输出一律用英文避免在别人机器上跑脚本时屏幕上一片问号这个问题排查起来特别玄学。5.5 pyecharts图表中文方块字体没配好现象词云图上中文全是方框但饼图的标题却是正常的。原因WordCloud组件的渲染需要加载字体默认字体对中文支持不全浏览器找不到能显示中文的字形就渲染成方块。解决在WordCloud的add参数里显式指定font_familyWindows上一般用Microsoft YaHeiLinux服务器上可以用Noto Sans CJK SC。如果还不行检查pyecharts版本部分旧版WordCloud对font_family支持有问题升级到较新的pyecharts版本即可。饼图和折线图的字体走的是ECharts默认字体大多没问题只有WordCloud最容易翻车。6. 让口碑报表每天自己生成定时任务增量更新One Page看板这一步把前面所有代码拧成一条流水线挂到定时任务里第二天起床就能看到新鲜的口碑报表。增量更新的技巧在于不用再爬全量数据把day_range改成1只拉过去24小时的评论入库时INSERT OR IGNORE去重情感分析只对sentiment_label为空的行执行。这样每天新增几百条评论处理耗时控制在两分钟以内对steam接口的压力也小。我用APSchedule做定时调度完整结构大致是这个样子from apscheduler.schedulers.blocking import BlockingScheduler from datetime import datetime def refresh_report(): conn init_db() rows crawl_reviews(APP_ID, max_pages3, day_range1) save_reviews(conn, APP_ID, rows) df pd.read_sql(SELECT * FROM reviews WHERE game_id? AND sentiment_label IS NULL, conn, params(APP_ID,)) for idx, row in df.iterrows(): score, label analyze_comment(row[review_text], HanLP) conn.execute(UPDATE reviews SET sentiment_score?, sentiment_label? WHERE review_id?, (score, label, row[review_id])) conn.commit() render_dashboard(conn, APP_ID) conn.close() print(report refreshed at, datetime.now()) sched BlockingScheduler() sched.add_job(refresh_report, interval, hours24) sched.start()验证这一步别偷懒。我习惯从全量数据里随机抽100条评论按自己的判断标成正/负/中再用sklearn的classification_report对比脚本结果看看hanlp加词典这组方案到底什么准确率新增词会不会误伤别的句子。跑上一周之后你会觉得真正的价值不在“能爬”而在能解释口碑为什么变化。比如某天情感分断崖下跌点开那天的评论看到“更新后闪退”出现的频率答案就摆在面前。这个方案的缺点也摆在这里词典维护是持续投入每次游戏版本更新都可能冒出新的社区黑话得定期回去看高频词如果哪天你想按“哪类负面问题最多”做细粒度分析就得把词典升级成分类模型用hanlp的文本分类微调能力再往前迈一步。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。