简介:这份资源是面向计算机相关专业学生与项目实战学习者的微博舆情分析系统毕业设计完整源码包,采用Python+Django+Vue3技术栈,配套爬虫代码与数据源,数据库使用MySQL,适合做大作业、毕业设计或需要舆情分析项目练手的读者。压缩包共约2000个文件,整体约547.61MB,其中1797个jpg与16个png为界面截图和图表素材,75个py与34个pyc构成后端与爬虫逻辑,28个vue负责前端页面,另有12个csv、4个sql、7个json等数据与配置文件,以及2个pth模型文件,目录结构清晰便于按模块查阅。该资源经导师指导并评审通过,源码均本地编译运行并严格调试,已有127人学习下载。读者可获得可运行的完整项目、爬虫与数据源、数据库脚本及文档说明,便于快速理解舆情采集、话题分析与可视化展示的实现思路,也可作为二次开发与答辩准备的参考。
1. 从一条热搜到一套能跑的系统:微博舆情分析到底在做什么
刷到“微博舆情分析系统”这个毕业设计题目时,很多人第一反应是去搜现成源码,结果下载下来发现跑不起来——数据库连不上、依赖版本对不上、爬虫一跑就封 IP。我带过几届学生的毕设,也帮朋友改过类似项目,血泪经验是:能跑通的系统从来不是靠抄源码,而是靠把数据流、存储、分析、展示这四段拆开,每段单独验证。这套系统要解决的核心问题很具体:给定一批微博文本(公开话题下的帖子、评论),自动判断情绪倾向、提取高频关键词、按时间画出热度曲线,最后用一个 Web 界面把结果呈现出来。适合谁?适合正在做计算机毕业设计、需要一套完整可演示项目的人,也适合刚学完 Python 想找个真实场景练爬虫和文本分析的新手。它不涉及任何敏感数据获取,所有数据都来自公开可访问的页面,分析逻辑也完全基于词频和情感词典,不碰用户隐私。下面我从环境搭建讲到部署排错,每一步都给出可复现的命令和参数。
2. 环境准备与数据采集:把微博公开数据变成结构化 CSV
2.1 为什么选 requests + BeautifulSoup 而不是 Scrapy
毕业设计项目最怕“过度工程”。Scrapy 适合大规模分布式采集,但配置复杂、调试成本高,一个异步管道报错能卡你两天。我一般会建议用requests加BeautifulSoup做单机采集,配合time.sleep控制频率,代码量少、逻辑透明,答辩时也好讲清楚每一行在干什么。Python 版本选 3.9 或 3.10,太新的 3.12 在某些 Windows 环境下装lxml会报编译错误。安装依赖直接用 pip:
pip install requests==2.31.0 beautifulsoup4==4.12.2 lxml==4.9.3 pandas==2.0.3 jieba==0.42.1 snownlp==0.12.3 flask==2.3.2这里锁版本是为了避免“昨天还能跑今天报错”的玄学问题。snownlp是中文情感分析库,虽然精度一般,但胜在轻量、无需训练,适合毕设演示。jieba做分词,flask做后端接口。
2.2 采集脚本:翻页逻辑与反爬规避
微博公开话题页的结构经常变,但核心思路不变:找到包含帖子文本的 DOM 节点,提取时间、内容、点赞数,然后翻页。下面是一个最小可运行示例,采集某个公开话题的前 5 页:
import requests from bs4 import BeautifulSoup import time import csv import random HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36", "Accept-Language": "zh-CN,zh;q=0.9", } def fetch_page(url): try: resp = requests.get(url, headers=HEADERS, timeout=10) resp.raise_for_status() return resp.text except requests.RequestException as e: print(f"请求失败: {e}") return None def parse_page(html): soup = BeautifulSoup(html, "lxml") items = [] # 注意:微博页面 class 名会变,需根据实际页面调整 for card in soup.select("div.card-wrap"): content = card.select_one("p.txt") time_tag = card.select_one("p.from a") if content and time_tag: items.append({ "content": content.get_text(strip=True), "time": time_tag.get_text(strip=True), }) return items def save_to_csv(rows, filename="weibo_data.csv"): with open(filename, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=["content", "time"]) writer.writeheader() writer.writerows(rows) if __name__ == "__main__": all_rows = [] base_url = "https://s.weibo.com/weibo?q=%23话题名称%23&page={}" for page in range(1, 6): url = base_url.format(page) html = fetch_page(url) if html: rows = parse_page(html) all_rows.extend(rows) print(f"第 {page} 页采集到 {len(rows)} 条") time.sleep(random.uniform(2, 5)) # 随机间隔,降低被封风险 save_to_csv(all_rows) print(f"总计 {len(all_rows)} 条,已保存")逻辑说明:fetch_page负责发请求并处理异常,parse_page用 CSS 选择器定位帖子卡片,save_to_csv用utf-8-sig编码防止 Excel 打开乱码。参数方面,timeout=10避免卡死,random.uniform(2,5)让间隔不固定,比固定sleep(3)更自然。注意:微博未登录状态下只能看前几页,这是平台限制,不是代码问题。如果采集量不够,可以换用公开的微博数据集(如 NLPCC 提供的情感分析语料),但毕设演示用几百条公开数据足够了。
提示:采集前先手动打开目标页面,按 F12 检查
div.card-wrap是否存在,不同时期 class 名可能不同,需要同步修改选择器。
3. 文本清洗与情感分析:让 jieba 和 snownlp 跑出可解释的结果
3.1 清洗规则:去噪比分词更重要
原始微博文本里混着大量噪声:@用户名、话题标签、表情符号、URL、重复转发内容。如果不洗,分词结果里全是“转发”“微博”“网页链接”这类无意义词,情感分析也会被带偏。我一般写一个clean_text函数,按顺序做五件事:去 URL、去 @、去话题标签、去表情符号、去多余空格。
import re def clean_text(text): # 去除 URL text = re.sub(r"http[s]?://\S+", "", text) # 去除 @用户名 text = re.sub(r"@[\w\u4e00-\u9fa5]+", "", text) # 去除 #话题# text = re.sub(r"#.*?#", "", text) # 去除表情符号(常见 Unicode 范围) text = re.sub(r"[\U00010000-\U0010ffff]", "", text) # 去除多余空白 text = re.sub(r"\s+", " ", text).strip() return text参数说明:\U00010000-\U0010ffff覆盖了大部分 emoji,\s+把连续空格、换行合并成一个空格。清洗后文本长度如果小于 5 个字符,建议直接丢弃,因为太短没有分析价值。
3.2 情感分析:snownlp 的阈值怎么定
snownlp返回 0 到 1 之间的情感分数,越接近 1 越积极,越接近 0 越消极。但直接按 0.5 分正负太粗糙,实际数据里大量中性文本会落在 0.4 到 0.6 之间。我的经验是设两个阈值:小于 0.3 判消极,大于 0.7 判积极,中间判中性。这样分类结果更符合人工直觉,答辩时也好解释。
from snownlp import SnowNLP def analyze_sentiment(text): if not text or len(text) < 5: return "中性", 0.5 score = SnowNLP(text).sentiments if score < 0.3: return "消极", score elif score > 0.7: return "积极", score else: return "中性", score逻辑说明:SnowNLP(text).sentiments内部基于朴素贝叶斯模型,对短文本效果尚可,对反讽、双重否定容易翻车。比如“呵呵,真是太好了”会被判积极,这是已知局限,毕设里可以在文档中说明,不必强行解决。参数方面,阈值 0.3 和 0.7 可以根据你的数据集微调,如果积极文本明显偏少,就把上限降到 0.65。
3.3 关键词提取:TF-IDF 与 TextRank 怎么选
jieba.analyse提供两种关键词提取方式:extract_tags基于 TF-IDF,textrank基于 TextRank。TF-IDF 依赖全局语料,适合有大量背景文本的场景;TextRank 只依赖单篇文本,适合短文本。微博帖子普遍短,我一般用 TextRank,取 top 20 个词,并过滤掉停用词。
import jieba.analyse STOPWORDS = set(["的", "了", "在", "是", "我", "有", "和", "就", "不", "人", "都", "一", "一个", "上", "也", "很", "到", "说", "要", "去", "你", "会", "着", "没有", "看", "好", "自己", "这"]) def extract_keywords(text, topK=20): words = jieba.analyse.textrank(text, topK=topK, withWeight=False) return [w for w in words if w not in STOPWORDS and len(w) > 1]参数说明:topK=20是经验值,太多会混入噪声,太少覆盖不全。len(w) > 1过滤单字词,因为单字词在中文里歧义太大。停用词表可以按你的数据补充,比如“转发”“微博”这类平台词一定要加进去。
4. 存储与可视化:用 Flask + ECharts 把分析结果端到端串起来
4.1 SQLite 表结构设计:三张表就够了
毕设项目不需要上 MySQL,SQLite 零配置、单文件、方便打包。建三张表:weibo存原始数据,sentiment存情感分析结果,keyword存关键词。用sqlite3标准库操作,不引入 ORM,代码更直观。
import sqlite3 def init_db(db_path="weibo.db"): conn = sqlite3.connect(db_path) cursor = conn.cursor() cursor.execute(""" CREATE TABLE IF NOT EXISTS weibo ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, publish_time TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) cursor.execute(""" CREATE TABLE IF NOT EXISTS sentiment ( id INTEGER PRIMARY KEY AUTOINCREMENT, weibo_id INTEGER, label TEXT, score REAL, FOREIGN KEY (weibo_id) REFERENCES weibo(id) ) """) cursor.execute(""" CREATE TABLE IF NOT EXISTS keyword ( id INTEGER PRIMARY KEY AUTOINCREMENT, word TEXT, weight REAL, weibo_id INTEGER, FOREIGN KEY (weibo_id) REFERENCES weibo(id) ) """) conn.commit() conn.close()逻辑说明:weibo_id作为外键关联三张表,查询时用 JOIN 就能拿到一条微博的完整分析结果。created_at记录入库时间,方便排查数据是哪天跑的。参数方面,SQLite 默认并发能力弱,但毕设演示单用户访问完全够用。
4.2 Flask 接口:两个路由返回 JSON
前端用 ECharts 画图,后端只需要提供两个接口:/api/sentiment_dist返回积极、中性、消极的数量,/api/keyword_top返回 top 10 关键词及权重。
from flask import Flask, jsonify import sqlite3 app = Flask(__name__) def query_db(sql): conn = sqlite3.connect("weibo.db") cursor = conn.cursor() cursor.execute(sql) rows = cursor.fetchall() conn.close() return rows @app.route("/api/sentiment_dist") def sentiment_dist(): rows = query_db("SELECT label, COUNT(*) FROM sentiment GROUP BY label") return jsonify({label: count for label, count in rows}) @app.route("/api/keyword_top") def keyword_top(): rows = query_db("SELECT word, SUM(weight) as total FROM keyword GROUP BY word ORDER BY total DESC LIMIT 10") return jsonify([{"word": w, "weight": round(t, 2)} for w, t in rows]) if __name__ == "__main__": app.run(debug=True, host="0.0.0.0", port=5000)逻辑说明:query_db封装了连接和关闭,避免每次写重复代码。sentiment_dist用GROUP BY统计三类情感数量,keyword_top用SUM(weight)聚合同一关键词在不同微博中的权重。参数方面,host="0.0.0.0"让局域网内其他设备也能访问,方便答辩时用手机演示。debug=True仅限开发阶段,部署时要关掉。
4.3 ECharts 前端:饼图加柱状图,十分钟搞定
前端不需要框架,一个 HTML 文件引入 ECharts CDN 即可。饼图展示情感分布,柱状图展示关键词权重。数据通过fetch从 Flask 接口获取。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>微博舆情分析看板</title> <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script> </head> <body> <div id="pie" style="width: 600px; height: 400px;"></div> <div id="bar" style="width: 600px; height: 400px;"></div> <script> fetch('/api/sentiment_dist').then(r => r.json()).then(data => { const chart = echarts.init(document.getElementById('pie')); chart.setOption({ title: { text: '情感分布' }, series: [{ type: 'pie', data: Object.entries(data).map(([name, value]) => ({ name, value })) }] }); }); fetch('/api/keyword_top').then(r => r.json()).then(data => { const chart = echarts.init(document.getElementById('bar')); chart.setOption({ title: { text: '关键词 Top 10' }, xAxis: { type: 'category', data: data.map(d => d.word) }, yAxis: { type: 'value' }, series: [{ type: 'bar', data: data.map(d => d.weight) }] }); }); </script> </body> </html>逻辑说明:fetch拿到 JSON 后直接映射成 ECharts 需要的格式,饼图用Object.entries把对象转成数组,柱状图用map提取字段。参数方面,echarts@5.4.3是稳定版本,CDN 加载慢的话可以下载到本地。图表容器宽高必须显式设置,否则 ECharts 不渲染。
5. 避坑与排查:那些让系统跑不起来的真实原因
5.1 采集返回空列表,但浏览器能打开页面
现象:requests.get返回 200,但BeautifulSoup解析出来 0 条数据。原因:微博对未登录请求返回的是简化版页面,DOM 结构和登录后不同,或者触发了验证码跳转。解决:先打印resp.text[:500]看实际返回内容,如果是登录页或验证页,就换用公开数据集,或者手动保存几页 HTML 再解析。不要试图绕过平台验证机制,毕设用公开数据完全合规。
5.2 snownlp 安装后导入报错“No module named”
现象:pip install snownlp成功,但from snownlp import SnowNLP报错。原因:Python 环境混乱,pip 装到了系统环境而 IDE 用的是虚拟环境。解决:在项目目录下执行python -m venv venv创建虚拟环境,激活后重新安装。Windows 用venv\Scripts\activate,Mac/Linux 用source venv/bin/activate。装完用pip list确认 snownlp 在列表里。
5.3 中文分词结果全是单字
现象:jieba.cut出来的词都是“我”“的”“了”这种单字。原因:没有加载自定义词典,或者文本被清洗成了无空格的长串。解决:先确认清洗后的文本保留了正常标点和空格,然后加载一个自定义词典补充领域词汇。jieba.load_userdict("userdict.txt"),文件里每行一个词,格式为“词 词频 词性”,词频可省略。
5.4 Flask 接口跨域报错 CORS
现象:前端 HTML 直接打开(file://协议)请求 Flask 接口,浏览器控制台报 CORS 错误。原因:file://和http://localhost:5000不同源。解决:把 HTML 文件放到 Flask 的static目录下,通过http://localhost:5000/static/index.html访问,或者安装flask-cors并配置CORS(app)。最简单的方法是前者,不用改代码。
5.5 SQLite 数据库被锁
现象:采集脚本还在写数据时,Flask 查询报“database is locked”。原因:SQLite 默认写操作会锁库,并发读写冲突。解决:采集和查询分开跑,先跑完采集再启动 Flask;或者在连接时设置timeout=10,让写操作等待而不是立即失败。sqlite3.connect("weibo.db", timeout=10)就能缓解。
6. 让系统更耐看:增量采集与情感阈值调优的两个技巧
答辩时老师最爱问两个问题:“数据量够不够?”和“情感分析准不准?”这两个问题其实可以用同一个思路解决:增量采集 + 动态阈值。增量采集的意思是每次只采集最新几页,按发布时间去重后追加到数据库,这样系统可以持续运行,数据量随时间自然增长。实现上给weibo表加一个publish_time唯一索引,插入时用INSERT OR IGNORE,重复数据自动跳过。
cursor.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_time ON weibo(publish_time)") cursor.execute("INSERT OR IGNORE INTO weibo (content, publish_time) VALUES (?, ?)", (content, pub_time))情感阈值调优则更取巧:先人工标注 50 条数据作为验证集,然后写一个循环,把积极阈值从 0.6 到 0.8 以 0.05 步长遍历,看哪个阈值下分类准确率最高。下面这段代码可以直接跑:
def tune_threshold(texts, labels): best_acc, best_th = 0, 0.7 for th in [0.6, 0.65, 0.7, 0.75, 0.8]: correct = 0 for text, true_label in zip(texts, labels): score = SnowNLP(text).sentiments pred = "积极" if score > th else ("消极" if score < 1 - th else "中性") if pred == true_label: correct += 1 acc = correct / len(texts) if acc > best_acc: best_acc, best_th = acc, th return best_th, best_acc参数说明:1 - th保证消极阈值和积极阈值对称,避免中性区间偏移。验证集 50 条是经验值,太少波动大,太多标注成本高。跑完把best_th写回analyze_sentiment函数即可。我自己的习惯是每次换数据集都重新跑一遍这个调优,因为不同话题的用词风格差异很大,固定阈值一定会翻车。希望这套流程能帮你把毕设稳稳跑通,少熬两个通宵。
本文还有配套的精品资源,点击获取