Python微博舆情分析系统:爬虫、情感分析与可视化实战
2026/9/10 7:16:11 网站建设 项目流程

简介:这份面向高校毕业设计、课程设计与期末大作业的Python微博舆情分析可视化项目,集爬虫采集、情感分析与可视化展示于一体,代码注释完整,适合Python初学者参照学习,也可作为答辩高分项目模板。压缩包共146个文件,包含Python源码与pyc编译文件、HTML/CSS/JS前端页面、CSV数据文件及SQL数据库脚本等类型,整体仅3.85MB,部署简单便捷。已有698人学习浏览,具备较高参考热度。项目内置多套页面样式与示例数据,可清晰还原舆情数据从采集、清洗、情感计算到可视化展示的完整链路,同时配合关键词列表、评论数据表等文件,便于理解微博文本处理与情感分类的实际应用。整体功能设计完善,管理逻辑清晰,适合用来快速完成一套可演示、可扩展的舆情分析系统,对毕业设计或课程报告有直接帮助。

1. 微博舆情分析为什么值得拆:从一条微博到一张舆情报表

大多数人在微博上刷到的是一条条碎片信息,但在舆情分析场景里,一条微博背后的发布时间、评论情感、转发扩散路径,才是真正有价值的数据。这套基于 Python 的微博舆情分析可视化系统,本质上是把「爬虫采集 → 数据清洗 → 情感分析 → 可视化报表」串成一条完整流水线:先从搜索接口拉取与目标词相关的微博正文和评论,落盘成 CSV,再做情感极性判断,最后通过 Flask + ECharts 展示关键词趋势、评论情感占比和热词分布。它适合三类人:正在做毕业设计或课程设计,需要一套有完整交互界面的 Python 项目;想学习 requests 爬虫与文本分析的初学者;以及需要快速验证舆情分析思路的从业者。比起动辄分布式爬虫、深度学习模型的方案,这套系统的设计更贴合「一个人、一台机器、一个周末跑通」的诉求。

2. 爬虫与数据持久化:requests 抓取、CSV 存储与字段设计

2.1 为什么选 requests + CSV,而不是 Scrapy 和 MySQL

微博的搜索接口反爬力度不算弱,但对毕业论文级别的项目来说,Scrapy 的下载中间件、Pipeline 调度反而增加了理解成本。requests 足够处理单机、低频、小批量的抓取任务,代码透明,出错了也容易定位。存储端我没有选择 MySQL,因为 CSV 三个显著优势:一是与 pandas、Excel、ECharts 无缝衔接,二是部署时不需要额外安装数据库服务,三是答辩时可以直接用 Excel 打开展示数据。项目里的articleData.csvarticleComments.csv正是这样的设计——前者存微博正文,后者存单条微博下的评论。

2.2 目标词配置:target.csv 的作用

系统把检索词放在target.csv里,每行一个词,爬虫启动后逐个读取。这样的好处是业务与代码分离,换题目、换关键词时不需要改代码逻辑。

# target.csv 研究生就业 考研压力 双减政策

爬虫启动时用pandas.read_csv读取目标词,再拼接成搜索 URL。注意目标词不要带换行符,否则 URL 编码后会变成%0A,导致搜索结果为空。我一般会在这里做一次strip(),把首尾空格和换行清掉。

2.3 请求构造与响应解析

微博搜索页的 URL 格式是https://s.weibo.com/weibo?q=关键词,返回 HTML。直接 requests.get 会被重定向到登录页,所以通常的做法是携带 Cookie。Cookie 从哪里来?手动登录微博网页版,打开开发者工具复制请求头里的Cookie字段,写进一个配置文件。注意 Cookie 有有效期,过期后系统会提示重新获取。

import requests import pandas as pd import time from bs4 import BeautifulSoup def fetch_weibo(keyword, cookie, page=1): headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Cookie': cookie, 'Referer': 'https://s.weibo.com/' } params = { 'q': keyword, 'page': page, 'Refer': 'g' } url = 'https://s.weibo.com/weibo' resp = requests.get(url, headers=headers, params=params, timeout=10) resp.encoding = 'utf-8' return resp.text

这段代码里Refer: g是搜索接口要求的一个固定参数,缺失会返回异常页。resp.encoding = 'utf-8'不能省,微博返回的页面头部没有显式 charset,requests 可能误判为 gbk,导致中文乱码。

拿到 HTML 后,用 BeautifulSoup 解析。搜索结果里每条微博是一个<div class="card-wrap">,需要从里面抽取 mid、用户名、正文、时间和转发评论点赞数。

def parse_html(html): soup = BeautifulSoup(html, 'html.parser') items = [] for card in soup.select('div.card-wrap'): try: mid = card.get('mid') username = card.select_one('.name').text.strip() content = card.select_one('.txt').text.strip() # 时间可能在 .from 里,也可能在 .time 里 time_node = card.select_one('.from a') or card.select_one('.time') pub_time = time_node.get('title') if time_node else '' nums = [int(x) if x.isdigit() else 0 for x in card.select_one('.act').stripped_strings] items.append([mid, username, content, pub_time, *nums]) except Exception as e: continue return items

stripped_strings会把转发、评论、点赞的文本和数字混在一起,例如转发 123评论 45赞 6。这里用列表推导把纯数字挑出来,不是数字的置为 0。注意异常处理必须写,因为很多卡片结构不完整,某一处select_one返回 None 会导致整条解析失败,用continue跳过是稳妥的做法。

2.4 CSV 字段设计与去重

采集到的字段保存为articleData.csv,字段不一定固定,但至少包含以下列:

字段名说明示例
mid微博唯一 ID,用于去重4fJqS2xyz
username发博用户名科技小观察
content微博正文今天看到一份报告…
pub_time发布时间2025-01-15 10:32
repost_count转发数123
comment_count评论数45
like_count点赞数678

写入 CSV 时,重点做两件事:按mid去重,以及把时间字符串统一成YYYY-MM-DD HH:MM:SS格式。时间格式化直接用pandas.to_datetime,不要自己写字符串切片,因为微博的时间格式有两种:今天发布的显示01-15,跨年显示2024-12-30,统一处理才方便后面按小时或按天聚合。

def save_articles(items, filepath='articleData.csv'): df = pd.DataFrame(items, columns=[ 'mid', 'username', 'content', 'pub_time', 'repost_count', 'comment_count', 'like_count']) df = df.drop_duplicates(subset='mid', keep='first') df['pub_time'] = pd.to_datetime(df['pub_time'], format='mixed') df.to_csv(filepath, index=False, encoding='utf-8-sig') return len(df)

utf-8-sig编码是为了让 Excel 直接打开时中文不乱码。如果用utf-8,Excel 会把 UTF-8 BOM 误认成 ANSI,导致第一列出现乱码。这一点在答辩演示时非常重要。

2.5 反爬与采集节奏

微博的反爬主要体现在两个维度:IP 频率限制和账号风控。常见做法是每次请求之间随机 sleep 1~3 秒,并限制单次任务抓取不超过 10 页。如果发现返回页面里没有card-wrap,几乎可以断定 Cookie 已失效或被风控,此时应该停止并提示用户重新登录,而不是继续跑空循环。

for page in range(1, 11): html = fetch_weibo(keyword, cookie, page) items = parse_html(html) if not items: break save_articles(items, f'data/{keyword}.csv') time.sleep(random.uniform(1, 3))

休息时间用random.uniform(1, 3)而不是固定 2 秒,是为了打乱请求间隔的固定模式。这里还要注意,采集每一条微博的评论时,需要再请求一次https://weibo.com/ajax/statuses/...获取评论列表,评论字段与正文分开存到articleComments.csv,用mid作为外键关联。

3. 情感分析:从 SnowNLP 到词典修正的落地细节

3.1 为什么选 SnowNLP 而不是 BERT

对于毕业设计来说,情感分析的结果只要符合直观认知即可,不一定非要追求 SOTA 准确率。SnowNLP 是一个纯 Python 实现的朴素贝叶斯情感分析库,训练语料来自电商评论,虽然领域不完全匹配微博,但胜在无需 GPU、无需微调、安装即用。BERT 等预训练模型准确率更高,但要处理 GPU 环境、模型下载、推理速度,反而容易把项目复杂度推向失控。如果后续想提升效果,可以替换成百度的paddlehubsenta模型,这是后话。

3.2 情感得分计算与阈值划分

SnowNLP 的sentiments属性返回 0~1 的分数,越接近 1 越正向,越接近 0 越负向。我一般按 0.4 和 0.6 作为分界:

from snownlp import SnowNLP def classify_sentiment(text, pos_th=0.6, neg_th=0.4): if not text or len(text.strip()) < 2: return '中性' s = SnowNLP(text) score = s.sentiments if score >= pos_th: return '正向' elif score <= neg_th: return '负向' else: return '中性'

阈值不是固定不变的。如果整套系统用在电商评论上,pos_th可以提高到 0.7,因为电商评论整体偏正向;而微博里负面情绪的表达更隐晦,0.6 更合适。调试时把分布打印出来,看三个类别的数量是否均衡,再微调阈值。

3.3 评论级与正文级的情感聚合

我建议对评论和正文分别做情感分析。正文表达的是博主的观点,评论反映的是大众反馈,两者分开统计更有价值。比如一条吐槽帖的正文是负向,但评论区可能吵成两派。聚合逻辑按目标词分组:

def aggregate_by_keyword(articles_df, comments_df): article_senti = articles_df.copy() article_senti['sentiment'] = article_senti['content'].apply(classify_sentiment) # 评论按 mid 关联到目标词 merged = comments_df.merge(articles_df[['mid', 'keyword']], on='mid', how='left') comment_senti = merged.copy() comment_senti['sentiment'] = comment_senti['comment'].apply(classify_sentiment) summary = article_senti.groupby(['keyword', 'sentiment']).size().unstack(fill_value=0) return article_senti, comment_senti, summary

这里merge的作用是把评论数据里每一条评论的mid映射到它所属的关键词。注意articles_df中必须保留keyword字段,否则多个目标词的结果全混在一起,无法区分。

3.4 自定义情感词典修正

SnowNLP 的缺陷是没考虑否定词和程度副词。比如「不漂亮」得分可能偏高,因为「漂亮」本身是强正面的。一个简单的修正规则是:检测文本中是否存在否定词(不、没、莫、非),如果存在且情感得分绝对值大于中位数,则反转类别。

NEG_WORDS = ['不', '没', '莫', '非', '难以', '无法'] def corrected_classify(text): label = classify_sentiment(text) if any(w in text for w in NEG_WORDS): if label == '正向': return '负向' elif label == '负向': return '正向' return label

注意这个规则的副作用:「不负责任」本来是负向,会被误判为正向。所以更稳妥的做法是只对得分介于 0.35~0.65 之间且含否定词的文本做翻转,避免用力过猛。我常用的策略是先直接分类,再看混淆矩阵——把随机抽样的 200 条人工标注,计算准确率,如果低于 70%,再考虑加否定词规则或换模型。

3.5 结果导出与复核

情感分析结果建议写回 CSV 新增列,不要覆盖原始数据。最终article_comments_analyzed.csv包含原始字段、情感标签和得分三部分,答辩时可以展示「原始评论 + 自动标签」的对照表。同时在可视化页面上提供一个「随机抽样」功能,从每类情感中随机抽 5 条展示,让老师逐条看是否合理。

4. Flask + ECharts 可视化:目标词检索、情感占比与趋势图实现

4.1 项目结构拆分

系统后端用 Flask 提供 JSON 接口,前端用 ECharts 生成图表。目录组织如下,与项目中的多个 CSS 文件对应,静态资源分离。

weibo_analysis/ ├── app.py # Flask 入口 ├── spider.py # 爬虫模块 ├── sentiment.py # 情感分析模块 ├── data/ │ ├── target.csv │ ├── articleData.csv │ └── articleComments.csv ├── templates/ │ └── index.html └── static/ ├── css/ # main.css main3.css 等 └── js/

4.2 Flask 数据接口设计

前端需要三类数据:总体情感占比、情感趋势、高频词。Flask 路由读取处理好的 CSV,按参数返回 JSON。

from flask import Flask, jsonify, render_template import pandas as pd app = Flask(__name__) @app.route('/') def index(): return render_template('index.html') @app.route('/api/summary/<keyword>') def summary(keyword): df = pd.read_csv('data/analyzed.csv', encoding='utf-8-sig') sub = df[df['keyword'] == keyword] pie_data = sub['sentiment'].value_counts().to_dict() # 转换 ECharts 格式 pie_data = [{'name': k, 'value': v} for k, v in pie_data.items()] return jsonify(pie_data)

to_dict()返回的 key 是情感类别字符串,但 ECharts 的 pie 系列需要{name, value}对象数组,所以必须做一次转换。这里有个细节:value_counts()默认按数量排序,如果某类情感数量为 0,结果里就不包含这个 key,前端饼图会缺一个扇区,可以在前端做默认填充,也可以在后端补零。

4.3 趋势图:按天聚合

评论的情感趋势需要按时间聚合,图表展示每天的正负中三条折线。先把pub_time转成日期,再分组:

@app.route('/api/trend/<keyword>') def trend(keyword): df = pd.read_csv('data/analyzed.csv', encoding='utf-8-sig') sub = df[df['keyword'] == keyword].copy() sub['date'] = pd.to_datetime(sub['pub_time']).dt.date trend = sub.groupby(['date', 'sentiment']).size().unstack(fill_value=0) trend = trend.reset_index() result = { 'dates': trend['date'].astype(str).tolist(), 'positive': trend.get('正向', [0]*len(trend)).tolist(), 'negative': trend.get('负向', [0]*len(trend)).tolist(), 'neutral': trend.get('中性', [0]*len(trend)).tolist() } return jsonify(result)

注意trend.get('正向', [0]*len(trend))很有必要。如果某一天没有正向评论,unstack后那一列不存在,直接trend['正向']会报 KeyError。用get兜底能避免接口崩溃,前端拿到的数组长度也和日期对齐。

4.4 词云与热词提取

高频词展示用jieba分词,排除停用词,然后取 top 30 渲染到词云。由于 ECharts 没有官方词云组件,可以用wordcloud2或 ECharts 的 scatter 模拟。后端只负责提供词频列表:

import jieba from collections import Counter STOP_WORDS = set('微博 转发 分享 回复 评论 https weibo com'.split()) @app.route('/api/words/<keyword>') def words(keyword): df = pd.read_csv('data/analyzed.csv', encoding='utf-8-sig') sub = df[df['keyword'] == keyword] texts = ' '.join(sub['content'].tolist()) tokens = [w for w in jieba.cut(texts) if w.strip() and len(w) > 1 and w not in STOP_WORDS] counter = Counter(tokens).most_common(30) return jsonify([{'name': w, 'value': n} for w, n in counter])

jieba.cut默认使用精确模式,切出来的词大多是名词和动词,能直接反映舆论焦点。len(w) > 1过滤单字词,避免「的」「了」「是」等高频无意义字干扰。停用词表不要写死在代码里,放stopwords.txt更便于扩展。

4.5 前端 ECharts 渲染逻辑

index.html里通过 fetch 请求接口,拿到数据后动态生成图表。概览页包含四个卡片:舆情总数、正向数、负向数、中性数,以及两个主体图表。

async function loadData(keyword) { const [summary, trend, words] = await Promise.all([ fetch(`/api/summary/${keyword}`).then(res => res.json()), fetch(`/api/trend/${keyword}`).then(res => res.json()), fetch(`/api/words/${keyword}`).then(res => res.json()) ]); renderPie(summary); renderLine(trend); renderWordCloud(words); }

这里用Promise.all并行请求三个接口,避免串行等待。注意要处理关键字中含中文的情况,前端需要encodeURIComponent(keyword),否则 URL 里的中文会被浏览器自动编码,后端request.path拿到的可能不是原始中文。

4.6 交互筛选与图表联动

页面左侧是目标词下拉框,从target.csv读取;切换目标词时,所有图表联动刷新。ECharts 实例要复用,不能每次 new 一个,否则内存泄漏,切换多次后页面卡顿。正确做法是初始化一次,后续用setOption更新:

const pieChart = echarts.init(document.getElementById('pieChart')); pieChart.setOption({ series: [{ type: 'pie', data: summary }] });

如果图表容器最初是隐藏的,echarts.init拿到的宽度是 0,渲染出来是空白。解决方法是初始化时传入容器宽度,或者在显示后再resize()

5. 部署与排错:环境配置、依赖安装与 Cookie 失效处理

5.1 本地部署三步走

环境建议 Python 3.8 以上版本,不要用 3.12 以下太旧,避免jiebalxml的编译问题。部署流程归纳为三个命令:

pip install flask pandas requests beautifulsoup4 snownlp jieba python spider.py python app.py

第一行安装所有依赖。beautifulsoup4在代码里用bs4导入,注意包名和导入名的差异。python spider.py会先抓取网页数据并落到 CSV,这个步骤不要省略,否则可视化界面打开是空的。python app.py启动后访问http://127.0.0.1:5000

5.2 常见异常与排查对照表

下面是这个项目最常遇到的五种报错,以及对应的处理方式。

报错信息原因解决办法
ModuleNotFoundError: No module named 'bs4'没装 beautifulsoup4pip install beautifulsoup4
KeyError: '正向'CSV 中不存在该情感列检查情感分析是否已执行,或在代码用 get 兜底
UnicodeDecodeErrorCSV 编码与读取不一致统一用encoding='utf-8-sig'gbk
HTTP 412Cookie 失效或频率过快重新登录微博获取 Cookie,增加 sleep 间隔
templates/404Flask 没找到模板确认templates目录和index.html是否存在

5.3 一个实用技巧:批量切换目标词自动生成报告

做毕业设计答辩时,老师大概率会问「换个关键词还能用吗」。为了避免现场重新跑爬虫,我写了个小函数,遍历target.csv里所有词,抓完数据后分别生成各自的趋势图 JSON,导出成静态 HTML 文件。这样答辩时可以直接打开预生成好的报告,不需要现场联网。

def generate_report(keyword): # 抓取数据 html = fetch_weibo(keyword, cookie) items = parse_html(html) save_articles(items, f'data/{keyword}.csv') # 情感分析与聚合 article_senti = articles_df.copy() article_senti['sentiment'] = article_senti['content'].apply(classify_sentiment) # 保存结果 article_senti.to_csv(f'data/{keyword}_analyzed.csv', index=False, encoding='utf-8-sig')

这个函数把爬虫、情感分析、结果导出串成一条链,跑完一个关键词就产出一个 CSV,前后端都从这些文件里读数据。日常调试时,我建议每次抓取数据后先看 CSV 的行数,如果不足 50 条,先查 Cookie 是不是过期了,再查目标词是否过于冷门。调整好采集这一步,后面所有分析才有数据支撑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询