简介基于Python实现的微博数据挖掘与社交舆情分析系统源码包主要面向计算机、信息安全、大数据、人工智能等专业的学生与教师也适合企业开发者用于课程设计、期末大作业或毕设演示项目。压缩包共收录669个文件整体约4.63MB包含Python爬虫脚本、Scrapy配置、C代理与爬虫管理模块、前端JS/LESS/CSS页面、JSON数据以及Dockerfile、Shell脚本和Makefile覆盖数据采集、清洗、分析与可视化展示的完整闭环。目前已有508人学习下载作者已验证代码可稳定运行同时通过清晰的目录结构和配套部署脚本降低上手门槛便于本地或容器环境快速复现。项目内附带微博评论数据、代理管理和可视化界面各模块职责分明既可直接作为课程大作业提交也可在入门进阶后继续扩展衔接毕业设计或初期项目立项演示。1. 从一条微博到一份舆情报告这门课设到底在做什么选微博数据挖掘与社交舆情分析做课程大作业最常踩的坑是把它做成了一个“只会爬数据的爬虫”。交上去的源码能跑、能存 Excel但老师一问“这个事件的情感走势是什么”“哪些话题在发酵”就答不上来。这门课设真正要交付的是一套从采集、清洗、分析到可视化串起来的系统——输入一个关键词输出一份包含热度曲线、情感分布、话题聚类的舆情快报。基于 Python 实现这套系统核心不是爬虫写得有多花哨而是数据链路是否完整、分析结论是否可信。适合正在选题、想用一份源码兼顾“技术含量”和“答辩效果”的在校生也适合想快速搭一套社交舆情分析原型做验证的从业者。2. 模块划分与选型先搭好四层数据流水线再写代码很多课程项目的源码一打开所有逻辑都堆在一个 main.py 里爬虫、清洗、分析、画图互相耦合。这种写法不是不能跑但调试一次就要把整个流程重跑一遍时间全耗在“不知道哪一步出错”上。我的习惯是先把系统拆成四层每一层只做一件事层与层之间用数据格式约定接口。2.1 四层模块边界采集、清洗、分析、呈现各管什么第一层是采集层只负责从微博拿到原始数据并落库。这一层不关心文本内容是什么只关心“拿到没有、字段全不全”。常见做法是定义一个统一的微博数据模型包含微博 id、正文、发布用户、转发数、评论数、点赞数、发布时间这七个字段落库后原始数据就冻结后面任何分析都从数据库读不回头再请求网络。第二层是清洗层把 HTML 标签、URL、用户名、话题符号这些噪音去掉同时做繁简转换、emoji 处理和文本截断补齐。这里的产出是“干净文本”情感分析和话题聚类都基于它。很多源码在这一步偷懒只做了 strip()结果后面情感词典匹配率极低因为微博正文里全是https://t.cn/xxxx和span classurl-icon。第三层是分析层负责分词、情感打分、话题聚类、时序统计。这一层是系统的“大脑”也是最容易做成黑匣子的地方。建议把情感词典、停用词表、用户词典都放在单独的配置目录里这样换数据集时不用改代码只换词典。第四层是呈现层输出词云、热度折线图、情感分布饼图以及一份汇总报告。课程设计答辩时这一层决定老师的第一印象——一张清晰的“事件热度 情感占比”图比一百行代码截图更有说服力。2.2 技术选型的三个决定请求库、数据库、可视化方案请求库直接用requests不要一上来就上 Scrapy。课程项目的数据量通常在几万条以内Scrapy 的分布式、中间件、Item Pipeline 这些特性用不上反而把工程复杂度抬高答辩时还容易被追问“你这个并发控制怎么做的”。requests配合 Session 维持登录态、随机延时控制频率足够应付课设场景。数据库选 SQLite零配置、单文件、Python 标准库自带的sqlite3就能操作。有的源码用了 MySQL但课设项目要在一台机器上演示MySQL 需要额外安装服务、配置账号密码评委换一台机器跑你的项目就起不来。SQLite 的数据量上限在 10 万条量级毫无压力一个weibo.db文件拷走就能换环境跑。如果你非要上 MySQL请把建表脚本和初始化数据一起放进源码包别让人家裸奔着跑。可视化用pyecharts或matplotlib都行。我更推荐 pyecharts因为它的图表是 HTML 交互式鼠标悬停能看到具体数值答辩演示时不用再解释“这个点代表多少”。词云用wordcloud库注意它依赖matplotlib装的时候一起装避免跑起来才报ModuleNotFoundError。分词用jieba情感分析用词典打分法模型法如用 BERT 微调门槛高、标注数据难找课程设计没必要。提示把所有依赖写进 requirements.txt并注明 Python 版本。课设源码最常翻车的不是在代码逻辑而是换了一台电脑后 import 直接报错。3. 微博数据采集层API 优先、爬虫兜底的双通道实现采集是整个系统里“看起来最简单、跑起来最容易翻车”的一层。微博网页版的反爬强度很高直接用requests.get拿搜索页 HTML 会被风控拦下来。常见做法是走移动端 H5 的搜索接口它返回的是 JSON解析成本低再配合一个登录后的 Cookie 来维持访问权限。3.1 通道一微博搜索接口的参数配置与解析移动端微博搜索的核心接口是https://m.weibo.cn/api/container/getIndex它的关键参数是containerid。搜索关键词“新能源汽车”时containerid需要拼成100103type1q新能源汽车page_type设为searchallpage表示页码。这个接口返回的 JSON 结构和网页版完全不同解析时要先判断cards数组里每一项的card_type只有card_type 9才是微博正文卡片其他是广告位和推荐位不处理的话会把广告算进舆情统计里热度曲线直接失真。import requests import time def fetch_weibo_by_keyword(keyword, page1): 通过微博H5搜索接口获取单页微博数据 url https://m.weibo.cn/api/container/getIndex params { # 100103type1q关键词 是搜索containerid的固定格式 containerid: f100103type1q{keyword}, page_type: searchall, page: page } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: fhttps://m.weibo.cn/search?containerid100103type%3D1%26q%3D{keyword}, Cookie: 换成你自己的登录Cookie } resp requests.get(url, paramsparams, headersheaders, timeout10) resp.raise_for_status() cards resp.json().get(data, {}).get(cards, []) results [] for card in cards: if card.get(card_type) ! 9: continue mblog card.get(mblog, {}) results.append({ mid: mblog.get(id), text: mblog.get(text), user_name: mblog.get(user, {}).get(screen_name, ), reposts_count: mblog.get(reposts_count, 0), comments_count: mblog.get(comments_count, 0), attitudes_count: mblog.get(attitudes_count, 0), created_at: mblog.get(created_at, ), }) return results这段代码里有两个细节值得注意。一个是Referer必须带上否则接口可能返回 403另一个是mblog.get(text, )取出来的是带 HTML 标签的富文本不是纯文本必须交给清洗层处理。mid字段是微博的唯一标识后续入库去重就靠它比用正文做键靠谱得多——正文可能有重复mid 不会。采集多页时建议每页之间time.sleep(random.uniform(1, 3))不要用固定延时。固定延时容易被识别为脚本行为随机区间更接近人工操作节奏。翻页的上限设置在 50 页以内就够课设用了再往后搜到的内容相关性会显著下降。3.2 通道二requests 会话与手动登录的兜底方案H5 接口有时会因为没有登录态而返回空数据。另一种常见做法是先在浏览器里登录微博从开发者工具里复制 Cookie拼到请求头里。这种方式的坑是 Cookie 会过期通常几小时到一两天不等源码里写死 Cookie 的话演示时大概率会失效。更稳妥的做法是先用requests.Session()保存登录态再手动配置Cookie参数同时把“检查接口是否返回登录失效”的逻辑写进去import requests def create_weibo_session(cookie_str): 创建带Cookie的requests会话 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, Referer: https://m.weibo.cn/, }) session.headers[Cookie] cookie_str return session def is_login_expired(resp_json): 判断返回数据是否为登录失效 data resp_json.get(data) if data is None: return True cards data.get(cards) if not cards and resp_json.get(ok) 0: return True return Falserequests.Session会把 Cookie 管理在会话内部不用每次请求都手动拼。is_login_expired这个函数在采样阶段很关键——发现返回的cards为空且ok为 0就直接抛异常提示“请重新更新 Cookie”而不是静默返回空列表否则分析层会拿到一份空数据集画出来的图全是空的你还以为是数据源出了问题。3.3 采集策略关键词覆盖、入库去重与频率控制采集模块还差最后一块拼图——入库。用 SQLite 建一张weibo表字段对应前面说的七个维度并对mid建立唯一索引。写入时用INSERT OR IGNORE重复的 mid 自动跳过从源头消灭重复数据。import sqlite3 def init_db(db_pathweibo.db): conn sqlite3.connect(db_path) conn.execute( CREATE TABLE IF NOT EXISTS weibo ( mid TEXT PRIMARY KEY, text TEXT, user_name TEXT, reposts_count INTEGER, comments_count INTEGER, attitudes_count INTEGER, created_at TEXT ) ) conn.commit() return conn def save_weibo_batch(conn, items): 批量写入微博数据mid冲突时忽略 sql INSERT OR IGNORE INTO weibo (mid, text, user_name, reposts_count, comments_count, attitudes_count, created_at) VALUES (?, ?, ?, ?, ?, ?, ?) conn.executemany(sql, [ (item[mid], item[text], item[user_name], item[reposts_count], item[comments_count], item[attitudes_count], item[created_at]) for item in items ]) conn.commit()关键词覆盖上建议不只搜一个词而是把“核心词 同义词 相关话题”三个维度组合。比如分析“新能源汽车”可以同时搜“新能源汽车”“电动汽车”“新能源车”“特斯拉”“比亚迪”这五个词然后把 mid 去重后合并。这样能避免只看单一关键词导致的数据偏差——很多人发微博根本不带“新能源汽车”这个完整词只提品牌名。4. 从原始文本到舆情指标清洗、分词、情感判定的完整实现采集层拿到的是带 HTML 标签的富文本和一堆数值字段直接拿去分析会得出完全错误的结论。比方说微博正文里的#话题#符号如果不拆掉“#新能源汽车#”会被 jieba 切成一个整体情感词典匹配不到“新能源”这个词。所以清洗和分词这步的质量直接决定后面所有分析的可信度。4.1 清洗链路HTML 标签、URL、提及与表情符号清洗有个固定的处理顺序顺序不对会留下残余噪音。先把 HTML 标签去掉再处理 URL然后删掉 用户名和话题符号最后做繁简转换和 emoji 归一化。import re import opencc converter opencc.OpenCC(t2s) # 繁体转简体 def clean_weibo_text(raw_text): # 第一步去掉HTML标签 text re.sub(r[^], , raw_text) # 第二步去掉URL text re.sub(rhttps?://\S, , text) # 第三步去掉用户名但保留话题词本身 text re.sub(r[\u4e00-\u9fa5\w\-], , text) text text.replace(#, ) # 第四步繁体转简体 text converter.convert(text) # 第五步去除多余空白和不可见字符 text re.sub(r[\s\u200b], , text).strip() return text这个顺序是有讲究的。先去掉 HTML 标签是因为标签里可能包含 URL 和空格如果把 URL 的替换逻辑放在前面a hrefhttps://t.cn/xxx里的链接会被https?://\S吃掉但a href这个壳还留在文本里后面 jieba 分词会把它切成a这种垃圾词。\u200b是零宽空格微博文本里很常见\s匹配不到它必须单独把它加进空白处理里。4.2 情感分析词典打分与阈值校准情感分析是社交舆情分析系统的核心评价指标。常见做法是词典打分法准备一份积极词表和一个消极词表对分词结果逐一匹配积极词 1、消极词 -1最后汇总得分。得分大于 0 判为积极小于 0 判为消极等于 0 判为中性。import jieba # 加载情感词典 def load_senti_dict(pos_path, neg_path): pos_words set() neg_words set() with open(pos_path, r, encodingutf-8) as f: for line in f: pos_words.add(line.strip()) with open(neg_path, r, encodingutf-8) as f: for line in f: neg_words.add(line.strip()) return pos_words, neg_words def sentiment_score(text, pos_words, neg_words, stopwords): words [w for w in jieba.lcut(text) if w not in stopwords and len(w) 1] score 0 for w in words: if w in pos_words: score 1 elif w in neg_words: score - 1 return score, words词典法的优点是解释性强答辩时能说清楚“这条微博为什么被判定为消极——因为出现了‘失望’‘维权’‘差劲’三个消极词”。缺点是覆盖率有限尤其是微博上大量出现的网络新词。一个补救措施是加载jieba.load_userdict(userdict.txt)把“绝绝子”“yyds”“破防”这些词做成用户词典让 jieba 正确切分再在情感词典里补上对应的情感词条。阈值校准这里有个常见误区直接用 0 作为积极/消极的分界线会把大量中性微博误判成积极。因为很多微博是纯转发只带“转发微博”四个字分词后没有情感词但也可能因为正文里有个“好”字得 1 分。我的做法是设置双阈值得分大于等于 2 才算积极小于等于 -2 才算消极中间都算中性。阈值具体取多少要根据你的人工抽样标注来调没有万能参数。4.3 话题聚类与时序统计事件热度曲线怎么画话题聚类在课程项目里不需要上复杂的 LDA 主题模型用“高频词共现”就能得到不错的结论。先统计所有文本里名词的出现次数取 Top 50 作为候选话题词再统计这些词两两在同一文本中的共现次数共现次数高的词对就是潜在话题点。from collections import Counter import itertools def extract_topics(word_list_per_text, top_n30): 基于词共现做简单话题聚类 word_counter Counter() co_occur Counter() for words in word_list_per_text: word_set set(words) word_counter.update(word_set) if len(word_set) 1: # 只统计Top高频词之间的共现降低计算量 co_occur.update(itertools.combinations(sorted(word_set), 2)) # 取出高频词 top_words [w for w, _ in word_counter.most_common(top_n)] # 筛出包含高频词的共现对按次数排序 topics {} for (w1, w2), cnt in co_occur.most_common(): if w1 in top_words and w2 in top_words: topics[(w1, w2)] cnt if len(topics) 20: break return topics时序统计则按天对微博数量做聚合再结合每天的情感均值画双轴图。横轴是日期左轴是发文量柱状图右轴是情感得分均值折线图。这样能直观看出“事件在哪个节点爆发同时舆论态度怎么变化”——比如某天发文量达到顶峰但情感均值跌到负值说明负面情绪在事件高点集中释放这个结论在答辩时非常有价值。5. 让系统真正跑稳的 4 个避坑点登录、限流、编码与词典边界这个项目踩坑的密度比想象中高很多问题不是逻辑写错而是数据源和文本本身带进来的。下面四条是课设迭代中最常见的每一条都值得在交源码前自查一遍。5.1 登录态失效导致采集中断现象爬虫跑了十几分钟后返回的数据全是空日志里没报错但库里一条新记录都没有。原因H5 接口的 Cookie 失效后接口返回 200 但 data 为 null代码里没有对这种情况做检查静默吞掉了错误。解决在抓取函数里加入“空数据即异常”的判断一旦发现连续 3 页没有任何有效卡片立刻抛出异常并停止采集。再配合一个手工更新的 Cookie 文件把 Cookie 单独放在cookie.txt里采集前从文件读取不要硬编码在源码中。5.2 请求频率触发风控现象采集到 200 条左右后返回的 JSON 里ok字段变成 0页面上提示“操作太频繁”。有的同学以为是 IP 被封了其实是请求频率超过了单账号的限额。解决每页之间加time.sleep(random.uniform(1, 3))另外把采集拆成几轮每轮最多 20 页轮与轮之间暂停 60 秒。如果你是在局域网里跑多台机器同时采集同一个关键词还会共享出口 IP尽量错峰跑。还有一种翻车场景是把延时放在循环外层结果第一次循环就触发了限流检查时先把延时位置打印出来看。5.3 分词和情感词典在微博语境下的误判现象情感分析把所有含“好”字的微博都判成积极连“这车质量好差”都成了高分积极文本。原因是 jieba 把“好差”切成了“好”和“差”情感词典里“好”是积极词、得 1“差”是消极词、得 -1但打分时只做了加法没有考虑修饰关系。解决在打分逻辑里加入否定词和程度副词前缀检测——如果情感词前 1 到 3 个字节内出现“不”“没”“太”“很”翻转或加权。具体规则是“否定词 情感词”整体算作相反情感程度副词则把权重乘以 1.5。这个规则很粗糙但比裸加词性得分要客观得多。def sentiment_score_with_negation(words, pos_words, neg_words): score 0 negation {不, 没, 无, 别, 莫} intensifier {很, 太, 超, 极, 特别} for i, w in enumerate(words): if w in pos_words: base 1 elif w in neg_words: base -1 else: continue # 向前找最近的一个词判断是否是否定/程度 if i 0: prev words[i - 1] if prev in negation: base -base elif prev in intensifier: base base * 1.5 score base return score这条代码解决的是“好差”“不太好”这类局部语境。更长的依存关系它处理不了但课程设计够用。5.4 文本截断与 emoji 编码问题现象词云里出现一堆“展开c”“全文”之类的垃圾词。原因是微博正文超过一定长度会被截断正文末尾带着“...展开c”而完整内容需要根据isLongText字段再请求一次长文本接口。很多源码直接忽略了这个字段导致分析结果缺失了最关键的论述部分。解决清洗时先检测mblog.get(isLongText)等于 1 就调用https://m.weibo.cn/statuses/extend?id{mid}拿全文再覆盖原 text 字段。emoji 的问题更隐蔽。像“”这类字符在 Python 字符串里是四字节的直接打印没问题但写入 SQLite 后如果连接没指定text_factory读出来可能变成乱码。处理方案是入库时用replace把 emoji 统一替换成文本标记比如把 替换成[笑哭]这样既不影响分词也不污染数据库。import emoji def normalize_emoji(text): # 把emoji转成文本描述例如“”变成“[笑哭]” return emoji.demojize(text, delimiters([, ]))6. 从能跑到能答辩可视化呈现与结果验证的一线做法系统跑通只完成了一半另一半是让结果“看得见、说得清”。课程设计的评分很大程度取决于演示页面的直观程度。一张丑图会让前面所有工作打折扣一张带交互的图表能让评委愿意多看两眼。6.1 用 pyecharts 和 WordCloud 快速生成舆情大屏舆情大屏是课设答辩的加分项但不需要做成实时大屏那么复杂。三个图表就够了一是按天发文量 情感均值的双轴折线图说明事件热度走势和舆论态度变化二是情感占比环形图直观展示积极、中性、消极三类微博的比例三是关键词词云展示高频话题词。from pyecharts.charts import Line, Pie from pyecharts import options as opts def build_trend_chart(dates, volumes, sentiments): line Line() line.add_xaxis(dates) line.add_yaxis(发文量, volumes, yaxis_index0) line.add_yaxis(情感均值, sentiments, yaxis_index1) line.set_global_opts( title_optsopts.TitleOpts(title事件热度与情感走势), tooltip_optsopts.TooltipOpts(triggeraxis), ) return line def build_sentiment_pie(pos_count, neg_count, neu_count): pie Pie() pie.add(, [(积极, pos_count), (消极, neg_count), (中性, neu_count)]) pie.set_global_opts(title_optsopts.TitleOpts(title舆论情感占比)) return pie两个图都输出为 HTML 文件双击就能在浏览器里交互查看。pyecharts 的默认主题配色偏商务答辩投影时比 matplotlib 的默认蓝色线条清楚得多。词云这里有一个参数建议WordCloud的font_path一定要指定中文字体比如msyh.ttc微软雅黑否则生成出来的词云全是方框。指定字体后再设置width800, height600词云的分辨率足够投影显示。提示词云有个“玄学”问题——同一个参数跑两遍词的排布随机变化。这是正常的wordcloud 内部有随机布局逻辑。答辩前把结果保存成图片再插入 PPT不要现场重新生成省得版式每次都不一样。6.2 效果验证人工标注对比和参数校验分析模块的可靠性直接关系到课设评分。我建议交源码前做一次系统性的结果验证随机从数据库里抽取 100 条微博自己或请同学人工标注情感类别然后和你系统的自动判定做对比计算准确率。def evaluate_accuracy(manual_labels, auto_labels): 计算人工标注与自动判定一致率 correct 0 for m, a in zip(manual_labels, auto_labels): if m a: correct 1 return correct / len(manual_labels)准确率能到 70% 以上在课程设计里就算合格低于 60% 就得考虑是不是情感词典覆盖不足或者阈值设置不合理。把这份对比数据写进课设报告里比写“本系统准确率高”要可信得多。另外把采集到的时间范围、关键词、数据量在报告里写清楚让结论可复现。最后一个习惯是我每次交课设前都会做的从源码包里新建一个干净的虚拟环境只装 requirements.txt 里的依赖然后完整跑一遍python main.py --keyword 新能源汽车。这个动作能抓到 90% 的环境依赖问题——很多源码在自己电脑上能跑是因为环境里装了一堆没写进 requirements 的库。确保换一台电脑、按你的文档操作也能复现整个流程这才是课程项目源码该有的样子。希望这篇笔记能帮你少走几条弯路。本文还有配套的精品资源点击获取