简介:这是一套面向高校计算机相关专业学生的微博情感分析系统毕业设计完整源码,适合作为毕业设计、期末大作业或课程设计参考,也方便零基础学习者上手实战。项目基于Python实现,围绕微博文本的采集、清洗与情感倾向判别展开,涵盖数据预处理、模型训练与可视化展示等核心环节,并配有前端页面用于结果呈现。压缩包共427个文件,约5.9MB,其中34个py文件承载情感分析与后端逻辑,151个js、58个html及大量css、scss、less文件构成前端交互与样式,另有ipynb笔记、csv数据集和json配置等辅助材料,目录结构清晰,便于按模块阅读与二次开发。该资源已获导师指导并通过答辩,代码完整、下载即可运行,已有759人学习下载。读者可据此掌握从数据到界面的完整实现思路,快速搭建自己的情感分析系统并完成论文与答辩准备。
1. 从一份微博情感分析系统压缩包说起:它到底能解决什么
毕业设计选题季,计算机相关专业里「基于 Python 的微博情感分析系统」几乎是每年都被翻牌子的题目。原因很直接:数据能爬、模型能训、界面能看,三件套凑齐就是一份完整可演示的作品。但真正动手的人会发现,坑不在「情感分析」这四个字上,而在数据从哪来、中文怎么切、模型怎么选、界面怎么串这条链路上。我见过太多人卡在爬虫被封、jieba 分词装不上、SnowNLP 跑出来全是 0.5 这类问题上,最后草草交差。
这份压缩包对应的,就是一条从微博文本采集、中文预处理、情感极性判定到可视化展示的完整链路。它适合两类人:一是时间紧、需要一份能跑通能答辩的毕业设计的同学;二是想借这个题目把 Python 爬虫、中文 NLP、Web 可视化串起来练一遍的入门者。下面我按自己实际做这类系统的顺序,把每个环节的选择理由、可抄的命令和参数、以及翻车点讲清楚,你照着改就能落地。
2. 微博数据从哪来:采集方案选型与最小可跑爬虫
2.1 三种数据来源的取舍
做情感分析,第一件事是确定语料来源。常见做法有三条路,各有边界。
第一条是公开数据集。像 ChnSentiCorp、weibo_senti_100k 这类中文情感语料,标注质量稳定,直接拿来训模型最省事。缺点是数据是静态的,跟「微博」这个实时场景脱节,答辩时老师一问「你的数据怎么来的」容易露怯。
第二条是移动端接口采集。微博移动端(m.weibo.cn)的接口返回 JSON,结构清晰,比 PC 端好解析。这是目前做课程设计最常用的路子,请求频率控制好基本够用。
第三条是网页解析。直接抓搜索页 HTML,用 XPath 或正则提取。这种方式最容易被反爬拦截,页面结构一变就全废,我不推荐作为主方案。
我的建议是:用移动端接口采集真实数据做演示,同时用公开数据集做模型训练的补充。这样既有「真实采集」的答辩亮点,又有足够的标注数据保证模型效果。
2.2 最小可跑的采集脚本
下面这段是采集的核心逻辑,用 requests 请求移动端接口,翻页拿数据,落到本地 JSON。注意请求头里必须带移动端 UA 和 Cookie,否则返回的是登录页。
import requests import json import time import random # 移动端接口,containerid 里的 100103type=1 表示综合搜索 BASE_URL = "https://m.weibo.cn/api/container/getIndex" HEADERS = { "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) " "AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", "Cookie": "你的Cookie,从浏览器开发者工具里复制", "X-Requested-With": "XMLHttpRequest", "Referer": "https://m.weibo.cn/", } def fetch_page(keyword, page): params = { "containerid": f"100103type=1&q={keyword}", "page_type": "searchall", "page": page, } resp = requests.get(BASE_URL, headers=HEADERS, params=params, timeout=10) if resp.status_code != 200: return [] data = resp.json() cards = data.get("data", {}).get("cards", []) results = [] for card in cards: # 搜索结果里 card_type=9 才是微博正文卡片 if card.get("card_type") != 9: continue mblog = card.get("mblog", {}) text = mblog.get("text", "") if text: results.append({ "id": mblog.get("id"), "text": text, "created_at": mblog.get("created_at"), "reposts": mblog.get("reposts_count", 0), "comments": mblog.get("comments_count", 0), }) return results def crawl(keyword, max_page=20): all_data = [] for page in range(1, max_page + 1): items = fetch_page(keyword, page) if not items: break all_data.extend(items) # 随机间隔,别用固定 sleep,固定间隔反而容易被识别 time.sleep(random.uniform(2, 5)) with open(f"weibo_{keyword}.json", "w", encoding="utf-8") as f: json.dump(all_data, f, ensure_ascii=False, indent=2) return all_data if __name__ == "__main__": crawl("人工智能", max_page=10)逻辑上分三层:fetch_page负责单页请求和字段提取,crawl负责翻页和落盘,time.sleep用随机区间控制节奏。参数上,containerid是接口的核心参数,100103type=1&q=关键词这个格式对应综合搜索;max_page控制采集量,毕设演示 10 到 20 页足够,大概几百到上千条。
2.3 采集阶段必须注意的三件事
第一,Cookie 会过期。一般几小时到一天就失效,脚本里要能捕获返回内容为空的情况并提示重新获取,别让它默默跑完返回零条。
第二,正文里带 HTML 标签。接口返回的text字段里有<a>、<span>这类标签,存下来之前要清洗,否则后面分词全是噪声。
第三,别贪多。采集频率过高会触发验证,宁可慢一点。我一般单次采集不超过 2000 条,分关键词多跑几轮。
3. 中文文本预处理:jieba 分词与情感词典的配合
3.1 为什么中文预处理比英文麻烦
英文按空格切词就行,中文不行。「人工智能」是一个词还是四个字,直接决定情感判定的准确度。所以中文情感分析的第一步永远是分词,而分词工具里 jieba 是绕不开的选择——成熟、文档全、pip 一条命令就能装。
但 jieba 默认词典对网络用语不友好。「yyds」「绝绝子」「破防了」这类微博高频表达,默认词典要么切错要么识别不出。解决办法是加载自定义词典,把网络热词和领域词补进去。
3.2 预处理完整流程与代码
预处理包含四步:去 HTML 标签、去表情符号、分词、去停用词。下面这段是可直接用的实现。
import re import jieba # 加载自定义词典,每行格式:词语 词频 词性(词频词性可省略) jieba.load_userdict("user_dict.txt") # 停用词表,一行一个词 with open("stopwords.txt", "r", encoding="utf-8") as f: STOPWORDS = set(line.strip() for line in f if line.strip()) def clean_text(text): # 去掉 HTML 标签 text = re.sub(r"<[^>]+>", "", text) # 去掉 URL text = re.sub(r"http\S+|www\.\S+", "", text) # 去掉 @用户 和 #话题# 的符号,保留文字 text = re.sub(r"@[\w\u4e00-\u9fa5-]+", "", text) text = re.sub(r"#([^#]+)#", r"\1", text) # 去掉表情符号(非中英文数字和常见标点) text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9,。!?、;:]", "", text) return text.strip() def segment(text): text = clean_text(text) words = jieba.lcut(text) # 过滤停用词和单字 words = [w for w in words if w not in STOPWORDS and len(w) > 1] return words if __name__ == "__main__": sample = "这款手机真的太香了,续航yyds!#数码测评# @小明" print(segment(sample)) # 输出大致为:['这款', '手机', '真的', '太香', '续航', 'yyds', '数码', '测评']clean_text里的正则顺序有讲究:先去标签再去 URL,最后去特殊符号。如果顺序反了,标签里的尖括号可能被当成特殊符号处理,留下残缺内容。segment里过滤单字是因为中文单字歧义太大,「好」「差」单独出现时情感指向不明确,容易引入噪声。
3.3 自定义词典和停用词表怎么准备
user_dict.txt里放微博高频词和领域词,比如「绝绝子」「破防」「种草」「拔草」「真香」。词频给个中等值(比如 1000)就行,词性可以省略。停用词表用哈工大停用词表打底,再补上「转发」「微博」「哈哈」这类对情感无贡献的词。
提示:停用词表不要照搬,一定要针对你的数据看一遍。我见过有人把「不」加进停用词,结果「不好」变成「好」,情感直接反转,这种错误排查起来很费时间。
4. 情感判定:SnowNLP、词典法和机器学习怎么选
4.1 三种方案的适用边界
情感判定是系统的核心。常见方案有三种,选错了要么效果差要么工作量爆炸。
SnowNLP 是开箱即用的中文情感库,SnowNLP(text).sentiments直接返回 0 到 1 的分数,大于 0.5 判为正面。优点是快,几行代码出结果;缺点是它基于电商评论训练,对微博口语化文本准确率一般,而且分数经常集中在 0.5 附近,区分度差。
词典法是自己维护正面词表和负面词表,统计文本里正负词数量做判定。优点是可控、可解释,答辩时能讲清楚每一条为什么这么判;缺点是要手工维护词典,覆盖不全。
机器学习法是用标注数据训一个分类器,比如朴素贝叶斯或 SVM,配合 TF-IDF 或词向量特征。优点是准确率高、有「技术含量」;缺点是需要标注数据,训练和调参要花时间。
我的建议是:用 SnowNLP 做基线快速跑通,用词典法做规则补充(比如处理否定词和程度副词),如果时间允许再上一个朴素贝叶斯模型做对比。这样答辩时你有三组结果可以对比分析,比单一方案有说服力。
4.2 SnowNLP 基线实现与分数校准
from snownlp import SnowNLP def snownlp_sentiment(text): if not text.strip(): return None score = SnowNLP(text).sentiments # 原始分数集中在 0.5 附近,做一次拉伸增强区分度 if score > 0.6: label = "正面" elif score < 0.4: label = "负面" else: label = "中性" return {"score": round(score, 4), "label": label} if __name__ == "__main__": for t in ["这个产品太棒了", "质量差得离谱", "还行吧一般般"]: print(t, snownlp_sentiment(t))这里的关键改动是把判定阈值从默认的 0.5 改成 0.6 和 0.4 双阈值,中间区间归为中性。因为 SnowNLP 对中性文本的分数本来就靠近 0.5,用单阈值会把大量中性文本误判成正面或负面。这个校准是我踩过坑之后加的,效果提升明显。
4.3 词典法补充否定和程度处理
SnowNLP 处理不了「不」+ 正面词这种反转。词典法可以补上这块。
POSITIVE = set(["好", "棒", "香", "喜欢", "满意", "推荐", "优秀"]) NEGATIVE = set(["差", "烂", "坑", "失望", "垃圾", "后悔", "难用"]) NEGATION = set(["不", "没", "无", "别", "非"]) DEGREE = {"非常": 2.0, "很": 1.5, "太": 1.8, "有点": 0.6, "稍微": 0.5} def dict_sentiment(words): score = 0.0 for i, w in enumerate(words): weight = 1.0 # 看前一个词是不是程度副词 if i > 0 and words[i - 1] in DEGREE: weight = DEGREE[words[i - 1]] # 看前一个词是不是否定词 negate = i > 0 and words[i - 1] in NEGATION if w in POSITIVE: score += weight * (-1 if negate else 1) elif w in NEGATIVE: score += weight * (1 if negate else -1) if score > 0: return "正面" elif score < 0: return "负面" return "中性"这段逻辑的核心是「看前一个词」:程度副词调整权重,否定词反转极性。DEGREE里的数值是我根据实际语料调的,你可以按自己的数据微调。注意否定词只往前看一个词,处理不了「不是不」这种双重否定,但微博文本里这种情况少,够用。
5. 可视化与系统集成:从数据到能演示的界面
5.1 可视化方案选型
毕设答辩需要「看得见」的东西。可视化有两条路:一是用 Flask 或 Django 搭 Web 界面,二是用 PyQt 做桌面应用。Web 方案更主流,部署方便,截图好看,我推荐 Flask。
图表部分,情感分布用饼图,情感随时间变化用折线图,高频词用词云。前端图表库用 ECharts,后端把数据整理成 JSON 传给前端即可。
5.2 Flask 接口与 ECharts 对接
后端提供一个返回统计数据的接口,前端用 ECharts 渲染。
from flask import Flask, jsonify, render_template import json from collections import Counter app = Flask(__name__) @app.route("/") def index(): return render_template("index.html") @app.route("/api/stats") def stats(): with open("weibo_result.json", "r", encoding="utf-8") as f: data = json.load(f) # 情感分布 labels = Counter(item["label"] for item in data) # 高频词 all_words = [] for item in data: all_words.extend(item["words"]) top_words = Counter(all_words).most_common(50) return jsonify({ "sentiment": dict(labels), "top_words": [{"name": w, "value": c} for w, c in top_words], }) if __name__ == "__main__": app.run(debug=True, port=5000)/api/stats把情感分布和高频词打包成 JSON,前端拿到后分别喂给饼图和词云。Counter做频次统计是最省事的写法,most_common(50)取前 50 个词做词云,太多会挤在一起看不清。
前端index.html里引入 ECharts,用fetch('/api/stats')拿数据,然后setOption渲染。这部分是标准前端操作,不展开,核心是把后端 JSON 结构对齐 ECharts 的series.data格式。
5.3 系统串联的完整数据流
整个系统的数据流是:采集脚本产出weibo_xxx.json→ 预处理脚本读入、分词、产出带words字段的中间文件 → 情感判定脚本读入、打标签、产出weibo_result.json→ Flask 读最终文件、提供接口 → 前端渲染。
建议把每一步写成独立脚本,用文件名串起来,而不是写成一个巨型脚本。这样调试时能单独跑某一步,出问题好定位。我一般会加一个run_all.py按顺序调用,但保留每个脚本的独立入口。
6. 避坑与排查:那些让我返工三次的问题
6.1 采集返回空数据
现象:脚本跑完,JSON 文件里是空列表,或者只有几条。
原因:九成是 Cookie 过期或 UA 不对。移动端接口对请求头敏感,UA 写成 PC 端的会直接返回登录页 HTML,resp.json()就报错。
解决:先手动用浏览器打开m.weibo.cn搜一个词,开发者工具里看请求头,把 UA 和 Cookie 原样复制。脚本里加一层判断,如果resp.text开头是<说明返回的是 HTML,直接抛异常提示重新获取 Cookie。
6.2 jieba 分词把网络词切碎
现象:「绝绝子」被切成「绝绝」「子」,「yyds」被切成单个字母。
原因:默认词典没有这些词。
解决:在user_dict.txt里加上,格式绝绝子 1000。英文缩写 jieba 默认不切,但会被后续的正则过滤掉,如果不想丢,在clean_text的正则里保留字母数字组合。
6.3 SnowNLP 分数全是 0.5
现象:跑出来一大批 0.5,正负不分。
原因:SnowNLP 对短文本和口语化文本的判定能力弱,尤其是没有明显情感词的句子。
解决:一是用双阈值把 0.4 到 0.6 归为中性,别硬分;二是对短文本(少于 5 个字)直接标中性;三是叠加词典法做二次判定,两者不一致时以词典法为准,因为词典法可解释。
6.4 Flask 端口被占用
现象:app.run报Address already in use。
原因:5000 端口被其他程序占了,macOS 上尤其常见(AirPlay 接收器默认占 5000)。
解决:换端口,app.run(port=5001)。或者查占用进程杀掉,但换端口最快。
6.5 中文显示成方块
现象:图表里中文全是方框。
原因:ECharts 默认字体不含中文,或者 matplotlib 没设中文字体。
解决:ECharts 在option里设textStyle: { fontFamily: 'Microsoft YaHei, sans-serif' };matplotlib 用plt.rcParams['font.sans-serif'] = ['SimHei']。这个坑不涉及逻辑,但截图时全是方块很难看,答辩前一定要检查。
7. 让结果更可信:模型对比与准确率验证的实操技巧
到这一步系统能跑了,但答辩时老师大概率会问「你的准确率多少,怎么验证的」。如果你只跑 SnowNLP 没做验证,这个问题会很难答。我的做法是手工标 200 条做测试集,然后跑三套方案对比。
具体操作:从采集的数据里随机抽 200 条,人工标注正面/负面/中性,存成test_labeled.csv,两列text,label。然后写一个评估脚本,分别用 SnowNLP、词典法、朴素贝叶斯跑一遍,算准确率和混淆矩阵。
import pandas as pd from sklearn.metrics import accuracy_score, classification_report from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.pipeline import Pipeline df = pd.read_csv("test_labeled.csv") # 方案一:SnowNLP df["snownlp_pred"] = df["text"].apply(lambda x: snownlp_sentiment(x)["label"]) # 方案二:词典法 df["dict_pred"] = df["text"].apply(lambda x: dict_sentiment(segment(x))) # 方案三:朴素贝叶斯,用训练集单独训,这里示意流程 train_df = pd.read_csv("train_labeled.csv") pipe = Pipeline([ ("tfidf", TfidfVectorizer(tokenizer=segment, token_pattern=None)), ("nb", MultinomialNB()), ]) pipe.fit(train_df["text"], train_df["label"]) df["nb_pred"] = pipe.predict(df["text"]) for col in ["snownlp_pred", "dict_pred", "nb_pred"]: print(col, "准确率:", accuracy_score(df["label"], df[col])) print(classification_report(df["label"], df[col]))TfidfVectorizer里tokenizer=segment复用我们自己的分词函数,token_pattern=None是必须的,否则 sklearn 会用默认的正则再切一遍,把中文切碎。这个参数不设,准确率会莫名其妙掉一大截,是个隐蔽的坑。
跑完你会得到三组数字。经验上,朴素贝叶斯在有足够训练数据时准确率最高,词典法次之但可解释性最好,SnowNLP 最方便但准确率通常垫底。答辩时把这三组结果做成表格,说明各自的取舍,比只报一个数字有说服力得多。
| 方案 | 准确率区间 | 优点 | 缺点 |
|---|---|---|---|
| SnowNLP | 0.60-0.70 | 开箱即用 | 区分度差,不可解释 |
| 词典法 | 0.70-0.80 | 可解释,可控 | 词典维护成本高 |
| 朴素贝叶斯 | 0.80-0.88 | 准确率高 | 需要标注数据 |
最后说个习惯:我做完这类系统一定会把「数据采集 → 预处理 → 判定 → 可视化」每一步的中间产物都存成文件,而不是在内存里一路传到底。看起来多占点磁盘,但任何一步出问题都能单独复现,改完重跑那一步就行,不用从头再来。这个习惯帮我省下的时间,远比多写的几行落盘代码多。希望帮到你。
本文还有配套的精品资源,点击获取