基于Python的网络舆情分析系统设计与实现
2026/9/1 20:10:23 网站建设 项目流程

简介:本资源是一套完整的基于Python的网络舆情分析系统毕业设计实现,面向计算机、通信、人工智能及自动化等相关专业的本科生与教师,适用于课程设计、大作业及毕业设计等实践场景。系统采用Django框架构建Web界面,集成CNN/LSTM模型进行情感分析,并包含词向量(sgns.zhihu.bigram)、预训练模型文件(.pb、.index)、数据集(hotel_comment)及可视化截图等核心模块,代码经实际调试运行验证,答辩评分高达98分。压缩包共36个文件,含13个Python源码、10个文本说明与配置文件、2个模型权重文件、2个索引文件、2个词向量数据文件及图像、Markdown文档等,整体大小为298.36MB。目前已有575人学习下载,配套文档详尽、结构清晰,既可作为零基础入门范例,也支持进阶用户二次开发与功能拓展,具备扎实的工程参考价值与教学示范意义。 又是一年毕业设计季,后台收到好几个同学留言问“基于Python的网络舆情分析系统”这个题怎么做。这个题目确实很经典,它把爬虫、文本挖掘、情感分析、可视化展示全部串起来了,技术栈完整,业务场景清晰,不管是对找工作还是读研都有帮助。我用一个实际跑通的毕业设计项目来讲讲整个系统怎么拆、怎么做,从架构设计到核心代码再到踩坑记录,完整走一遍。

1. 选题定位与系统整体架构设计

1.1 网络舆情分析系统到底在解决什么问题

网络舆情分析系统,核心任务是从互联网上抓取特定主题的文本数据,然后对数据进行清洗、分词、情感判断,最后通过图表把舆论倾向和热度趋势呈现出来。说得直白一点,就是帮用户回答三个问题:大家都在聊什么、情绪是正面还是负面、热度是涨还是跌。

毕业设计做这个题有几个天然优势。第一,数据源开放,新闻网站、微博评论区、论坛帖子都可以作为采集目标,不需要对接商业API;第二,技术链路完整,从requests请求到jieba分词再到snownlp情感分析,每个环节都有自己的技术点,论文和答辩都有内容可讲;第三,演示效果好,最终成果是一个带Web界面的系统,输入一个关键词就能看到词云、情感饼图、趋势折线图,老师一眼就能看懂做了什么。

1.2 技术选型:为什么最终选择这套Python组合方案

我见过不少同学一开始就上Scrapy + Elasticsearch + Bert模型,结果做了两个月连数据都没跑通,最后草草交差。毕业设计的核心原则是:在有限的周期内,把每个环节做到完整、可解释、能演示。所以我的技术选型遵循了一条“够用且不复杂”的思路。

Web框架选Flask。Django虽然自带Admin后台和ORM,但它的重量级对于一个舆情分析项目来说是负担,Flask的路由和模板机制足够撑起整个前端展示。

数据采集用requests + BeautifulSoup。Scrapy的并发能力强,但它的Spider中间件、Item Pipeline这些概念需要额外学习成本,而且对于毕业设计的采集量级(几千到几万条)完全用不上。requests的代码直观,出了问题容易排查。

情感分析用SnowNLP。它内置了一个中文情感分类模型,输入一句话返回0到1之间的情感倾向值,0到0.5是消极,0.5到1是积极。为什么不直接用深度学习模型?因为需要标注数据集做微调,数据准备的工作量远超系统本身。SnowNLP面向通用语料训练,在电商、新闻场景下准确率能到七成左右,作为毕业设计已经够用,后面通过自定义情感词典做补充修正。

可视化方面,词云用wordcloud + jieba配合生成,趋势图和饼图用ECharts。ECharts的图表交互效果好,鼠标悬停能显示具体数值,答辩演示时加分明显。

1.3 四层架构设计:数据从采集到展示的完整流转路径

整个系统我分了四层,每一层的职责非常明确,层与层之间只通过数据传递,互不耦合。

数据采集层负责从目标网站抓取网页内容,包括新闻标题、正文、发布时间、来源等字段。数据存储层统一落地到MySQL,表结构在设计时就预留了情感分析的字段。分析处理层负责对文本进行清洗、分词、情感打分,这个阶段产出的数据直接决定可视化的准确性。可视化展示层从数据库读取处理结果,渲染出趋势图、词云、情感分布图。

这种分层模式的好处有两个。一是每一层都可以单独测试,爬虫挂了不影响已经入库的数据,情感分析代码改动了也不需要重新抓取数据,开发效率高;二是后期扩展方便。比如想增加数据源,只需要在采集层加一个crawler,其他三层完全不用动。

2. 核心模块详细拆解:每个环节的关键实现细节

2.1 数据采集层:爬虫代码怎么写得既稳定又不被拦截

采集层是整个系统的数据入口,也是踩坑最多的地方。很多同学写爬虫一上来就写死了一个固定的User-Agent,结果请求几十次之后就被对方服务器拦截,然后就跑来问我是不是IP被封了。

我的爬虫模块用了一个基础的请求函数,每次请求都从User-Agent池里随机选一个,配合随机的延时时间,尽量避免触发对方的风控机制。

import requests import random import time from bs4 import BeautifulSoup USER_AGENTS = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36" ] def fetch_page(url): headers = { "User-Agent": random.choice(USER_AGENTS), "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3" } try: resp = requests.get(url, headers=headers, timeout=10) resp.encoding = resp.apparent_encoding resp.raise_for_status() except requests.RequestException as e: print(f"[请求失败] {url}, 错误: {e}") return None time.sleep(random.uniform(1, 3)) return resp.text

这段代码里有两个容易忽略的地方。第一个是resp.encoding = resp.apparent_encoding,requests库在猜测编码时经常会出错,导致中文变成乱码,用apparent_encoding让requests根据网页内容实际检测编码,能规避大部分乱码问题。第二个是每次请求后time.sleep(random.uniform(1, 3)),1到3秒的随机间隔,既不会给对方服务器造成压力,也让请求行为看起来更像人工浏览。

拿到HTML之后,用BeautifulSoup解析提取数据。以新闻列表页为例,标题通常在h2或者h3标签下的a里面,正文在divclass属性里。解析时建议先打印出HTML结构确认选择器,不要凭感觉猜。

def parse_news_list(html, base_url): soup = BeautifulSoup(html, "html.parser") news_items = [] for item in soup.select("div.news-item"): title_tag = item.select_one("h3 a") if not title_tag: continue title = title_tag.get_text(strip=True) link = title_tag.get("href") if link.startswith("/"): link = base_url + link date_tag = item.select_one("span.date") publish_time = date_tag.get_text(strip=True) if date_tag else "" news_items.append({ "title": title, "url": link, "publish_time": publish_time }) return news_items

采集层有一个设计细节:列表页只拿标题和链接,正文内容在下一层解析时补齐。这样做的原因是列表页响应快、数据量小,先快速拿到一批链接,然后再逐个进入详情页抓正文。如果列表页和详情页一起解析,一旦某个详情页超时,整个采集流程都会卡住。

2.2 数据存储层:MySQL表结构设计与入库逻辑

数据库设计是很多毕业设计容易翻车的地方。如果表结构设计得不好,后期写查询SQL时会非常痛苦。我设计的news表如下:

CREATE TABLE news ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL COMMENT '新闻标题', content TEXT COMMENT '正文内容', source VARCHAR(50) DEFAULT '' COMMENT '来源网站', url VARCHAR(500) DEFAULT '' COMMENT '原文链接', publish_time VARCHAR(50) DEFAULT '' COMMENT '发布时间', sentiment_score FLOAT DEFAULT 0 COMMENT '情感得分, 0-1, 小于0.5为负面', sentiment_label VARCHAR(10) DEFAULT 'neutral' COMMENT '情感标签: positive/neutral/negative', keyword VARCHAR(50) DEFAULT '' COMMENT '搜索关键词', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '入库时间', UNIQUE KEY uk_url (url(255)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里有两个特别重要的设计决策。

第一个是给url加了唯一索引,这是为了防止同一篇新闻被重复采集。爬虫在翻页过程中,经常会出现不同列表页包含同一篇新闻的情况,没有唯一索引的话,入库数据会有大量重复,情感分析和统计结果都会被带偏。

第二个是publish_time用了VARCHAR而不是DATETIME。原因很简单:不同网站的日期格式五花八门,有的是“2024-05-20 10:30”,有的是“5小时前”,还有的是“2024年5月20日”。在入库阶段强行统一格式会引入很多异常处理逻辑,不如先原样存字符串,在分析阶段再统一转成标准格式。

写入逻辑在SQL层面直接做去重,用INSERT IGNORE配合唯一索引,重复记录直接跳过,代码里不需要再做一次查重查询。

def save_news(item, keyword): sql = """ INSERT IGNORE INTO news (title, content, source, url, publish_time, keyword) VALUES (%s, %s, %s, %s, %s, %s) """ cursor.execute(sql, ( item["title"], item["content"], item["source"], item["url"], item["publish_time"], keyword ))

2.3 文本处理层:数据清洗与jieba分词的正确姿势

从网页抓下来的正文首先要去掉HTML标签、特殊符号和广告噪声。我写了一个文本预处理函数,顺序很重要:先解HTML实体,再去标签,最后清理冗余字符。

import re import html def clean_text(raw_text): # 1. 解HTML实体,比如 &amp; -> & text = html.unescape(raw_text) # 2. 去标签 text = re.sub(r'<[^>]+>', '', text) # 3. 去脚本和样式内容 text = re.sub(r'<(script|style)[^>]*>.*?</\1>', '', text, flags=re.S) # 4. 去空格和特殊符号 text = re.sub(r'\s+', '', text) text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9,。!?、;:""''()]', '', text) return text.strip()

这里要注意,清理顺序不能乱。如果先去标签再去HTML实体,有些标签属性里的&amp;可能会被提前转换,导致后面的正则匹配失败。先处理实体、再处理标签,能保证标签结构完整,匹配更可靠。

分词用的是jieba,但直接对全文分词会得到大量无关词。比如“我们”“但是”“因为”这类停用词,以及“可以”“一下”这类口语词,需要加载停用词表过滤。

import jieba import jieba.analyse def segment(text, stopwords_file="stopwords.txt"): with open(stopwords_file, "r", encoding="utf-8") as f: stopwords = set(line.strip() for line in f) words = jieba.lcut(text) words = [w for w in words if w not in stopwords and len(w.strip()) > 1] return words

这里有一个经验:len(w.strip()) > 1这个条件可以过滤掉大量单个字。中文里单字词的信息量很低,而且高频单字(如“的”“了”“是”)往往在停用词表里已经存在,但难免有漏网之鱼,放在分词之后统一过滤效果更好。

生成词云时,还可以用jieba.analyse.extract_tags按TF-IDF权重提取关键词,这样词云不会出现“频率高但无实际含义”的词,视觉上更有信息含量。

2.4 情感分析引擎:SnowNLP配合自定义词典的效果优化

情感分析是整个系统的“灵魂”模块。SnowNLP用起来非常简单,几行代码就能跑:

from snownlp import SnowNLP text = "这家公司发布的新产品获得了用户一致好评" s = SnowNLP(text) print(s.sentiments) # 输出0到1的值

但是直接这样用会遇到一个尴尬的问题:SnowNLP对新闻文本的适应度不高。因为SnowNLP最初是面向电商评论训练的,对“正面”“负面”的判断逻辑偏向口语化表达。比如“这款产品价格贵但质量好”这样的句子,SnowNLP往往会判成负面,因为“贵”字在训练语料里是高频负面词。

所以我加了自定义情感词典来修正。具体做法是维护一个custom_sentiment_dict.txt文件,每一行一个词加上情感分,取值为-1到1:

暴跌, -0.9 暴涨, 0.9 稳中有升, 0.6 不尽人意, -0.6 强烈推荐, 0.8 严重亏损, -0.9

在计算情感得分时,先判断文本中是否包含词典中的词,如果包含就对原始得分做偏移修正:

def analyze_sentiment(text): base_score = SnowNLP(text).sentiments adj_score = 0.0 with open("custom_sentiment_dict.txt", "r", encoding="utf-8") as f: for line in f: word, score = line.strip().split(",") if word in text: adj_score += float(score) final_score = max(0.0, min(1.0, base_score + adj_score)) if final_score > 0.6: label = "positive" elif final_score < 0.4: label = "negative" else: label = "neutral" return final_score, label

阈值为什么选0.6和0.4?这是因为SnowNLP的输出集中在0.3到0.7之间,大部分文本评分都在中间地带徘徊。如果阈值设成0.5,会导致大量中立文本被误判为正面或负面。把中间带拉宽,只对明确的情感信号做二分类,整体的准确率反而更高。这个调参过程在论文里也可以作为一个亮点来分析。

另外一个优化思路:短文本的情感判断比长文本更准确。对于整篇新闻正文,建议把正文按句号分句,逐句算情感得分,最后取平均值。整篇文本一锅端的话,正负情绪相互抵消,最后得分趋近0.5,完全没有区分度。

import re def analyze_long_text(text): sentences = re.split(r'[。!?]', text) sentences = [s for s in sentences if len(s) > 5] if not sentences: return 0.5, "neutral" scores = [analyze_sentiment(s)[0] for s in sentences] avg_score = sum(scores) / len(scores) if avg_score > 0.6: return avg_score, "positive" elif avg_score < 0.4: return avg_score, "negative" else: return avg_score, "neutral"

2.5 可视化展示层:Flask + ECharts的组合

可视化展示层,我用Flask提供Web服务和数据接口,前端页面用ECharts渲染图表。整个页面分三个区块:顶部是搜索框,可以输入关键词重新采集和分析;中间是情感分布饼图和热度趋势折线图;底部是词云图。

Flask的后端需要提供两个接口:一个用于触发采集和分析任务,一个用于读取分析结果并返回JSON。

from flask import Flask, render_template, request, jsonify import json app = Flask(__name__) @app.route("/") def index(): return render_template("index.html") @app.route("/api/analyze", methods=["POST"]) def analyze(): keyword = request.json.get("keyword", "") # 触发采集任务 crawl_and_analyze(keyword) # 返回分析结果 result = get_analysis_result(keyword) return jsonify(result) @app.route("/api/trend") def trend(): keyword = request.args.get("keyword", "") data = get_trend_data(keyword) return jsonify(data)

ECharts的数据格式是JSON,所以后端要处理成ECharts能直接消费的结构。比如饼图的格式是[{"name": "正面", "value": 120}, {"name": "负面", "value": 30}, ...],折线图的数据格式是{"dates": ["05-01", "05-02"], "positive_count": [10, 15], "negative_count": [3, 5]}

词云图我用的是wordcloud库生成图片,然后把图片Base64编码传给前端,或者保存为静态文件直接引用。这里有一个坑:wordcloud默认对中文支持不好,必须指定中文字体路径,比如Windows下的C:\Windows\Fonts\simhei.ttf

from wordcloud import WordCloud import matplotlib.pyplot as plt def generate_wordcloud(keyword): words = get_keywords_from_db(keyword) text = " ".join(words) wc = WordCloud( font_path="C:/Windows/Fonts/simhei.ttf", width=800, height=600, background_color="white", max_words=200 ) wc.generate(text) wc.to_file("static/wordcloud.png")

词云图之前还有一个容易被忽略的步骤:生成图片之前,要确认数据库中是否真的有分词后的数据。如果某次采集只抓到了列表页,正文还没来得及抓取,数据库里的content字段是空的,分词结果自然也是空的,词云图就会生成一张纯白图片。

3. 完整跑通全流程:环境搭建到一键启动

3.1 开发环境与依赖清单

这个项目在Windows 10上完成开发,Python版本3.8。建议使用虚拟环境隔离依赖,避免污染全局Python环境。

在项目根目录执行以下命令创建虚拟环境并安装依赖:

python -m venv venv # Windows下激活 venv\Scripts\activate # Linux/Mac下激活 source venv/bin/activate pip install flask requests beautifulsoup4 jieba snownlp pandas pymysql wordcloud

有个细节要单独说:pandas在这个项目里不是必须的,但我在做时间序列聚合的时候用了它。比如要按天统计正负面新闻数量,用pandas的groupby比手写SQL循环高效很多,代码可读性也好。

3.2 数据库初始化

在MySQL中创建数据库和表:

mysql -u root -p
CREATE DATABASE sentiment_system DEFAULT CHARACTER SET utf8mb4; USE sentiment_system; -- 将上文面的CREATE TABLE语句粘贴执行

数据库字符集一定要用utf8mb4,如果用了utf8,某些特殊字符(比如生僻字和表情符号)入库时会报错。这个坑非常隐蔽,我一开始用默认字符集,爬虫抓取的数据入库时报Incorrect string value错误,排查了半天才发现是字符集问题。

3.3 核心代码文件组织

我的项目目录结构如下:

sentiment_system/ ├── app.py # Flask主程序 ├── config.py # 数据库和爬虫配置 ├── crawler.py # 爬虫模块 ├── analyzer.py # 文本清洗、分词、情感分析 ├── storage.py # 数据入库、查询 ├── templates/ │ └── index.html # 前端页面 ├── static/ │ ├── echarts.min.js │ └── wordcloud.png # 生成的词云图 ├── stopwords.txt # 停用词表 ├── custom_sentiment_dict.txt # 自定义情感词典 └── requirements.txt

config.py里保存数据库连接信息和爬虫请求信息,这些配置集中管理,后续修改不用在代码里到处找:

DB_CONFIG = { "host": "localhost", "user": "root", "password": "your_password", "database": "sentiment_system", "charset": "utf8mb4" } CRAWLER_CONFIG = { "target_url": "https://news.example.com", "max_pages": 10, "delay_min": 1, "delay_max": 3 }

3.4 启动系统并验证全流程

启动前先确认MySQL服务已开启,然后运行:

python app.py

浏览器访问http://127.0.0.1:5000,输入一个关键词比如“人工智能”,点击分析按钮。系统会依次执行:爬虫采集→文本入库→分词清洗→情感分析→可视化渲染。整个过程可能需要几十秒到几分钟,取决于抓取的文章数量。

我建议第一次测试时把max_pages设小一点,比如3页,先跑通流程再加大采集量。我曾经在调试阶段设置抓取50页,结果某个网页卡死,整个流程卡了几分钟没有响应,最后发现是某条数据的URL不规范导致请求超时。先小规模验证流程,再逐步扩容,这是做爬虫项目的基本习惯。

4. 常见问题与排查技巧实录

4.1 中文乱码问题

爬虫抓取网页后返回的中文全是乱码,这是最常见的问题。原因在于部分网站的响应头没有正确声明编码,requests默认按ISO-8859-1解码导致乱码。

解决办法是使用resp.apparent_encoding重新检测编码,或者在请求头中指定Accept-Charset: utf-8。如果依然乱码,可以手动指定编码,比如resp.encoding = 'gbk',因为不少国内老站还在用GBK编码。先打印resp.apparent_encoding看看检测结果是什么,再决定用哪种编码。

4.2 采集被拦截

页面详情请求正常,但到了第几十页就被返回403。这通常不是IP被封,而是没有带Cookie或Referer头。某些网站的服务器会校验Referer和请求来源。

一个简单有效的方法是先从浏览器复制完整请求头,包括Cookie、Referer、Sec-Fetch-*等字段,在requests里原样带上。我用这种方法处理过来自同一个IP连续采集上千条数据被拦截的情况。如果目标网站对请求频率特别敏感,把延时时间从1到3秒提高到3到6秒,采集速度慢一点但胜在稳定。

4.3 情感分析结果明显不准

我在测试中发现SnowNLP把“这个手机性价比很高,强烈推荐”判成了0.3分(负面)。原因前面提到了,SnowNLP的语料是电商评论,对“推荐”“性价比”这类词在特定语境下的权重不够。

解决方式是调自定义情感词典,把“强烈推荐”加入正面词典,权重设为0.8。词典的维护是个持续迭代的过程,跑一批新闻出来看一下误判样本,把高频的误判词加入词典。另外,新闻类文本中很多表达是中性的“据悉”“据报道”,这些词不影响情感判断,不用特殊处理。

4.4 可视化图表数据显示为空

词云图为空,或者折线图没有数据。这种情况优先检查MySQL中的数据。控制台里执行:

SELECT COUNT(*) FROM news;

如果数量为0,说明采集环节就没成功;如果数量不为0,但content字段为空,说明爬虫只写了标题没写正文。还有一种情况是sentiment_score字段全为0,说明情感分析环节可能没跑或者报错被吞掉了。这种问题一定是数据结构问题,优先查库,不要先去改前端代码。

4.5 使用过程中的体验优化与后续扩展

跑通这个系统之后,如果想让毕业设计更高一个档次,可以在几个方向上做扩展。

第一个是增加定时采集能力。目前系统是手动触发,改进后可以用APScheduler做定时任务,每天自动采集一次,长期积累数据后就能生成月度舆情报告,这个功能在真实业务场景中非常实用。

第二个是引入更丰富的数据源。目前只采新闻网站,可以扩展微博评论、微信公众号文章、知乎回答等。不同的数据源有不同的抓取方式和文本结构,这本身就是很好的论文素材。

第三个是升级情感分析模型。如果用SnowNLP的准确率无法满足需求,可以尝试用预训练的BERT模型做情感二分类。不需要从零训练,用transformers库加载开源的bert-base-chinese模型,在少量标注数据上微调即可。

5. 毕设答辩与技术文档的整理思路

系统代码写完不是结束,毕业设计的最终成绩很大一部分取决于论文和答辩展示。结合这个项目,我建议论文框架按“系统需求分析-关键技术研究-系统设计与实现-测试与结果分析”四个章节展开,其中技术研究部分重点写情感分析原理和文本预处理流程,测试部分对比不同情感分析方案的效果差异。

答辩时,演示流程建议控制在5分钟以内:输入关键词→展示采集过程日志→展示数据库中的结构化数据→展示情感分布饼图、趋势图、词云→说明系统架构图和核心技术点。把流程走通比讲一堆理论更有说服力。

文档说明这一块,如果源码是网上找的或者从学长那里要来的,一定要自己把整个项目跑通一遍,把依赖环境、数据库配置、运行步骤重新整理成自己的README。很多同学拿到的源码本身是可用的,但缺少环境配置说明,导致在自己的电脑上跑不起来。我拿到任何一份毕设源码,第一件事就是创建虚拟环境、安装依赖、配置数据库,然后把启动命令一步步记录下来,这才是真正掌握了这份代码。

我个人在实际操作中的体会是,网络舆情分析系统这个题目之所以值得做,不是因为技术有多前沿,而是它把一个完整的数据闭环呈现了出来。你从Web上收集原始数据,经过清洗和分析,最终转换成有业务价值的可视化信息,这个过程本身就是数据分析工作的缩影。把每个环节做扎实,比盲目追求花哨的算法模型更重要。

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

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

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

立即咨询