基于HanLP的Steam评论爬取与情感分析可视化Python实战
简介面向Python课程设计与期末大作业的完整源码包基于hanlp实现Steam评论爬取、情感分析与可视化。项目以《赛博朋克2077》等实际游戏评论为示例数据代码注释详尽目录结构清晰从评论接口采集、文本预处理到情感判别与图表展示均有实现新手也能按依赖清单快速部署运行支撑课程汇报与高分答辩。压缩包内共53个文件涵盖Python主程序及辅助脚本、爬取得到的JSON/CSV评论数据、情感分析词典与停用词表、可视化图表HTML/PNG以及答辩PPT、数据挖掘分析报告docx和README说明类型覆盖开发、数据、文档与演示便于对照学习与二次开发。已有264人学习下载口碑可见。借助这套资源读者可完整掌握爬虫设计、HanLP情感分析、可视化呈现全流程并可直接修改参数应用于其他游戏或商品评论场景是课程设计、期末大作业的高性价比选择。1. 基于hanlp的steam评论爬取情感分析可视化Python源码一套能直接套用的口碑分析流水线想评估一款游戏在Steam上的真实口碑与其翻几百页玩家评论不如把这套“爬取—清洗—情感分析—可视化”串成一条流水线。基于hanlp的steam评论爬取情感分析可视化Python源码解决的就是这个需求用公开接口把评论拉下来交给hanlp做中文情感判断最后落到图表上告诉你好评率是多少、负面情绪集中在哪些关键词上。适合三类人拿它做课程设计的学生、想盯自家游戏舆论的独立开发者、以及需要定期输出竞品口碑报告的运营。先说一个反直觉的结论整套流程里最耗时的不是情感分析hanlp那个环节反而最稳定真正让你翻车的是爬虫的接口规则和清洗时的脏数据。2. Steam评论爬取先摸透AppReviews接口的边界再写第一行requests2.1 为什么优先用官方接口而不是直接爬网页很多人一上来就对着Steam商店页面写BeautifulSoup这是个能跑但很脆的方案。Steam的页面结构改版频繁而且对非浏览器的请求有Cloudflare拦截你今天写好的CSS选择器下周可能就整体失效。这个项目的正确起点是官方公开接口https://store.steampowered.com/appreviews/{appid}返回的是结构化JSON不需要Steam Web API Key也不需要登录态。这个接口天然就是为“拿评论”设计的省去了解析HTML的脆弱环节。另一个优点是它内置了分页游标机制。接口用cursor字段做翻页而不是传统的页码偏移这意味着即使评论总量在爬取过程中增长也不会出现“插入新评论导致某一页内容重复”的经典问题。我一般会把首次请求当作“取游标取第一批数据”的组合动作后续请求只靠游标前进。2.2 一个可复用的最小爬虫脚本import requests import time import json def fetch_steam_reviews(appid, max_pages20, sleep_sec1.5): 爬取指定游戏的Steam评论公开接口无需API Key 参数说明: appid: Steam应用ID例如《赛博朋克2077》是1091500 max_pages: 最大翻页数每页最多100条 sleep_sec: 两次请求之间的间隔单位秒建议不低于1 base_url https://store.steampowered.com/appreviews/{}.format(appid) params { json: 1, language: schinese, # 只爬简体中文评论减小后续hanlp处理压力 num_per_page: 100, # 官方上限是100 filter: all, # all好评差评都要recent只看最近 } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } cursor * # 首次请求必须用 * 作为起始游标 reviews [] for page in range(max_pages): params[cursor] cursor try: resp requests.get(base_url, paramsparams, headersheaders, timeout15) if resp.status_code 429: print(第{}页触发限流等待30秒后重试.format(page)) time.sleep(30) continue data resp.json() except Exception as e: print(请求异常: {}.format(e)) time.sleep(5) continue batch data.get(reviews, []) if not batch: break # 没有新数据说明已经翻到底 total data.get(query_summary, {}).get(total_reviews, 0) for item in batch: reviews.append({ review_id: item.get(recommendationid), vote: item.get(votes_up), rating: item.get(voted_up), # True好评False差评 content: item.get(review), timestamp: item.get(timestamp_created), playtime: item.get(author, {}).get(playtime_forever, 0) }) cursor data.get(cursor, ) print(已爬取{}条总计{}条游标: {}.format(len(batch), total, cursor[:20])) time.sleep(sleep_sec) return reviews if __name__ __main__: data fetch_steam_reviews(appid1091500, max_pages5) with open(reviews.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2)这段代码有几个值得注意的参数选择。languageschinese不是必填项但对于“情感分析”这个目标来说混入多语言会让hanlp的SKEP模型输出不稳定所以一开始就锁死中文更省事。filterall必须显式声明否则部分应用默认只返回好评后续情感分析就会得到一个一边倒的失真样本。num_per_page官方上限就是100填200不会报错但结果会被截断属于那种“不报错但有毒”的参数。关于限流这个接口对家用带宽的容忍度还不错我以1.5秒间隔单线程跑20页约2000条评论基本不会触发429。但云服务器出口IP的待遇明显更严格如果你在服务器上跑建议把sleep_sec调到3秒以上并且加一个“429后退避重试”的逻辑上面的代码里已经写了基础版本。拿到JSON之后先别急着进情感分析。看一下timestamp_created这是Unix秒级时间戳votes_up是这条评论获得的“有价值”投票数后续做关键词分析时可以按这个字段加权把“沉默的大多数”和“高赞声音”区分开。第二步清洗通常是去掉超短评论比如只包含“”以及合并重复的review_id这些我会放到第4章的清洗环节一起说。3. hanlp情感分析SKEP模型如何把“这游戏我肝了200小时”判成正面3.1 选hanlp而不是直接跑一个大模型评论数据到手后接下来是情感判断。常见的做法有很多直接调用大模型API、微调一个BERT、或者用情感词典。这个项目锁定hanlp是因为它在“中文口语化文本”上的适配度做得比较细而且不需要你准备标注数据。hanlp 2.1内置了百度开源的SKEP情感分析预训练模型通过hanlp.pretrained.sentiment.SKEP_SENTIMENT_ANALYSIS一行就能加载。SKEP在中文评论语料上做过针对性预训练对“手感丝滑”“打击感拉满”这类游戏领域表达有较好的识别能力。另一个实际好处是hanlp本身是以管道式设计组织的。你可以先用它的分词组件把评论切成词序列再对每个词做词性标注最后接情感分析。分词这个前置步骤对结果的影响经常被忽略Steam评论里大量出现“魂系”“肉鸽”“刷子游戏”这类词汇分词不对后面的模型就很难判断。hanlp的分词器默认词典已经覆盖了大部分游戏圈黑话这比直接丢给BERT一个长句子要可控。3.2 加载SKEP模型并做批量预测import hanlp import json import re # 加载SKEP情感分析模型 # 首次运行会从远端下载模型文件耐心等待之后会缓存到本地 senta hanlp.load(hanlp.pretrained.sentiment.SKEP_SENTIMENT_ANALYSIS) def split_and_predict(text, max_len450): 对超长评论做分句预测再按句长加权聚合。 hanlp的SKEP模型对单条输入长度有限制 直接把超长句子丢进去会被截断导致负面信息丢失。 if not text or len(text.strip()) 0: return 0.5 # 空文本返回中性 sentences re.split(r[。!?;,], text) sentences [s for s in sentences if len(s.strip()) 2] if not sentences: return 0.5 # batch预测避免一条条调用带来的额外开销 preds senta(sentences) # preds是list每个元素是0(负面)或1(正面) scores [float(p) for p in preds] # 按句子长度加权长句是主要观点载体的概率更高 weights [len(s) for s in sentences] weighted sum(s * w for s, w in zip(scores, weights)) / sum(weights) return weighted def batch_analyze(reviews, sample_limitNone): 给评论列表批量打情感分返回0~1之间的情感倾向值。 大于0.5为正面小于0.5为负面。 if sample_limit: reviews reviews[:sample_limit] results [] for r in reviews: score split_and_predict(r[content]) results.append({ review_id: r[review_id], content: r[content][:200], rating: r[rating], # 玩家自己点的好评/差评 sentiment: score, label: positive if score 0.5 else negative }) return results if __name__ __main__: with open(reviews.json, r, encodingutf-8) as f: reviews json.load(f) analyzed batch_analyze(reviews, sample_limit500) with open(analyzed.json, w, encodingutf-8) as f: json.dump(analyzed, f, ensure_asciiFalse, indent2)这里的关键参数是max_len450。SKEP对输入长度敏感中文按字计算450字以内的评论可以完整保留语义。超过这个长度的评论我会先用标点符号切句逐句预测后再用句长加权汇总。加权的原因是Steam上不少差评正文是“游戏时长0.1小时”一段具体吐槽短句往往情绪更极端长句则更多是解释性内容简单平均会把极端情绪放大句长加权能稍微中和这个偏差。label的判断阈值0.5你可以在后期根据需求调整。比如只看强烈情绪时可以把阈值提到0.65以上如果只是想区分好坏口碑0.5就够。这段代码还保留了原始rating字段这是玩家自己点的“推荐/不推荐”可以用来和模型预测结果做交叉验证。如果某个游戏rating和sentiment的冲突率超过30%大概率是模型在该游戏的特殊语境上失效这时候就需要回到第5章的避坑办法处理。3.3 为什么“好评但情感倾向为负”并不一定是模型错了一个容易误判的场景是反讽和玩梗。比如“不是我说这优化真不行”这句字面逻辑是正的但语义是负的。SKEP对这类语句的鲁棒性一般。我一般会在分析结果里专门标记出“rating和sentiment不一致”的样本把它们单独抽出来看而不是直接当成模型错误。实际上这些不一致往往是“玩家觉得游戏还可以但某个具体问题让他很生气”的混合情绪它们对于运营分析的价值甚至高于那些一致样本。4. 可视化把上千条情感分数变成能直接汇报的图表4.1 清洗这一步决定了图表的可信度import pandas as pd import numpy as np df pd.read_json(analyzed.json) # 1. 删除重复评论 df df.drop_duplicates(subsetreview_id) # 2. 转换时间戳 df[date] pd.to_datetime(df[timestamp], units) # 3. 过滤无意义内容只包含标点/表情符号的评论 def is_noise(text): import re text re.sub(r[^\u4e00-\u9fa5], , text) return len(text) 3 df df[~df[content].apply(is_noise)] print(清洗后剩余评论: {}条.format(len(df))) print(好评率(按玩家rating): {:.1%}.format(df[rating].mean())) print(正面情感占比(按模型): {:.1%}.format((df[sentiment] 0.5).mean()))清洗是可视化之前最容易偷懒却最影响结果质量的一步。drop_duplicates(subsetreview_id)清掉同一用户在同一时间对同一游戏的重复提交这在批量爬取时很常见。is_noise函数会把“”“2333”“nb”这类长度太短或没有有效汉字的内容过滤掉它们对情感统计只会产生干扰。然后看两个数字的对比玩家自己点的好评率和模型判断的正面情感占比。如果差异在5个百分点以内说明模型在这个游戏上的表现基本可信图表可以做如果差异过大请回到第5章排查。4.2 用PyECharts直接输出HTML图表可视化我推荐用PyECharts它生成的HTML文件可以任意分享不需要部署服务端这一点对课程设计和临时汇报非常友好。from pyecharts.charts import Pie, Bar, Line from pyecharts import options as opts # 饼图玩家评价分布 vs 模型情感分布 pie Pie() pie.add(玩家自评, [(好评, int(df[rating].sum())), (差评, int((~df[rating]).sum()))]) pie.add(模型判断, [(正面, int((df[sentiment] 0.5).sum())), (负面, int((df[sentiment] 0.5).sum()))]) pie.set_global_opts(title_optsopts.TitleOpts(titleSteam评论情感分布对比)) # 折线图按日期的情感均值走势 df[week] df[date].dt.to_period(W).apply(lambda x: x.start_time) trend df.groupby(week)[sentiment].mean() line Line() line.add_xaxis([str(d.date()) for d in trend.index]) line.add_yaxis(情感均值, [round(v, 2) for v in trend.values]) # 柱状图负面评论关键词Top10 neg_df df[df[sentiment] 0.5] keywords [优化, 闪退, 服务器, 外挂, 掉线, BUG, 价格, 剧情, 手柄, 存档] counts [neg_df[content].str.contains(k).sum() for k in keywords] bar Bar() bar.add_xaxis(keywords) bar.add_yaxis(负面评论提及次数, counts) # 三个图放在同一个HTML里 from pyecharts.charts import Tab tab Tab() tab.add(pie, 分布对比) tab.add(line, 情感趋势) tab.add(bar, 负面关键词) tab.render(steam_sentiment_report.html) print(已生成报告: steam_sentiment_report.html)这段代码里有几个值得解释的细节。饼图同时展示了“玩家自评”和“模型判断”两组数据两组数据能直接对比一眼就能看出哪些游戏存在“好评但模型读出来是负面”的情绪差。折线图按周聚合是因为Steam评论每天的量不够均匀日粒度折线会抖动得无法阅读周均值更稳。负面关键词列表是写死在代码里的常用游戏问题词如果你的目标游戏有特殊槽点比如“卡顿”“氪金”需要自己扩充这个列表。这里有替代方案如果只是想快速看一眼分布不做长期监控用pandasmatplotlib也够但中文字体乱码是绕不开的坑。PyECharts在目前版本里对中文支持相对省心这是它适合这个项目的原因之一。图表生成之后最好检查一下文件中能否正常显示中文标题如果不能通常是因为你的HTML文件没有声明UTF-8编码。5. 避坑评论爬取和情感分析最容易翻车的5个点5.1 429错误与限流不是你的代码写错了是接口在教你做人现象连续翻页10次之后所有请求都返回429 Too Many Requests然后整个脚本停止或空跑。原因Steam的AppReviews接口对请求频率有隐形限制尤其是同一个IP在短时间内高频请求时会被直接限流一段时间。云服务器出口IP因为被多人共用更容易被标记。解决把sleep_sec从1秒提高到2-3秒在请求层增加指数退避逻辑第一次429等待30秒第二次等待60秒第三次120秒不要无限重试。更稳妥的方案是给爬虫加断点续爬逻辑每成功保存一页就写一次临时文件这样即便中途被限流重启后也能从游标位置继续而不是从头再来。5.2 cursor参数的“伪死循环”现象翻页翻到第10页之后发现新拿到的评论和前面某页完全重复或者一直拿同一个游标。原因这是典型的游标分页误用。AppReviews返回的cursor是给“下一次请求”用的但如果你在参数里没有保持filterall接口内部可能会切换到不同的汇总逻辑导致游标回退。解决每次请求都显式带上filterall和languageschinese不要依赖上一次的参数缓存把游标持久化存到文件里定期检查它是否在最近几轮中重复出现超过3次。代码里已经用recommendationid去重兜底但治本还是得坚持显式传参。5.3 hanlp首次加载像卡死一样等了10分钟还没反应现象第一次执行hanlp.load(SKEP_SENTIMENT_ANALYSIS)时终端没有输出像死循环。原因hanlp会在首次加载时从远端下载预训练模型文件这个文件体积不小下载速度取决于你的网络环境。它不会打印进度条看起来像卡住。解决先单独跑一次hanlp.load(hanlp.pretrained.sentiment.SKEP_SENTIMENT_ANALYSIS)等它完全加载后再关掉第二次运行时模型会走本地缓存加载时间降到几秒。不要在每次爬取评论前都重复加载模型正确结构是“模型只加载一次评论循环处理”。如果网络下载一直失败可以考虑设置hanlp的配置文件指向镜像源但不要用系统代理之类的方案直接换一个更稳定的网络环境更省心。5.4 差评被识别成好评反讽和玩梗的锅现象某一款游戏的“肉麻好评”比例出奇的高比如“这游戏太棒了我玩了5分钟就退款”模型判成了正面。原因SKEP模型的训练语料主要来自电商和影视评论对于游戏社区特有的反讽表达覆盖不够。像“这优化真是太好了我3080都卡成PPT”这句字面正、语义负模型分不清。解决在进入情感分析前先做一轮针对性的规则兜底。常见做法是准备一个负面触发词表比如“退款”“闪退”“卡死”“无语”“垃圾”“呵呵”如果一条评论命中其中任意一个直接把这句的情感分数压到0.5以下。规则先行、模型后置的混合策略在这个场景下比单独换模型更务实。5.5 可视化时间轴乱序时间戳没转成日期现象折线图横坐标出现2025年、2026年跳来跳去或者按字符串排序导致月份错位。原因timestamp_created是Unix时间戳直接用字符串格式转换后pandas会把它按object类型排序不是按时间顺序。解决用pd.to_datetime(df[timestamp], units)显式转成datetime类型并且把这一列设为索引df.set_index(date)再在折线图里调用时pandas会按时间自动排序。另外Steam的时间戳是UTC如果对“当天几点”有分析需求要自行加8小时很多第一次做的人会忽略这一点导致日报数据每天差8小时。6. 进阶把这个脚本做成一个随时能跑的Steam口碑监控小工具写脚本的时候把爬取、分析、可视化三段拆成三个独立的函数留出清晰的输入输出接口。我一般会做一个run_pipeline(appid, pages20)的总入口执行完毕后同时输出reviews.json、analyzed.json、steam_sentiment_report.html三个文件。这样每周跑一次就能基于历史数据对比两次的情感均值曲线看一个游戏在版本更新或打折前后的口碑变化而不是一次性看完就扔掉。一个值得做的扩展是“按评论长度拆分分析”。短评少于50字通常表达强烈情绪长评则信息量大但情绪温和。把两者分开统计能得到“热烈好评率”和“深度好评率”两个指标这在判断一个游戏是老玩家沉淀还是新玩家涌入时非常有用。另外把玩家自己点的rating作为监督信号定期计算模型预测准确率能及时发现你的游戏是否存在“反讽文化”导致模型整体漂移的风险。我之前在这个项目上踩过一个印象深刻的坑把filter参数写成了recent结果爬到的全是近期好评后面情感分析和可视化全建在一个错样本上图表好看但没有意义。从那以后每次爬完我都会先顺手打一条统计——好评和差评的条数各占多少再决定要不要继续往下做。这套源码方案真正的价值不在一行代码的精巧而在于每一步都留了校验和兜底的口子能让数据始终站得住。希望帮到你。本文还有配套的精品资源点击获取