简介:基于微博数据的舆情分析项目,整合微博爬虫、LDA主题分析与情感分析三大模块,面向计算机、通信、人工智能等专业学生及从业者,适用于课程设计、大作业或毕业设计等场景。资源包内含三十九个文件,以Python脚本、Markdown说明文档和TXT语料为主,另附训练好的word2vec模型及Excel数据表,整体体积约十六兆,结构清晰便于按模块查阅。项目为个人高分作品,答辩评审达九十八分,所有代码均经过调试测试可直接运行,涵盖热度计算、评论情感均值分析、主题余弦相似度、多日期降维等功能,并附正向负向语料、近义词表等资料,方便替换数据快速复现。目前已有211人学习下载,兼具完整性与可扩展性,既能支撑入门实践,也可在此基础上二次开发,具有较高的学习借鉴价值。
1. 微博舆情分析项目:爬虫、LDA与情感分析的完整流水线
这个项目把微博舆情分析完整串成了一条流水线:先用微博爬虫把评论和用户信息拉下来,再做数据清洗和分词,然后兵分三路——LDA跑主题分析、情感分析打正负标签、热度计算给事件排序,最后汇总成图表和结论。对于准备毕业设计,或者想一口气把爬虫、NLP、可视化全过一遍的人,这套源码比零散教程省事得多。项目全用 Python 实现,核心模块分为 weibo-crawler、LDA、emotional analysis、heat calculation 四组,中间夹着停用词表、正负向语料和训练好的 w2v.model,代码按目录结构复原后即可运行。下面我从爬虫层开始,把每个模块的选型逻辑、参数设置和踩坑点逐一拆开说。
2. 微博爬虫模块:评论抓取、用户信息与随机化策略
weibo-crawler 目录下放了 5 个脚本:comment crawler.py 负责通用评论抓取,comments-crawler_random.py 在它的基础上加了随机延迟,comments-crawler_random(仅针对去年的评论).py 把抓取范围限定到指定年份,user information crawler.py 收集博主主页信息,data cleaning.py 统一做清洗,requirement.txt 锁依赖。这样的分工意味着你不用改一个脚本跑到底,爬评论、爬用户、补旧数据是三种独立任务,可以分开调度。
2.1 评论爬虫:m.weibo.cn 接口与字段解析
评论爬虫的核心是移动端接口m.weibo.cn/api/comments/show。微博 PC 端网页版结构复杂,频繁请求容易触发验证码,而移动端接口相对稳定,返回的 JSON 字段够用。常见做法是直接拿微博 mid 和 page 两个参数翻页,一次拉 20 条。
# comment crawler.py 核心逻辑示意 import requests def fetch_comments(mid, page=1): url = "https://m.weibo.cn/api/comments/show" params = {"id": mid, "page": page} headers = { "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X)", "Referer": f"https://m.weibo.cn/detail/{mid}", } resp = requests.get(url, params=params, headers=headers, timeout=10) data = resp.json() if not data.get("data"): return [] result = [] for c in data["data"]: result.append({ "comment_id": c.get("id"), "user_id": c["user"]["id"], "nickname": c["user"]["screen_name"], "content": c.get("text", ""), "created_at": c.get("created_at", ""), "like_count": c.get("like_count", 0), }) return result逻辑说明:params里的id就是微博正文的 mid,page从 1 开始递增,返回列表为空就说明翻到底。headers里带Referer是为了模拟从微博详情页跳转过来的流量,缺了这个头经常出现 200 但data为空的情况。timeout=10防止单条请求卡死整个流程。like_count是可选字段,但后续热度计算会用到,尽量保留。
调用时在外面套一个 while 循环,直到返回结果少于 20 条或者为空就停止。created_at是英文时区格式,比如Fri Dec 22 12:00:00 +0800 2023,这一步不用急着转标准格式,清洗阶段统一处理。
2.2 用户信息爬虫:从用户 ID 到地域与认证信息
拿到评论里的user_id之后,需要去补用户维度信息时就走container/getIndex接口。它的参数是containerid=100505{uid},返回体里包含性别、认证状态、粉丝数、所在地。
# user information crawler.py 核心逻辑示意 def fetch_user_info(uid): url = "https://m.weibo.cn/api/container/getIndex" params = {"containerid": f"100505{uid}"} headers = { "User-Agent": "Mozilla/5.0 (Linux; Android 10)", "Referer": "https://m.weibo.cn/", } resp = requests.get(url, params=params, headers=headers, timeout=10) data = resp.json().get("data", {}) user = data.get("userInfo", {}) return { "user_id": uid, "gender": user.get("gender", ""), "verified": user.get("verified", False), "followers_count": user.get("followers_count", 0), "location": user.get("location", ""), }逻辑说明:containerid拼接规则是固定前缀100505加用户 ID,这是移动端用户主页的标准参数。返回的数据层级要注意,用户资料嵌在data.userInfo里,不是直接挂在data下。location字段可能是北京、海外这类文本,后续做地域分布聚合时可以直接 groupby。
这里有个容易被忽略的点:抓用户信息属于二次请求,请求量和评论爬取不在一个量级。如果评论有 1 万条,去重后的用户可能只有几千,建议先把user_id去重存进集合,再逐批请求。
2.3 随机化与时间窗口:requests 请求的频率控制
comments-crawler_random.py存在的意义很明确:固定频率请求是爬虫翻车的第一大原因。微博服务端对单 IP 单位时间内的请求数有隐式限制,连发几十个请求后必然被限流。随机化的核心不是随意 sleep,而是让每次请求间隔落在一个合理区间内。
# comments-crawler_random.py 频率控制片段 import time import random def crawl_with_random_delay(crawl_fn, max_pages=10): for page in range(1, max_pages + 1): try: items = crawl_fn(page) if not items: break delay = random.uniform(2.0, 5.0) # 随机延迟 print(f"page={page}, sleep={delay:.1f}s") time.sleep(delay) except requests.exceptions.RequestException: time.sleep(10) # 网络异常时退避逻辑说明:random.uniform(2.0, 5.0)是经验值。太短容易被识别为脚本,太长的话单微博几百页评论要等很久,2 到 5 秒是多数人实测的折中区间。异常分支单独睡 10 秒,是为了在接口临时抖动时让流量降下来,而不是立刻重试。如果你只爬几十条做演示,这段可以砍掉;如果要爬完整条微博的评论,它是必需项。
另一个脚本comments-crawler_random(仅针对去年的评论).py加的是时间窗口过滤:把created_at字符串转成 datetime,判断年份是否落在指定区间。这样爬出来的评论天然按年分类,做同比分析时省一道筛选。
# 仅针对去年的评论.py 时间窗口判断 from datetime import datetime def in_target_year(date_str, target_year): dt = datetime.strptime(date_str, "%a %b %d %H:%M:%S %z %Y") return dt.year == target_year参数说明:target_year由外部传入,比如 2023。注意strptime的格式串里%z对应+0800时区后缀,格式写错会直接抛 ValueError。数据清理阶段我用data cleaning.py做二次加工,去 HTML 标签、去重复评论、统一日期格式,这些坑在第 5 章专门展开。
3. LDA 主题分析:分词处理与超参调整的落地流程
LDA 目录下有 8 个文件:LDA.py 是标准流程,LDA+超参.py 是可调参版本,分词处理.py 负责把文本切成词袋,excel转txt.py 把 Excel 里的文本列抽成纯文本,自建词表.txt、近义词表.txt、停用词表.txt 是三份词表资源。它们的分工很清晰:先用词表武装分词器,再把语料转成中间格式,最后喂给 gensim 建模。
3.1 分词处理:jieba、自建词表与近义词合并
LDA 之前必须做中文分词。项目里用的是 jieba,但直接对微博文本跑默认 jieba 会出问题——网络新词、领域词会被拆得四分五裂。所以分词处理.py 的加载顺序是:先加载自建词表,再过滤停用词,最后做近义词合并。
# 分词处理.py 核心逻辑示意 import jieba jieba.load_userdict("自建词表.txt") # 领域词一次性装入 stopwords = set() with open("停用词表.txt", encoding="utf-8") as f: for line in f: stopwords.add(line.strip()) synonym_map = {} with open("近义词表.txt", encoding="utf-8") as f: for line in f: group = line.strip().split() if len(group) >= 2: for word in group[1:]: synonym_map[word] = group[0] def tokenize(text): words = [] for w in jieba.lcut(text): w = w.strip() if not w or w in stopwords: continue words.append(synonym_map.get(w, w)) return words逻辑说明:load_userdict把“数字经济”“双减政策”这类复合词整体锁定,分词时才不会被拆成“数字/经济”。停用词过滤放在 jieba 切词之后,凡是命中停用词表的直接丢弃。近义词合并在最后一步,例如把“房价走势”“房价波动”同时映射到“房价”,LDA 的稀疏矩阵会变紧凑,主题词收敛更明显。
这里有个值得注意的点:微博文本里大量出现的“网页链接”“转发微博”这类噪声词,靠通用停用词表覆盖不到。运行项目前建议打开停用词表.txt,把这些平台泛化词手动追加进去,否则 LDA 的主题会被这些高频词主导。
3.2 Excel 转 TXT:从新增.xlsx 到 LDA 输入格式
LDA 输入不关心 Excel 格式,只认纯文本。excel转txt.py 的作用就是把新增.xlsx里的文本列逐行抽出来,写成一个干净的行式文本文件。这个中间步骤看似多余,实际很有必要,因为 Excel 列里的换行符和特殊字符会污染后续的词典构建。
# excel转txt.py 核心逻辑示意 import pandas as pd df = pd.read_excel("新增.xlsx") # 假设文本列名为 content,按需修改 with open("corpus.txt", "w", encoding="utf-8") as f: for text in df["content"].dropna(): f.write(str(text) + "\n")逻辑说明:read_excel底层依赖 openpyxl 引擎,如果环境里没装会直接报 ModuleNotFoundError,安装 requirement.txt 时要把 openpyxl 一并装好。写入文件时务必指定encoding="utf-8",否则某些 Windows 环境下默认编码写出的文件,LDA 读取时会出现乱码。dropna()把空评论过滤掉,避免空行进入 LDA 语料。
3.3 LDA 建模与超参调整:num_topics、passes 与 alpha
LDA.py 使用 gensim 跑标准流程,LDA+超参.py 则把关键超参暴露成变量,方便批量实验。gensim 的 LdaModel 核心参数有四个:num_topics、passes、iterations 和 alpha/eta。
# LDA+超参.py 核心逻辑示意 from gensim import corpora, models texts = [] with open("corpus.txt", encoding="utf-8") as f: for line in f: tokens = tokenize(line) if tokens: texts.append(tokens) dictionary = corpora.Dictionary(texts) corpus = [dictionary.doc2bow(doc) for doc in texts] lda = models.LdaModel( corpus, num_topics=6, id2word=dictionary, passes=20, iterations=200, alpha="auto", eta="auto", random_state=42, ) for topic_id in range(lda.num_topics): words = [w for w, _ in lda.show_topic(topic_id, topn=10)] print(f"主题{topic_id}: {' '.join(words)}")参数说明:num_topics=6是我在这个项目里跑了一圈后选出来的起始值,不一定对所有数据都成立。常见做法是从 4 跑到 10,逐个看show_topic输出的主题词是否互斥再定。passes=20表示整个语料库被训练 20 轮,小规模语料 20 足够,语料大了可以加到 50,但耗时线性上升。iterations=200是内部迭代的默认值,短文本场景不用改。alpha="auto"和eta="auto"让模型自己估计文档-主题和主题-词分布,比手工指定固定值更稳,这也是中文短文本 LDA 的常规做法。
LDA+超参.py 的核心价值在于批量对比:循环尝试不同主题数,输出每轮的主题词和困惑度。这里强调一点,LDA 是概率模型,困惑度低不代表主题可解释性强,最终要以人工读主题词为准。我在实际项目里会把自动对比结果当参考,主题词看着像一回事才算数。
4. 情感分析实现:API 版与 SDK 版的双路线解读
emotional analysis 目录下同时存在两个版本:情感分析_API版.py 走外部通用接口,情感分析_SDK版.py 跑本地语料打分。配套的还有正向语料.txt、负向语料.txt、正向比重.py、折线图绘制.py、多日期降维.py、修改日期格式.py。有一份双版本代码的好处是:网络环境不稳时切 SDK 版离线跑,需要高精度时切 API 版,互不干扰。
4.1 API 版与 SDK 版:两条路线的取舍
先看对比表,再展开讲怎么选。
| 维度 | API 版 | SDK 版 |
|---|---|---|
| 依赖 | requests、接口密钥 | jieba、本地正负向语料 |
| 共性 | 对网络流行语覆盖一般 | 依赖领域语料质量,可扩展 |
| 性能 | 每条一次网络请求,慢 | 本地计算,秒级批量处理 |
| 适用 | 小批量、容错高的场景 | 离线批量分析、课程答辩演示 |
API 版的实际流程就是读一行文本、调用接口、取回标签和置信度、写入结果表。SDK 版是我更常用的一条路,因为批量数据分析场景里,逐条打网络请求太慢,本地语料打分把一轮几万条评论处理完也就是几十秒。两版配合使用的方式很常见:先用 SDK 版快速刷完全量数据,挑出得分接近 0 的模糊样本,再用 API 版复核这一小部分。
4.2 正负向语料与情感得分:从词典到加权窗口
SDK 版最大的资产是那两份语料文件——正向语料.txt 和负向语料.txt,里面每行一个情感词。情感分析_SDK版.py 把这两个文件加载成集合,再对每条微博分词后逐词命中统计。
# 情感分析_SDK版.py 核心逻辑示意 import jieba def read_vocab(path): with open(path, encoding="utf-8") as f: return set(line.strip() for line in f if line.strip()) pos_words = read_vocab("正向语料.txt") neg_words = read_vocab("负向语料.txt") def sentiment_score(text): score = 0 for word in jieba.lcut(text): if word in pos_words: score += 1 elif word in neg_words: score -= 1 return score逻辑说明:计分逻辑直接,正向词加一、负向词减一。它不考虑否定词和程度副词,比如“不太高兴”会被算成负向,因为命中了“高兴”;“很不满意”还是负向,因为“满意”是正向词抵消不了“不”的语义反转。这是词典法的已知短板,但胜在逻辑透明,答辩时一句“这是词典法的典型取舍”就能讲清楚。
使用这两份语料时注意:正向语料.txt 是一份基础集,直接拿来跑通用话题问题不大,但如果你分析的是具体行业,比如地产或教育,强烈建议往里补领域情感词。比如分析楼市舆情,“上涨”“松绑”在语境里是正向;分析就业舆情,“裁员”“降薪”必须进负向语料。
4.3 情感均值、正向比重与折线图:把文本变成时间序列
单条评论的情感得分没意义,按天聚合后的趋势才有价值。情感均值.py 和 Comment mean.py 做同一件事——按日期分组计算平均分;正向比重.py 多算一列正向占比;折线图绘制.py 最后把两条线画出来。
# 情感均值.py + 折线图绘制.py 组合逻辑 import pandas as pd import matplotlib.pyplot as plt df = pd.read_excel("新增.xlsx", parse_dates=["created_at"]) df["score"] = df["content"].apply(sentiment_score) df["date"] = df["created_at"].dt.date daily = df.groupby("date").agg( mean_score=("score", "mean"), positive_ratio=("score", lambda x: (x > 0).mean()), ) daily.plot(y=["mean_score", "positive_ratio"], kind="line") plt.savefig("sentiment_trend.png", dpi=150)逻辑说明:这里有两个容易踩的地方。第一,parse_dates=["created_at"]告诉 pandas 把该列解析成日期类型,否则 groupby 按字符串聚合会把同一天的数据拆成不同 key。第二,positive_ratio的 lambda 表达式计算当天得分大于 0 的评论占比,它和均值一纵一横,一个看负面程度、一个看负面范围。多日期降维.py 就是在这个聚合结果上按月份或季度再做一次降维,适合跨年对比的场景。
5. 避坑笔记:从爬虫数据到 LDA 主题的常见坑点与排查
这里我把项目跑通过程中最容易翻车的 5 个问题按“现象 → 原因 → 解决”整理成记录,每一条都是实际踩过的。
5.1 评论里全是 HTML 标签
现象:爬回来的 content 字段带着<a href="..." rel="nofollow">、<br/>、<span class="...">之类标签,分词后主题词全被标签拆散。
原因:m.weibo.cn 返回的text字段是富文本,不是纯文本,微博在评论内容里嵌了表情和短链。
解决:在 data cleaning.py 里做两层清洗,先正则去标签,再压缩空白。
import re content = re.sub(r"<[^>]+>", "", content) content = re.sub(r"\s+", " ", content).strip()这个坑不解决,后面 LDA 的词表会被http、class这类残片污染,主题可解释性直接崩盘。
5.2 excel转txt 后 LDA 读文件乱码
现象:excel转txt.py 生成的 corpus.txt 在 LDA 读取时报UnicodeDecodeError,或者读进来全是框框。
原因:Excel 文件在不同版本另存时的默认编码不一致,写出的 TXT 可能是 ANSI 或带 BOM 的 UTF-8,而读文件的代码指定了纯utf-8。
解决:读取时统一用utf-8-sig并加errors="ignore"兜底。
with open("corpus.txt", encoding="utf-8-sig", errors="ignore") as f: texts = [tokenize(line) for line in f]utf-8-sig能自动处理 BOM 头,errors="ignore"保证个别脏字节不会让整个任务中断。从那以后我写任何中文文本读取都会默认考虑编码问题。
5.3 日期格式不统一导致按天聚合失败
现象:groupby 日期后,数据全挤在同一天,或者聚合结果出现一堆 NaN。
原因:爬虫返回的created_at是含时区的英文格式,例如Fri Dec 22 12:00:00 +0800 2023,而 Excel 里手工录入的日期可能是2023/12/22或2023-12-22,两种格式混在一起。
解决:用修改日期格式.py 统一转成%Y-%m-%d后再聚合。
from datetime import datetime def normalize_date(s): return datetime.strptime(s, "%a %b %d %H:%M:%S %z %Y").strftime("%Y-%m-%d")如果来源复杂,就用pd.to_datetime(s, format="mixed")做自动推断,解析不了的丢到异常列表人工补。日期是时间序列分析的命脉,这一步值得多花几分钟验证。
5.4 LDA 主题词全混在一起,每个主题都像在说同一件事
现象:跑完 LDA,5 个主题里 4 个都包含“转发”“微博”“视频”“链接”这类词,主题之间无法区分。
原因:停用词表覆盖不全,平台泛化词把主题词挤掉了。微博文本里的高频噪声词不是自然语言,是功能词。
解决:手动扩展停用词表,把转发、微博、网页链接、视频、展开这类词全部加进去,重新跑分词处理.py 后再建 LDA。这一步是纯体力活,但值得做,主题模型的输出质量有很大一部分取决于词表质量。近义词表.txt 同理,领域词不合并,主题就永远是散的。
5.5 爬虫爬到一半被限流
现象:请求连续返回 401 或空列表,手动刷新浏览器正常,过几分钟脚本又恢复。
原因:请求频率过于固定,User-Agent 和 Referer 不匹配,触发了微博服务端的接口频控。
解决:随机延迟时间间隔,并准备多个 UA 交替使用。
ua_list = [ "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X)", "Mozilla/5.0 (Linux; Android 10)", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", ] headers["User-Agent"] = random.choice(ua_list)核心不是单个 UA 多强,而是请求特征分布要像真人。固定 2 秒一发的规律请求最容易被识别。
6. 热度计算与主题验证:用余弦相似度给分析兜底
6.1 热度三层加权:从转发、评论、赞到时间衰减
heat calculation 目录下的热度_1.py、热度_2.py、热度_3.py 代表了三种热度模型,逐级递进。第一层是线性加权,把转发、评论、点赞按固定权重相加;第二层加时间衰减;第三层引入多参数因子。实际分析中用最多的是带时间衰减的那个版本,因为舆情事件的生命周期差异很大,微博发布 10 分钟和发布 3 天后的热度意义完全不同。
# 热度_2.py 时间衰减热度示意 import math def heat_with_decay(like_count, comment_count, repost_count, hours_since_publish, weights=(1.0, 2.0, 3.0), decay_factor=0.5): raw = ( weights[0] * like_count + weights[1] * comment_count + weights[2] * repost_count ) decay = math.exp(-decay_factor * hours_since_publish / 24) return raw * decay参数说明:weights=(1, 2, 3)表示转发权重最高,评论次之,点赞最低,这是从“行为成本”角度取的默认值,转发需要二次创作,评论成本居中,点赞是一键行为。decay_factor=0.5是每 24 小时的衰减系数,数值越大热度掉得越快。分析单日舆情用 0.5 合适;分析一周维度的演化建议降到 0.2 左右,不然旧微博的热度会迅速归零,曲线全塌在一侧。
热度_3.py 的模型更接近行业里用的“多因子合成”,在时间衰减之外引入了原创率、用户等级等修正项。但要注意,热度不是越高越好,要结合情感倾向看——高热负向才是真正的舆情风险点,这个联动分析思路可以写在报告里。
6.2 用 Word2Vec 与主题余弦相似度做一致性验证
主题余弦相似度.py 是整个项目里最容易被忽略但价值最高的一个脚本。它做的事情是把 LDA 输出的主题词通过 word2vc.py 训练好的 w2v.model 映射成向量,再计算主题之间的余弦相似度,用来判断主题是不是重叠了。
# word2vc.py 训练词向量 from gensim.models import Word2Vec sentences = [jieba.lcut(line) for line in open("corpus.txt", encoding="utf-8-sig")] model = Word2Vec(sentences, vector_size=128, window=5, min_count=2, workers=4) model.save("w2v.model")参数说明:vector_size=128是词向量的维度,短文本场景下 128 到 256 都够用,维度太小丢失语义,太大训练慢且容易过拟合。window=5表示只看词前后各 5 个词内的共生关系,min_count=2过滤掉只出现一次的生僻词,能显著降低训练噪声。训练语料直接用 LDA 的输入 corpus.txt,保证两个模型在同一语义空间。
# 主题余弦相似度.py 计算主题间重叠度 import numpy as np from sklearn.metrics.pairwise import cosine_similarity model = Word2Vec.load("w2v.model") def topic_vector(topic_words, model): vecs = [model.wv[w] for w in topic_words if w in model.wv] if not vecs: return np.zeros(model.vector_size) return np.mean(vecs, axis=0) topic_a = lda.show_topic(0, topn=10) topic_b = lda.show_topic(1, topn=10) sim = cosine_similarity([topic_vector(topic_a, model)], [topic_vector(topic_b, model)])[0][0] print(f"主题相似度: {sim:.2f}")逻辑说明:相似度超过 0.7 基本可以判定两个主题高度重叠,说明主题数设少了,需要回 LDA+超参.py 调大 num_topics;反过来如果所有主题相似度都很低,说明主题数设大了,有一部分主题只是把同一批词拆散。这就是整个分析流程的兜底验证步骤。
这套流程跑完后,我习惯用 map 目录下的 Number of new employees.py 结合新增.xlsx 做一次地域维度扩展,把不同地区的正负向均值叠到地图上看空间分布,情绪密度和地域分布一旦对上,报告的落地感会强很多。从那以后每次换数据源,我都强制走一遍“爬虫 → 清洗 → 分词 → LDA → 主题一致性校验 → 情感均值 → 热度关联”闭环,直到主题相似度全部低于 0.7 才收工。希望帮到你。
本文还有配套的精品资源,点击获取