简介基于微博情感分析系统的毕业设计项目面向计算机相关专业毕业生及有Python基础的实践者完整呈现从微博数据获取、文本预处理到多种分类器训练与评估的工程链路。压缩包共71个文件以31个Python脚本为核心搭配15个npy参数文件、4个model模型、13个txt语料与词表以及docx论文参考和README说明整体约6.25MB目录按数据、模型、实验等模块拆分便于按阶段复现。项目覆盖微博API与OAuth2.0授权采集、中文分词、TF-IDF特征构建并分别实现SVM初步分类、朴素贝叶斯情感打分以及AdaBoost集成优化模型文件可直接加载验证也可基于训练脚本重新调参训练。资源附带了停用词表、正负向情感词典与多版本模型参数能辅助理解分类器差异与调优思路。已有6976人学习适合需要系统复现实验、独立完成毕业设计或希望借鉴现成文本分类脚本快速搭建情感分析任务的中高级Python学习者。1. 基于微博情感分析系统从爬虫到可视化的完整闭环做毕业设计选「微博情感分析系统」这个题目的同学十有八九是看中它好讲、好演示、技术栈齐全。确实是这么回事——它同时覆盖了爬虫采集、中文分词、情感判定、可视化展示四个模块能向评委完整展示从数据获取到业务分析的全链路能力。但这条链路里真正的难点不在写代码而在四个环节的衔接微博的反爬限制怎么破、中文文本怎么切才准、情感词典怎么定阈值才不飘、最终结果怎么呈现才像样。这套系统把 Python 生态里处理中文情感分析的主流方案串了一遍适合想快速搭出一套能跑通、能答辩、能截图演示的毕业设计也适合想了解微博数据到底怎么落地的初学者。本文会把这套系统的设计思路、核心代码、关键参数和最容易翻车的几个坑全部拆开讲。2. 微博数据采集Cookie 伪装与评论抓取的工程细节2.1 移动端接口为什么选 m.weibo.cn 而不是主站微博主站的 HTML 结构复杂接口做了大量动态加载和风控校验直接抓很容易触发滑块验证。这套系统选择的是移动端接口 m.weibo.cn 下的 API原因很简单移动端接口返回的是结构化 JSON字段完整而且爬取难度比 PC 端低一个量级。采集层的工作流是先用关键词搜索微博内容拿到微博 ID 之后再逐条抓取评论。核心逻辑是requests.Session()维持会话配合 Cookie 冒充浏览器身份。代码里有一个比较关键的参数是since_id它是微博移动端的分页游标跟传统翻页的 page 参数不同——第一页返回的 JSON 里如果带since_id下次请求就用它替代不传since_id就是从头开始。import requests import time import random session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1, Referer: https://m.weibo.cn/, }) def fetch_comment(weibo_id, cookie_value, max_pages5): session.headers[Cookie] cookie_value base_url https://m.weibo.cn/api/comments/show?id str(weibo_id) since_id None result [] for page in range(max_pages): params {} if since_id: params[since_id] since_id # 分页游标不传时从第一页开始 resp session.get(base_url, paramsparams, timeout10) if resp.status_code ! 200: break data resp.json() # 接口正常时返回的 data 里是评论列表同时携带下一次分页的游标 if data not in data or not data[data]: break result.extend(data[data]) new_since_id data.get(data, {}).get(max_id, 0) \ if isinstance(data[data], dict) else 0 if not new_since_id or new_since_id since_id: break since_id new_since_id time.sleep(random.uniform(1.0, 2.5)) return result这段代码里核心参数有三个max_pages控制单条微博评论的采集深度毕业设计一般 35 页就够形成样本量random.uniform(1.0, 2.5)是请求间隔单位是秒这是最基础的防封策略——太短容易被识别为机器太长会让采集效率变得难以接受max_id是评论分页游标的字段名注意它跟微博正文搜索的since_id字段名不同不要混用。采集完成后结果要落盘。通常的做法是存 CSV字段至少要保留评论正文、发布时间、点赞数三个维度——正文用于情感分析时间用于趋势统计点赞数可以做加权情感值。这个数据文件是后续分析的底座建议采集完立刻做去重和空值剔除别拖到分析阶段再处理。2.2 登录态维护Cookie 失效是最常见的中断原因抓取微博评论时一个难点是未登录状态和登录状态能拿到的评论数量差距很大。未登录时接口会限制只能看到前几页评论对样本量要求高的分析来说根本不够。所以这套系统普遍的做法是手动登录微博网页版在浏览器 DevTools 的 Network 面板里把请求头中的 Cookie 完整复制出来填到配置区里。Cookie 失效的表现很有规律连续请求总是返回 401 或者返回的 data 为空。需要注意Cookie 里SUB和SUBP这两个字段是微博的登录态凭证失效后整个 Cookie 都要重新替换不能只改其中一段。还有一种容易被忽略的情况是同一个 Cookie 并发用比如你本地跑程序的同时浏览器还挂着微博在高频刷新下触发风控登录态会被强制过期。我一般会在采集函数外面套一层异常捕获专门拦截 Cookie 失效的响应码def safe_fetch(weibo_id, cookie_value, max_pages3): try: comments fetch_comment(weibo_id, cookie_value, max_pages) if not comments: print(返回为空优先排查 Cookie 是否失效或接口字段名是否变动) return comments except requests.exceptions.JSONDecodeError: print(接口返回非 JSON大概率被风控拦截等待后重试) time.sleep(30) except Exception as e: print(其他异常:, repr(e)) return []这段代码的用意是把异常和业务逻辑隔离。JSONDecodeError 是非常信号化的错误——正常接口永远返回 JSON一旦返回了 HTML 文本基本可以断定是风控页面。遇到这种情况不要再循环重试直接把 Cookie 换掉或者歇 30 秒以上再继续。3. 中文分词与停用词过滤Jieba 的精确模式与自定义词典3.1 为什么精确模式是情感分析的默认选拿到文本之后第一道工序是分词。英文按空格切中文没有天然分隔符分词质量直接决定后续情感判定的准确率。这里用 Jieba 的精确模式它针对情感分析场景比全模式更合适——全模式会把「中华人民共和国」切成一堆词引入大量无意义片段精确模式则把长词优先保留减少信息碎片。Jieba 精确模式底层用的是前缀词典加载加动态规划计算最大概率路径日常使用不需要理解全部细节但有几个参数值得注意。cut_allFalse就是不使用全模式HMMTrue控制是否启用隐马尔可夫模型来识别新词比如新的网络流行语。毕业设计里的评论文本通常夹杂网络用语建议把 HMM 保持开启。import jieba import re STOPWORDS_PATH stopwords.txt def clean_and_segment(text): # 去掉 URL、用户、#话题#只保留中文和基础标点 text re.sub(rhttps?://\S, , text) text re.sub(r[\u4e00-\u9fa5\w][:]?, , text) text re.sub(r#.*?#, , text) # 注意感叹号和问号是情感强信号先保留分词后再做停用词过滤时按需处理 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9!?。], , text) stopwords set() with open(STOPWORDS_PATH, r, encodingutf-8) as f: for line in f: word line.strip() if word: stopwords.add(word) words jieba.lcut(text, cut_allFalse, HMMTrue) # 过滤长度为 1 的词与停用词但保留「不」「没」这类否定词需要谨慎 meaningful [] for w in words: if w.strip() and w not in stopwords and len(w) 1: meaningful.append(w) return meaningful这里有个容易糊弄过去的细节长度等于 1 的词在中文里大量是语气词和代词过滤掉没问题但「不」「没」「别」这类否定词一旦被丢掉整个句子的情感就会反过来。这段代码里 len(w) 1 会把这些词误杀所以真正的做法是在停用词表里单独维护一个白名单让否定词和程度副词保留。后面讲情感判定时会看到否定词翻转是影响准确率的一大变量。3.2 停用词表的构建和自定义词典的加载时机停用词表决定了分词后哪些噪声会被剔除。网上有公开版的中文停用词表但直接拿来用会有一个尖锐问题——舆情分析领域的高频词汇比如「转发」「点赞」「微博」「评论」这些平台功能词在通用停用词表里是没有的它们会大量进入情感计数器稀释真实情感信号。正确的做法是基础停用词表加上领域补充表。系统在初始化时把它们合并成一个集合而不是两个列表拼接这样查重的时间复杂度是 O(1)。自定义词典需要在分词之前加载而且要在 Jieba 第一次被调用前加载否则后续加载的新词不生效。这句话是血泪经验——Jieba 的词典是懒加载的首次调用后部分计算会被缓存。def init_jieba(user_dict_path): # 每次运行都强制重新加载避免上次运行时生成的缓存词典影响本次结果 jieba.initialize() # 加载用户词典每行格式词语 词频 词性 # 例yyds 100000 nz if user_dict_path: jieba.load_userdict(user_dict_path) user_dict user_dict.txt init_jieba(user_dict)用户词典格式很关键每行三个字段分别是词、词频可选、词性可选。词频建议大致填 100000 左右这表示这个词在语料中的出现权重。不填词频时Jieba 会按默认频率处理网络新词容易被切成片段比如「yyds」可能被拆成「y」「yds」。填了词频和词性如 nz 表示其他专名之后就能作为整体被识别保留。有个细节顺带说一句Jieba 默认词典里包含很多生僻词和低频词在情感分析里是会干扰的。比如把人名、地名切成独立词后它们既不是情感词也不是否定词但会占据后续 TF 统计的比例影响权重归一化。不过对毕业设计这个体量来说只要停用词表和自定义词典配合得当影响基本可控不必追求极端清洗。4. 情感判定核心模块词典权重、否定翻转与程度副词调节4.1 情感词典的冷启动与权重映射情感分析大致分两类基于机器学习模型的和基于情感词典的。前者需要标注好的训练集对毕业设计来说数据量往往不够后者是规则驱动透明可解释答辩时能讲清楚每一层逻辑。这套系统走词典路线采用「基础情感词典 领域词扩展 程度副词 否定词」的复合结构。基础词典从大连理工大学的中文情感词汇本体库转出来这份资源是学术界常用的开放词典提供了词语的褒贬倾向和强度等级。转成系统可用格式时通常做法是把每个情感词映射成三个权重档位强正向 2、弱正向 1、强负向 -2、弱负向 -1。中性词不进词典靠后续的得分阈值把人分到中性类。标定强度的逻辑不复杂核心是把七级情感分类压缩成三档sentiment_dict {} def load_sentiment_dict(path): global sentiment_dict # 文件格式词语,情感类别,强度 # 情感类别pos 正 / neg 负 # 强度1 弱 / 2 强 with open(path, r, encodingutf-8) as f: for line in f: parts line.strip().split(,) if len(parts) 3: continue word, category, intensity parts[:3] score 0 if category pos: score 1 if intensity 1 else 2 elif category neg: score -1 if intensity 1 else -2 sentiment_dict[word] score def get_base_score(word): return sentiment_dict.get(word, 0.0)这段代码暴露了一个优化空间同一个词在不同语境下褒贬完全不同的情况并不少见但词典法解决不了这种多义词问题。对这个系统的定位来说可以接受——毕业设计追求的是整体准确率和可讲清楚的逻辑不是 SOTA。如果后面想优化可以在词典里加入词性消歧逻辑比如「骄傲」修饰人时偏向贬义修饰成绩时偏向褒义这需要大量语料做条件概率统计性价比不高建议放到展望部分提一下即可。4.2 否定翻转和程度副词一句话情感准确率的分水岭词典打分本身不难难的是上下文修饰。「这部电影不怎么样」——影评级来看「怎么样」若收入词典且是偏正倾向直接求和就是正分而「不」字被过滤后全面翻车。所以系统里一定要做否定词检测和程度副词调节。否定词处理逻辑是经典的「窗口翻转」往前扫描若干词如果出现「不、没、无、别、莫、非、难、未」等词汇这个情感词的得分就取反。注意「难」这个字很特殊——「难受」「难过」里的「难」是情感词的一部分不能当否定词处理所以否定词匹配时建议用「整词匹配 前面是独立词」的模式或者直接把这些词从分词阶段就做特别标记。NEGATIVE_WORDS {不, 没, 无, 别, 莫, 非, 未, 难, 甭} BOOSTER_WORDS { 很: 1.5, 非常: 2.0, 极其: 2.5, 太: 1.8, 有点: 0.8, 比较: 1.2, 稍微: 0.7, 超: 1.8 } def sentence_score(segment_list): score 0.0 length len(segment_list) for i, word in enumerate(segment_list): base get_base_score(word) if base 0: continue # 窗口大小设为 2即只检查当前词往前两个词范围内有没有否定词 neg_factor 1.0 for j in range(max(0, i - 2), i): if segment_list[j] in NEGATIVE_WORDS: neg_factor * -1 break # 程度副词从当前词往前找 3 个词内的第一个程度副词 boost_factor 1.0 for j in range(max(0, i - 3), i): if segment_list[j] in BOOSTER_WORDS: boost_factor * BOOSTER_WORDS[segment_list[j]] break score base * neg_factor * boost_factor return score窗口大小的设定是这里最值得琢磨的工程参数。窗口太短比如 1跨两三个词修饰情形的否定词检测不到窗口太长比如 5会把无关句子成分卷进来比如「我不是说电影不好看」——「不」在「是」后面窗口内检测到「不」但是「不好看」里的「不」确实是应该翻转的这里逻辑恰好成立。但「我不认为这部电影好看」这种句式「不」与「好看」相距 4 个词窗口 2 检测不到这也是词典法的一个已知局限。实测下来2 词的否定窗口和 3 词的程度副词窗口在短评场景下表现相对稳定。微博评论普遍短口语化严重长距离修饰很少见窗口设太大会引入噪声。这个参数值得多跑几组对比数据是调优空间最大的位置之一。4.3 加权聚合点赞数不该被忽略简单的系统对每条评论算完分就完事了但这套系统做了一个值得在答辩时强调的设计按评论点赞数做加权。原因很直白高赞评论代表更多人的态度倾向在整体情感分布里应该有更高的话语权而低赞或零赞评论可能是沉默的小众观点权重降下来反而能减少噪音。加权策略实现上不能直接拿点赞数当权重——点赞数从 0 到几千都有直接乘会把分数拉爆所以要做一个映射。常见做法是log1p压缩把点赞数的量级压到 0 到几的区间再参与聚合import math def aggregate_sentiment(comment_list): total_pos 0.0 total_neg 0.0 total_weight 0.0 for comment in comment_list: base sentence_score(comment[segment_list]) like_count comment.get(like_count, 0) weight math.log1p(like_count 1) total_weight weight if base 0: total_pos base * weight elif base 0: total_neg abs(base) * weight if total_weight 0: return {pos: 0.0, neg: 0.0, neutral: 0.0} # 归一化成百分比展示中性由总评论数减去正负向得到 pos_ratio total_pos / (total_pos total_neg) * 100 neg_ratio total_neg / (total_pos total_neg) * 100 neutral_ratio 100 - pos_ratio - neg_ratio return { pos: round(pos_ratio, 2), neg: round(neg_ratio, 2), neutral: round(neutral_ratio, 2) }log1p是log(1x)的数值稳定版本x 接近 0 时不会出现log(0)的无穷值比直接math.log安全得多。点赞数为 0 的评论权重是log1p(1)≈0.693并不是 0——这个细节说明零赞评论也有基础表达权不会被完全忽略。加权之后还有一个值得观察的指标正负向占比与总评论数的交叉分析。比如某条微博总体是正面的但高赞评论里有很多负向吐槽说明舆论存在分层。这个现象在答辩时拿出来讲比单纯报数字更有说服力。5. 避坑排查手册微博接口变动、分词乱切与结果漂移5.1 接口返回结构变了代码立刻崩溃现象昨天还能正常跑通的采集程序今天突然报 KeyError 或者 data 为空。 原因微博移动端接口偶尔会调整字段结构。经历过data从评论数组变成「评论数组 分页信息」的嵌套结构也见过max_id改名的版本。接口不是契约文件不通知你的。 解决第一反应看响应原文。程序不好改先手动curl一下接口看返回写一个接口字段探测函数把返回结果的关键字段名打印出来再适配。不要盲猜不要对着旧文档改要以当天实际返回为准。5.2 分词把整句话切成碎渣现象分词结果里出现大量单字比如「这电影真好看」被切成「这 / 电影 / 真 / 好 / 看」。 原因两种可能。一是自定义词典没加载成功jieba.load_userdict传了不存在的路径静默失败二是停用词表把「好看」这类词拆进了停用集合过滤时干掉了半个词。 解决先确认词典路径是否存在再在加载后打印jieba.get_dict_file()看看有没有生效。最笨但最有效的办法是把每条评论的分词结果和情感打分一起写进日志随机抽几十条肉眼扫一遍分词质量立刻见分晓。5.3 情感结果永远是正向的现象负面吐槽评论都被算成正向或者所有分数都在 0 以上。 原因大概率是否定词处理被绕过了。「不怎么样」里的「怎么样」如果不在情感词典里得分本来就是 0句子最终就落到中性——这不算严重。严重的是「不是很差」这种双重否定窗口 2 检测到「很」前面的「不」和第二个「不」负负得正逻辑没处理好结果出现漂移。 解决把双重否定单独拉出来做测试用例。写三条常识句给普通句子、含一个否定词的句子、含双重否定的句子跑完对比预期。双重否定在微博文本里很少见支持基本否定翻转已经够用但测试用例文档要保留——答辩评委很可能问这句话。5.4 采集速度一快就会被封现象抓到几百条评论后后续请求全部返回 401 或滑块验证页面。 原因请求频次太高。微博的风控不是线性触发而是滞后惩罚——前面高频抓几分钟可能没事但风控触发后你歇几秒再试还是被封。 解决把请求间隔加长到 24 秒并且每次批量采集前随机选一条无关页面先访问预热 cookie。与其连续跑半小时再被封不如分段跑采集 → 停顿 → 换一批微博 ID → 继续采集每次都伪造一点浏览行为。5.5 停用词表把情感词误杀了现象分词结果里「精彩」「喜欢」这些词消失了情感分析结果全面飘中性。 原因网上找的通用中文停用词表里有「喜」「好看」这类词汇。比如「精彩」没有被收录但「彩」是单字「喜欢」如果被某个广泛流传的停用词表收录就会直接从情感计数里消失。 解决在加载停用词表后加一行防御逻辑——把sentiment_dict的 key 和停用词集合做交集找出同时出现在两边的情感词打印警告并强制从停用词集合中移除。这个防御能拦截大部分任性的停用词表。6. 结果可视化与答辩展示Pyecharts 图表和演示闭环6.1 情感占比饼图和趋势折线的组合呈现最后一环是把分析结果呈现出来。整套系统选择了 Pyecharts 而不是 Matplotlib关键原因是 Pyecharts 生成的是 HTML 页面浏览器直接打开就能看交互性比静态图强得多。答辩时评委眼神落在动态图表上记忆力远高于静态曲线。做图表时有一个细节值得注意把每条评论的情感值按时间聚合生成一条情感走势曲线比单纯一个饼图更有深度。它能把「愤怒事件点」凸显出来——某天突然出现一个负向高峰背后往往对应一条热点事件。from pyecharts.charts import Pie, Line from pyecharts import options as opts from collections import defaultdict from datetime import datetime def build_charts(comment_list): # 先按每条评论的绝对情感分做类别映射 pos_count sum(1 for c in comment_list if c[sentiment_score] 0.3) neg_count sum(1 for c in comment_list if c[sentiment_score] -0.3) neutral_count len(comment_list) - pos_count - neg_count pie ( Pie() .add(情感分布, [list(x) for x in [ (正面, pos_count), (负面, neg_count), (中性, neutral_count)]]) .set_global_opts(title_optsopts.TitleOpts(title微博评论情感分布)) .set_series_opts(label_optsopts.LabelOpts(formatter{b}: {c} ({d}%))) ) daily_scores defaultdict(list) for c in comment_list: day datetime.fromtimestamp(c[created_at]).strftime(%Y-%m-%d) daily_scores[day].append(c[sentiment_score]) sorted_days sorted(daily_scores.keys()) daily_mean [sum(daily_scores[d]) / len(daily_scores[d]) for d in sorted_days] line ( Line() .add_xaxis(sorted_days) .add_yaxis(日均情感分, daily_mean, is_smoothTrue, markpoint_optsopts.MarkPointOpts( data[opts.MarkPointItem(type_max), opts.MarkPointItem(type_min)])) .set_global_opts(title_optsopts.TitleOpts(title情感分时间走势)) ) return pie, line这里注意.3这个阈值的选择。它不是一个数学推导出来的值而是来自经验短评的绝对情感分通常在 02 之间滑动0.3 之内可以看作情绪不明显的中性。这个阈值会影响最终分布的形态建议跑完数据后微调调到一个让三种类别分布看起来有区分度又不过分极端的值。答辩时能说清楚怎么选的阈值比直接说默认值得分更高。6.2 高频情感词词云的验证作用词云不是一个必要的图表但它是审核分析质量的好工具。把正向情感词和负向情感词分开生成两张词云肉眼扫一遍能发现很多算法层面的遗漏。比如负向词云里大量出现「无语」而情感词典里没有这个词它的得分就一直被算到中性——这个现象直接暴露词典覆盖不足。补词典的路子有两个一是用 jieba 分词后统计评论文本中的高频词人工标注一批情感词补充进自定义词典二是从已有的几个大情感词库合并词汇表去重后做人工审核。对毕业设计来说第一种方法效果更好因为语料就是你的微博数据针对性最强。词云生成用 WordCloud 库字体必须指定中文字体路径否则中文全部乱码成方框。最常见的坑是 Windows 上simhei.ttf路径因系统版本不同而千差万别建议直接引用一份自带的中文字体文件放在项目目录里避免答辩现场换台电脑就崩掉。6.3 系统演示的两个顺序原则答辩现场演示有一套不容易透露的排序逻辑。先跑可视化页面再展示数据表最后才现场演示爬虫采集——这个顺序是反着来的跟直觉相反。原因有二可视化和数据表是确定性的只要代码没改图像就在那里不会因为网络波动而翻车而现场爬虫采集是不确定性最高的环节Cookie 可能刚好失效、接口可能刚好变动、网络可能刚好卡顿一旦现场爬不动整场演示的节奏就垮掉了。如果评委要求看爬虫就展示事先采集好的数据文件把采集代码放出来讲逻辑即可。现场演示爬虫成功的概率大概只有六成别拿这个赌答辩。另外把所有图表保存成独立 HTML 文件放在 U 盘里哪怕项目环境全崩了浏览器打开 HTML 也不会丢人。从那以后我每次构建这套系统都会强制走一遍手工测试流程把「分词日志 → 情感打分日志 → 图表输出 → 数据表」四条检查线全部过完才收工。这一套流程看起来很笨但它救过我好几次——有一回情感词典找错了版本所有以「不」字开头的词全部成了负向基准正是这份手工日志把我从答辩前夜的数据灾难里捞了回来。希望这篇拆解能帮你在复现这套系统时少走几条弯路。本文还有配套的精品资源点击获取