资讯动态

网易云评论爬虫与情感分析:从数据采集到交互可视化全链路实践

发布时间:2026/10/3 14:36:44 来源:尧图企业网站定制
简介基于Python的网易云音乐评论采集与情感分析项目面向计算机相关专业学生、毕设开发者及对爬虫和自然语言处理感兴趣的初学者集成了歌曲评论用户信息抓取、评论情感判断、可视化展示与实时评论分析功能。资源共122个文件压缩包18.67MB包含14个Python源码、13个JS、8个HTML与7个CSS等前后端页面文件另有图片、字体、SQL及各类配置文件代码均已测试运行成功可直接导入开发环境使用。包内文件类型覆盖爬虫脚本、情感分析逻辑、Web可视化界面与数据存储目录结构清晰便于按模块理解与修改。目前已有245人学习下载适合用于课程设计、毕业设计或作为入门人工智能与爬虫项目的一次完整实践参考。1. 网易云评论爬虫与情感分析先搞清楚这条链路最值钱的是情绪聚合“基于Python通过爬虫获取网易云音乐歌曲评论用户信息、评论信息对评论信息进行情感分析用户信息、分析结果进行可视化注释”——这句话放进简历里是一行但真正跑通它要靠一条完整的工程链路用requests构造评论请求、翻页去重、追用户详情、清洗评论文本、用情感分析模型给评论打正负分、最后把分门别类的统计结果用pyecharts画成交互页面。网易云评论区能成为这个链路的理想素材因为它是真实语料大量网络流行语、emoji、反讽和情绪化短句比任何标准数据集都更能暴露模型的短板。这条链路适合谁正在学Python、手头缺一份真实数据、想把“爬虫→分析→可视化”完整串起来的从业者。反直觉的是最终交付里最值钱的不是抓到的几万条评论而是聚合后的情绪分布、用户画像和情绪随时间变化的趋势。只会搬运数据报告没有说服力把情感分析和可视化标注做扎实这份结果才值得被别人采用。2. 网易云评论与用户数据接口解析先拿热门评论再追用户画像2.1 抓取前准备为什么请求头里必须有 User-Agent 和 Referer网易云音乐Web端的评论区数据不是面向第三方开放的公开API而是网页内部接口。直接用requests裸GET服务器大概率回403或一段用于人机校验的HTML。requests能在Python爬虫入门里站稳脚跟就是因为它把HTTP请求、响应解析收拢成几十行代码能快速验证接口行为。在vscode里配好Python环境后建议先把公共请求头做成全局字典后续所有请求都复用。import requests HEADERS { User-Agent: (Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36), Referer: https://music.163.com/, Accept: application/json, text/plain, */*, }逻辑说明这个字典在整段爬虫代码里被反复引用评论接口和用户详情接口共用同一套请求头让服务端看到的请求来源像同一个浏览器会话。参数说明User-Agent不要直接抄网上过时的老版本可以在Chrome地址栏输入about:version查看当前浏览器字符串。Referer写站内首页通常也能过但更稳妥的做法是写歌曲详情页URLAccept只是声明期望JSON响应不填一般不影响结果。请求头是反爬的第一道门槛也是最容易排查的参数。2.2 热门评论接口limit 和 offset 的翻页边界网易云Web端一直存在一类老接口路径形如https://music.163.com/api/v1/resource/comments/R_SO_4_歌曲ID?limit20offset0。R_SO_4_后面跟歌曲IDlimit是单页条数offset是偏移量。返回的JSON里包含comments数组、total字段以及每个评论里的user对象、content文本、likedCount点赞数和time时间戳。def fetch_comments(song_id, offset0, limit20): url fhttps://music.163.com/api/v1/resource/comments/R_SO_4_{song_id} params {limit: limit, offset: offset} resp requests.get(url, paramsparams, headersHEADERS, timeout10) resp.raise_for_status() data resp.json() return data.get(comments, []), data.get(total, 0)逻辑说明fetch_comments每次调用只取一页数据。data[total]是这首歌的总评论数翻页循环以total为边界不会跑到空页还继续请求。参数说明offset从0开始每次翻页加limit比如limit20第二次请求offset20第三次40。timeout设成10秒避免网络波动时脚本长期挂起。老接口在部分歌曲上会返回“评论加载失败”这时可以改请求方式或换一批歌曲ID。翻页时不要贪多limit固定20翻得又快又密更容易触发频控。2.3 用户详情接口从评论者 ID 追到用户标签评论列表里的user对象只有userId、nickname和avatarUrl拿不到年龄、城市和性别。要补齐用户画像需要再请求用户详情接口。常见做法是访问https://music.163.com/api/v1/user/detail/{userId}返回结构里的profile字段包含年龄、性别、城市编码、个人简介等信息。def fetch_user_detail(user_id): url fhttps://music.163.com/api/v1/user/detail/{user_id} resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() data resp.json() if data.get(code) ! 200: return None profile data.get(profile, {}) return { user_id: profile.get(userId), nickname: profile.get(nickname), age: profile.get(age), gender: profile.get(gender), city: profile.get(city), province: profile.get(province), signature: profile.get(signature), }逻辑说明用户注销或开启隐私保护时接口会返回非200的code这时不做判断直接取profile[age]会抛KeyError所以先判code拿不到就返回None。参数说明gender字段0是保密、1是男、2是女出图前记得做映射。city和province是行政区划编码不是中文名需要在可视化阶段再做一次字典转换这一步最容易漏。用户详情接口的请求量很大不能对每条评论都追一次。热门评论里同一批活跃用户会反复出现比较聪明的做法是先去重再限量抓取from collections import Counter def fetch_top_users(comments_df, min_count2, top_n100): user_counts Counter(comments_df[user_id].dropna()) top_ids [uid for uid, cnt in user_counts.items() if cnt min_count] results [] for uid in top_ids[:top_n]: info fetch_user_detail(uid) if info: results.append(info) return results逻辑说明先统计每个userId在评论里出现的次数只保留出现次数不低于min_count的用户最多追top_n个。这样即使用户详情接口有限制也能覆盖评论区的主要活跃者。参数说明min_count2意味着至少在评论区出现两次的用户才值得追详情这类用户往往是忠实听众对画像分析更有价值。top_n建议控制在100以内详情接口的疲劳速度比评论接口更快。2.4 把抓取结果落成 DataFrame先清理字段再进分析爬虫拿回来的是列表套字典直接做情感分析会在数据类型的各种细节上踩坑。先把评论和用户信息规整成pandas DataFrameimport pandas as pd def comments_to_frame(comments): rows [] for c in comments: user c.get(user, {}) rows.append({ comment_id: c.get(commentId), user_id: user.get(userId), nickname: user.get(nickname), content: c.get(content), liked_count: c.get(likedCount), time: c.get(time), }) df pd.DataFrame(rows) df[time] pd.to_datetime(df[time], unitms) return df逻辑说明把评论时间和点赞数统一转成方便处理的类型。time原本是毫秒级Unix时间戳用unitms直接转成可读时间后面画“几点钟用户最爱发评论”就顺手了。参数说明likedCount是当前页面展示的点赞数作为热度和传播力指标足够用。content为空字符串可能是接口返回异常或系统折叠评论先保留到清洗阶段再统一处理。这个DataFrame就是整条分析链路的原料情感分析输入content列可视化要用的时间在time列用户画像的键在user_id列。字段名含义处理注意comment_id评论唯一ID用于去重和增量存储user_id评论者ID整数勿转字符串content评论文本清洗后再给情感模型liked_count点赞数可作为情感聚合权重time评论时间毫秒级时间戳需转换到这一步爬虫侧的数据采集已经闭环接下来就能把文本交给情感分析模块。3. 评论情感分析落地选型、阈值与规则修正3.1 为什么默认先试 SnowNLP轻量、不训练、中文友好给网易云评论这样的真实语料做情感分析大致有三条路线。基于情感词典的规则打分比如把“好听”“温暖”记成正向把“难听”“失望”记成负向问题是要长期维护词典一条评论里同时出现“好听但失望”时很难权衡。基于预训练模型的中文情感分类效果更好但依赖环境重、模型下载动辄几百MB对以爬虫为主的单机脚本来说门槛抬得太高。相比之下SnowNLP是纯Python实现内置一份中文语料模型调用sentiments属性直接返回0到1的概率值最适合先把链路跑通。还有一条更往后的路是多模态情感分析把图片评论、短视频弹幕也纳入判断那是文本情感分析做扎实以后的话题。现在先把SnowNLP当成一个带温度计的黑匣子用但它默认模型偏商品评论对网易云风格的文艺表达识别能力有限所以不能拿来就用后面必须叠加清洗和规则修正。方案上手成本依赖体积适合场景情感词典低维护费劲无业务词汇固定的场景SnowNLP最低小快速验证、通用中文评论预训练模型高大准确率要求高的生产环境多模态方案很高大需要理解图片、视频语义时3.2 评论清洗emoji、URL、用户都要在进模型之前干掉网易云评论里大量混着emoji、某人、歌曲直链和零宽字符这些字符串会干扰分词和情感打分。清洗函数要依次处理删掉URL、删掉用户、剔掉控制字符、压缩空白最后截断超长文本。import re def clean_comment(text): text re.sub(rhttps?://\S, , text) text re.sub(r\S, , text) text re.sub(r[\u0000-\u001f\u007f-\u009f], , text) text re.sub(r\s, , text).strip() return text[:200]逻辑说明四个re.sub依次处理链接、用户、控制字符和多段空白。\S匹配连续非空白字符能把整段URL连带着删干净。最后切片到200字以内减少分词耗时也避免超长句子让情感模型判得四不像。参数说明控制字符的正则范围覆盖换行符、零宽空格和不可见字符。这一步效果最直观——清洗前“好想哭……(´;ω;)”这种文本会被分词拆得七零八落清洗后变成干净的短句情感分数更有参考意义。3.3 情感判定先打分再用阈值切成三个阵营SnowNLP的API很简单from snownlp import SnowNLP def analyze_sentiment(text): if not text: return None try: return SnowNLP(text).sentiments except Exception: return None逻辑说明sentiments返回0到1之间的小数越接近1越正向越接近0越负向。SnowNLP面对空字符串或纯emoji可能抛异常所以用try包住返回None给上层统一处理。参数说明默认阈值一般取0.6和0.4大于等于0.6判正向小于等于0.4判负向中间归中性。这是经验值不是官方规定。如果抓的是“深夜emo”类歌单大量评论带丧但不算负可以下调到0.55和0.45提高情感识别的灵敏度。三分类函数def sentiment_class(score, pos_th0.6, neg_th0.4): if score is None: return unknown if score pos_th: return positive if score neg_th: return negative return neutral逻辑说明返回字符串而不是数字是为了后续做groupby分组时表意清晰。最后统计正向占比时再把字符串映射回数值即可。参数说明pos_th和neg_th不一定要对称如果想更敏感地捕捉负面情绪可以把neg_th抬到0.45让更多的边界评论落进负向阵营。阈值怎么定才不玄学最靠得住的办法是拿自己标注的100条评论做反向校准最后一章会专门讲。3.4 不能用完即弃反讽和网络黑话需要规则修正网易云评论里经常有“这歌也太好听了吧狗头”这种文案SnowNLP几乎一定会判成正向因为训练语料里根本没有这种反讽样本。这时候调阈值没意义需要叠加一个针对领域的修正词典REVERSE_WORDS [狗头, 手动滑稽, 认真的吗, , 就这, 也就那样] def rule_correct(score, text, correction-0.15): if any(w in text for w in REVERSE_WORDS): return score correction return score逻辑说明文本里出现反讽信号词时直接把原始分数压低0.15再返回把“阴阳怪气”从正向拉到中性甚至负向附近。规则看起来粗暴但能明显减少把反讽当赞美的翻车。参数说明correction取-0.15而不是-0.3是为了保留模型自身判断的空间规则只做纠偏不喧宾夺主。REVERSE_WORDS这张词表要根据抓到的语料持续补充每首歌的评论区高频梗都不一样这是真实语料项目的常态。到这一步情感分析已经从一句模型调用变成“清洗、打分、阈值、规则修正”四段流水线跑出来的分布数据才禁得起追问。4. 可视化与代码注释把情感结果做成能直接交付的交互页面4.1 第一张图pyecharts 情感分布饼图让结果先“看得见”很多人一上手就想要可视化大屏但大屏本质是把多张单图组合起来。先用pyecharts画单图验证数据合理性pyecharts是ECharts的Python封装直接输出HTML浏览器里就能交互不需要额外的前端项目。from pyecharts.charts import Pie from pyecharts import options as opts def draw_emotion_pie(df, outputemotion_pie.html): counts df[sentiment].value_counts() data [(正向, int(counts.get(positive, 0))), (中性, int(counts.get(neutral, 0))), (负向, int(counts.get(negative, 0)))] pie Pie() pie.add(情感分布, data, radius[35%, 65%], label_optsopts.LabelOpts(formatter{b}: {c} ({d}%))) pie.set_global_opts(title_optsopts.TitleOpts(title评论情感分布)) pie.render(output) return output逻辑说明先用value_counts清点三个阵营的条数再转成pyecharts需要的二元组列表。radius的35%和65%表示环形内半径和外半径中间留空看起来比实心饼图清爽。参数说明{d}%是pyecharts内置的百分比占位符会自动计算占比。如果希望每次出图配色一致应该在add里用itemstyle指定各分类颜色而不是依赖默认色板否则同一份数据换台机器跑出来颜色会变。提示在Linux服务器上出图中文可能显示成方框需要在代码里注册中文字体或下载字体文件后指定font_path。4.2 词云图把高频词和情感词分开提取再画词云是评论区可视化的另一件趁手工具但直接把所有词扔进去出来的全是“音乐”“真的”“感觉”这类高频无用词。正确做法是先用jieba分词、过滤停用词再分别对正向评论和负向评论做词云。from wordcloud import WordCloud import jieba STOP_WORDS {真的, 感觉, 喜欢, 音乐, 一首, 一直} def draw_comment_wordcloud(df, outputcomment_cloud.png): pos_text .join(df[df[sentiment] positive][clean_content]) words .join(w for w in jieba.cut(pos_text) if w not in STOP_WORDS) wc WordCloud(font_pathmsyh.ttc, width800, height600, max_words200, background_colorwhite).generate(words) wc.to_file(output) return output逻辑说明wordcloud的generate输入是空格分隔的词串所以先对正向评论做jieba分词再过滤停用词后拼接。font_path必须指向中文字体文件否则输出图片里全是方框。参数说明STOP_WORDS根据数据不断迭代像“哈哈”“呜呜”这类语气词是否需要保留要看分析目的。max_words200控制词云只显示前200个高频词太大或太小都会影响可读性。输出是PNG方便直接塞进汇报文档。多层图可以叠加到Tab里做成一个小型可视化大屏效果from pyecharts.charts import Tab tab Tab() tab.add(pie, 情感分布) tab.add(bar, 评论时间分布) tab.add(cloud, 正向高频词) tab.render(report.html)逻辑说明Tab把多个图表合并到一个HTML文件里前端通过标签页切换。这是从单图到大屏之间最平滑的一步而且不需要写一行前端代码。4.3 用户画像可视化评论时间、性别与年龄分布用户信息抓回来后至少要出两张图才算完成“用户信息可视化”这半件事。第一张是评论时间分布图。把time字段转成小时统计每个时段的评论数量能直观看出这首歌的听众在深夜更容易“破防”。第二张是性别饼图或年龄直方图注意gender字段要先做0/1/2到保密/男/女的数据映射age字段里的0值也要过滤——网易云很多用户没填年龄默认值是0而不是空。def hourly_counts(df): hour df[time].dt.hour return hour.value_counts().sort_index().rename_axis(hour).reset_index(namecount)逻辑说明利用pandas的dt.hour从已经转成datetime的time字段里提取小时再做value_counts一条低配的情绪时辰图就出来了。用柱状图配上这个数据可读性比散点图好很多。4.4 代码注释规范让爬虫脚本半年后还能自己看明白标题里的“注释”既指图表上的数据标签也指代码里的工程注释。图表注释靠pyecharts的label_opts控制代码注释则更应该建立一套固定习惯文件头写docstring说明用途函数docstring写清参数和返回值关键行只注释“为什么”而不是“是什么”。def fetch_all_comments(song_id, max_pages50): 抓取一首歌的全部热门评论。 Args: song_id (int): 网易云歌曲ID。 max_pages (int): 最大翻页数防止死循环。 Returns: list: 清洗后的评论字典列表。 all_comments [] for offset in range(0, max_pages * 20, 20): comments, _ fetch_comments(song_id, offsetoffset) if not comments: break all_comments.extend(comments) return all_comments逻辑说明range里写max_pages * 20这个20必须和fetch_comments默认的limit保持一致否则翻页数不对。断在空列表时立刻break防止无限请求把请求方送进风控名单。参数说明函数docstring里最有用的是Args和Returns半年后回来看脚本的人靠这两行就能秒懂函数契约。max_pages*20的20是魔法数字最好提成模块级常量PAGE_SIZE避免改一处漏一处。5. 网易云爬虫常见问题与避坑频控、emoji、注销用户和情感误判5.1 接口返回403不是封IP是低频控现象连续翻页二十多次后resp.status_code变成403响应体不再是JSON而是一段HTMLjson()直接抛异常。原因评论区接口属于网页内部接口哪怕带了Referer也有请求频率限制同一IP单位时间内请求次数太多服务端直接拒绝。解决在每次请求之间sleep随机延迟再对失败请求做重试。import time import random def fetch_comments_with_retry(song_id, offset0, limit20, retry3): for attempt in range(retry): try: return fetch_comments(song_id, offsetoffset, limitlimit) except Exception: time.sleep(2 random.random() * 3) return [], 0逻辑说明sleep的粒度是2到5秒随机避免固定间隔的规律性请求。重试3次仍失败就返回空页让主流程继续往下走而不是整个脚本崩掉。5.2 评论里的 emoji 变乱码现象DataFrame里content列出现方块字符保存到MySQL直接报错。原因emoji是四字节Unicode字符超出常规utf8的存储范围连接MySQL时需要utf8mb4控制台打印时也需要合适编码。解决抓取阶段就把emoji替换成占位文本或统一用utf8mb4建表import emoji def replace_emoji(text): return emoji.demojize(text, delimiters( [, ] ))逻辑说明demojize把转成[slightly_smiling_face]这种文本占位符分析情感时不影响语义存储也不会撑爆utf8字段。如果不想引入emoji库用正则单独摘除也可以。5.3 用户详情取不到注销用户和隐私设置现象fetch_user_detail返回None后续代码没判断直接取profile[age]就抛KeyError整个循环中断。原因网易云存在大量注销用户和隐私保护账号用户详情接口对这类ID返回code非200profile字段可能是空对象。解决先判code拿不到就跳过导出报告时对缺失字段统一填充“未知”。抓取规模大时把返回None的user_id放进一个集合避免重复请求浪费额度。5.4 SnowNLP 把“这也太好哭了吧”判成负向现象一条明显正向的评论“这也太好哭了吧”被标成negative占比统计失真。原因SnowNLP默认语料偏商品领域对网易云评论区的文艺表达和口语程度量不足字面上有“哭”就判定负面。解决先累积错题清单再针对性补充规则不要盲目调阈值。MISCLASSIFIED_PAIRS [ (太好哭了吧, 0.85), (听哭了, 0.80), (鸡皮疙瘩起来了, 0.80), ] def rule_boost(score, text): for kw, target in MISCLASSIFIED_PAIRS: if kw in text: return max(score, target) return score逻辑说明当文本命中表情强烈的正向关键词时直接把分提到一个下限值。这类规则每积累一批就要更新一次比反复调全局阈值更精准。5.5 换台电脑就抓不动了请求头指纹和 Cookie 问题现象在A机器上跑得顺畅换到B机器后第一次请求就被拦截。原因反爬除了核对请求头还会看TLS指纹和请求顺序。requests在不同Python环境下发出的TLS指纹不完全一致部分反爬系统能识别出非浏览器客户端。解决优先把Cookie带全Cookie能从浏览器开发者工具直接复制不要一开始追求免Cookie。如果带Cookie仍被拦可改用Playwright走真实浏览器内核selenium方案因特征明显已被很多站点识别Playwright的现代浏览器指纹更容易通过。提示不要为了绕过频控去频繁更换伪造参数那可能加重风控标记控制抓取频率才是长期做法。6. 进阶玩法增量抓取、阈值校准与“后悔药式”回滚6.1 增量抓取用 SQLite 记住已经见过的评论歌曲的评论会不断新增重复抓全量既浪费配额又会让下游分析重复计算。轻量场景下我直接用Python内置的sqlite3保存comment_id作为主键每次入库前做一次差集。如果以后要跨服务共享数据再用SQLAlchemy把同一套表结构映射到MySQL也不迟。import sqlite3 def init_db(db_pathcomments.db): conn sqlite3.connect(db_path) conn.execute(CREATE TABLE IF NOT EXISTS comments ( comment_id INTEGER PRIMARY KEY, song_id INTEGER, content TEXT, sentiment REAL)) return conn def save_new_comments(conn, song_id, df): exists {r[0] for r in conn.execute(SELECT comment_id FROM comments)} new_df df[~df[comment_id].isin(exists)] for _, row in new_df.iterrows(): conn.execute( INSERT OR IGNORE INTO comments (comment_id, song_id, content, sentiment) VALUES (?, ?, ?, ?), (int(row[comment_id]), song_id, row[content], row[sentiment]) ) conn.commit() return len(new_df)逻辑说明先从库中读一遍已有评论ID集合用DataFrame的isin筛出未见过的新评论再批量写入。INSERT OR IGNORE作为二次保险防止并发或重复调用时炸主键。返回新增条数便于定时任务打日志。6.2 校准阈值给别人看图表之前先自己标注 100 条有一套我每次跑新歌单都会做的动作随机抽100条非重复评论人工标注成positive、negative、neutral再用脚本搜一遍最优阈值。def best_threshold(samples): best_acc, best_params 0, (0.6, 0.4) for pos_th in [x / 100 for x in range(55, 71)]: for neg_th in [x / 100 for x in range(25, 46)]: pred [sentiment_class(s, pos_th, neg_th) for _, s, _ in samples] acc sum(1 for p, (_, _, h) in zip(pred, samples) if p h) / len(samples) if acc best_acc: best_acc, best_params acc, (pos_th, neg_th) return best_params逻辑说明在pos_th和neg_th的候选网格里穷举找到与人工标注准确率最高的组合。samples是(text, score, human_label)三元组列表。这套方法不要拿去和业务方争论模型好坏只是帮自己定一个更靠谱的切分点。阈值每次都会变我把校准结果存成一个JSON和当批数据放在一起。下次重跑如果发现准确率下降直接回滚到上次阈值这就是我给自己的“后悔药”。我最早跑通这条链路时没有做阈值校验图表交上去被业务方拿三条评论就推翻从那以后固定动作都是先采样标注再出图。整个流程里最值得投入时间的不是爬虫写得有多快而是情感分析这段能不能经得起抽查。希望这套从接口解析到阈值校准的流程能帮到你让这条爬虫链路不只是跑通而是真的敢把结果交出去。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑