简介:这是一套面向高校课程设计与Python进阶学习者的舆情分析实战项目源码,围绕网易新闻及其用户评论构建完整的数据采集与分析链路,帮助读者理解从爬虫抓取到情感研判的工程化落地过程。项目涵盖新闻标题与评论的定期抓取、热点话题追踪、基于NLP的情感倾向判断、趋势预测、关键词提取以及图表与时间线可视化,并支持自定义监测词、结果订阅和自动生成舆情报告,技术栈涉及Scrapy或BeautifulSoup、jieba、Gensim、Pandas、NumPy及Matplotlib等。压缩包为zip格式,整体约23.84MB,文件总数与类型明细上游暂未提供,但按项目结构应包含Python源码、依赖配置与数据存储相关文件。目前已有160人学习下载,适合需要课程设计参考、NLP入门实践或舆情系统搭建思路的读者,可据此快速理解模块划分、数据流转与可视化呈现方式。
1. 网易新闻评论舆情分析平台:一份能跑通全链路的 Python 课程设计源码
课程设计选题里,「舆情分析」这四个字命中率极高,但真正能跑通的没几个。多数人卡在第一步——数据从哪来。这份基于网易新闻评论的舆情热点分析平台,把爬虫、分词、情感分析、可视化串成了一条完整链路,不是那种只贴几个函数的半成品。它解决的核心问题是:给定一批新闻和评论,自动告诉你哪些话题在升温、公众情绪偏正向还是负向、关键词是什么。适合正在做课程设计的学生、需要快速搭舆情原型的开发者,以及想找一个完整 Python 数据分析项目练手的人。拿到手之后,你要做的不是从头写,而是理解每个模块的输入输出,然后按自己的选题替换数据源和分析维度。
2. 环境搭建与项目结构:从 Python 安装到依赖对齐
2.1 为什么选 Python 3.8+ 而不是最新版
这个项目的依赖链里有 jieba、gensim、pandas、matplotlib 这几个核心库。gensim 在 Python 3.12 上的 wheel 支持并不总是及时,常见做法是锁在 3.8 到 3.10 之间。我一般会推荐 3.9,兼容性最稳。如果你用的是 python 官网下载的最新版,大概率会在pip install gensim那一步翻车,报编译错误。
安装 Python 时记得勾选「Add Python to PATH」,否则后面在命令行里敲python会提示找不到命令。装完之后验证:
python --version pip --version两条命令都能正常输出版本号,说明基础环境没问题。如果pip版本过旧,先升级:
python -m pip install --upgrade pip这一步的逻辑是:后续安装依赖时,旧版 pip 可能无法正确解析某些包的依赖关系,导致装了一半报错。参数上没什么可调的,就是确保 pip 是最新的。
2.2 虚拟环境与依赖安装
不要直接在全局环境里装依赖。课程设计项目往往依赖版本比较旧,全局装容易和你已有的其他项目冲突。用 venv 隔离:
python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate激活后命令行前面会出现(venv)标识。接下来安装依赖。项目一般会带一个requirements.txt,直接:
pip install -r requirements.txt如果没有这个文件,手动装核心库:
pip install requests beautifulsoup4 jieba gensim pandas numpy matplotlib seaborn flask这里解释几个关键库的角色:requests负责发 HTTP 请求拿网页;beautifulsoup4解析 HTML 提取新闻标题和评论;jieba做中文分词;gensim用来构建 LDA 话题模型;pandas和numpy做数据清洗和统计;matplotlib和seaborn出图;flask如果项目带 Web 界面的话会用到。
提示:如果
gensim安装报错,先确认是否装了 C++ 编译环境。Windows 上需要 Visual C++ Build Tools,Linux 上需要gcc和python3-dev。
2.3 项目目录结构与模块职责
拿到源码包后,先别急着跑。花五分钟把目录结构看清楚,后面排查问题会快很多。典型结构大致如下:
project/ ├── spider/ # 爬虫模块 │ ├── news_spider.py │ └── comment_spider.py ├── analysis/ # 分析模块 │ ├── keyword_extract.py │ ├── sentiment.py │ └── topic_model.py ├── visualization/ # 可视化模块 │ └── charts.py ├── data/ # 数据存储 │ └── news.db ├── app.py # 入口 / Web 服务 └── requirements.txt每个模块的职责是独立的:爬虫只管抓数据入库,分析模块从库里读数据做处理,可视化模块再把分析结果画出来。这种分层设计的好处是,你想换数据源,只改 spider 目录就行;想换分析算法,只动 analysis 目录。
2.4 数据库初始化与首次运行
项目默认用 SQLite 的话,不需要额外装数据库服务,直接跑就行。如果是 MySQL 或 PostgreSQL,先建库建表:
CREATE DATABASE opinion_analysis DEFAULT CHARACTER SET utf8mb4;然后在项目的配置文件里改连接串。常见做法是把数据库配置放在config.py或.env文件里,不要硬编码在代码中。
首次运行建议按顺序来:先跑爬虫脚本抓一批数据,再跑分析脚本,最后启动可视化。如果直接启动 Web 服务而数据库是空的,页面会一片空白,这不是 bug,是没数据。
3. 爬虫模块拆解:新闻与评论数据的抓取策略
3.1 网易新闻页面结构分析与请求构造
网易新闻的页面结构这几年改过几次,但核心思路不变:新闻正文在一个容器里,评论通过单独的接口异步加载。所以爬虫要分两步走——先抓新闻页面拿标题和正文,再请求评论接口拿评论数据。
新闻页面的请求比较简单:
import requests from bs4 import BeautifulSoup def fetch_news(url): headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36" } resp = requests.get(url, headers=headers, timeout=10) resp.encoding = "utf-8" soup = BeautifulSoup(resp.text, "html.parser") title = soup.select_one("h1.post_title").get_text(strip=True) content = soup.select_one("div.post_body").get_text(strip=True) return {"title": title, "content": content}这段代码的逻辑是:构造带 User-Agent 的请求头模拟浏览器访问,拿到 HTML 后用 BeautifulSoup 解析,通过 CSS 选择器定位标题和正文。timeout=10是防止某个请求卡死导致整个脚本挂起。resp.encoding = "utf-8"是手动指定编码,因为有些页面返回的 header 里编码声明不准确,不设的话中文会乱码。
参数方面,User-Agent 建议用一个真实的浏览器标识,不要用默认的python-requests,否则容易被识别。timeout 一般设 10 到 15 秒,太短容易误判超时,太长出问题时等得久。
3.2 评论接口的翻页与增量抓取
评论数据不在新闻页面的 HTML 里,而是通过一个独立的 JSON 接口返回。常见做法是打开浏览器开发者工具,切到 Network 面板,翻一页评论,找到那个返回 JSON 的请求,把 URL 模式和参数记下来。
import json import time def fetch_comments(news_id, page=1): base_url = "https://comment.api.example.com/api/v1/comments" params = { "newsId": news_id, "page": page, "pageSize": 20, "sort": "newest" } headers = {"User-Agent": "Mozilla/5.0 ..."} resp = requests.get(base_url, params=params, headers=headers, timeout=10) data = resp.json() comments = [] for item in data.get("comments", []): comments.append({ "user": item.get("user", {}).get("nickname", ""), "content": item.get("content", ""), "time": item.get("createTime", ""), "likes": item.get("vote", 0) }) return comments def crawl_all_comments(news_id, max_pages=50): all_comments = [] for page in range(1, max_pages + 1): batch = fetch_comments(news_id, page) if not batch: break all_comments.extend(batch) time.sleep(1.5) # 控制请求频率 return all_comments逻辑说明:fetch_comments负责单页请求和字段提取,crawl_all_comments负责翻页循环。time.sleep(1.5)是必须的,不加的话连续快速请求很容易触发限流,表现为返回空数据或者 403。max_pages=50是安全上限,防止某个新闻的评论异常多导致脚本跑几个小时。
参数上,pageSize一般设 20 到 50,太大可能被服务端拒绝。sort参数决定评论排序方式,newest按时间倒序,hot按热度排序,做舆情分析建议用newest,能拿到最新的舆论动态。
3.3 数据入库与去重
抓到的数据要存起来,不然后面分析模块没数据可读。用 SQLite 的话:
import sqlite3 def init_db(db_path="data/news.db"): conn = sqlite3.connect(db_path) cursor = conn.cursor() cursor.execute(""" CREATE TABLE IF NOT EXISTS news ( id INTEGER PRIMARY KEY AUTOINCREMENT, news_id TEXT UNIQUE, title TEXT, content TEXT, crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) cursor.execute(""" CREATE TABLE IF NOT EXISTS comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, news_id TEXT, user TEXT, content TEXT, comment_time TEXT, likes INTEGER DEFAULT 0, UNIQUE(news_id, user, content) ) """) conn.commit() return connUNIQUE约束是关键。爬虫跑多次的时候,同一条新闻和评论不应该重复入库。INSERT OR IGNORE配合 UNIQUE 约束就能实现自动去重:
cursor.execute( "INSERT OR IGNORE INTO comments (news_id, user, content, comment_time, likes) " "VALUES (?, ?, ?, ?, ?)", (news_id, c["user"], c["content"], c["time"], c["likes"]) )这样即使脚本中断后重跑,也不会产生重复数据。
4. 文本分析与情感计算:jieba 分词到情感倾向判定
4.1 中文分词与停用词过滤
中文和英文不一样,词与词之间没有空格,必须先分词才能做后续分析。jieba 是绕不开的工具:
import jieba def segment(text, stopwords_path="stopwords.txt"): # 加载停用词 with open(stopwords_path, "r", encoding="utf-8") as f: stopwords = set(line.strip() for line in f) words = jieba.lcut(text) # 过滤停用词、单字和纯数字 filtered = [ w for w in words if w not in stopwords and len(w) > 1 and not w.isdigit() ] return filtered逻辑说明:jieba.lcut返回一个列表,每个元素是一个词。停用词表里放的是「的」「了」「是」「在」这类没有实际语义的高频词,不过滤的话关键词提取会被这些词淹没。len(w) > 1过滤掉单字,因为单个汉字在舆情分析里信息量太低。not w.isdigit()过滤纯数字。
停用词表网上有很多现成的,哈工大停用词表、百度停用词表都行。也可以根据你的数据特点自己补充,比如新闻评论里常见的「小编」「转发」「路过」这类词,对分析没帮助,加进去。
4.2 基于 SnowNLP 的情感打分
情感分析这块,课程设计项目里最常见的是用 SnowNLP。它自带一个基于电商评论训练的模型,对新闻评论的情感判断虽然不是完美,但作为课程设计的基线够用:
from snownlp import SnowNLP def sentiment_score(text): if not text or len(text.strip()) < 2: return None s = SnowNLP(text) return s.sentiments # 返回 0~1 之间的浮点数 def classify_sentiment(score): if score is None: return "无效" if score > 0.6: return "正面" elif score < 0.4: return "负面" else: return "中性"sentiments返回的是 0 到 1 的概率值,越接近 1 表示越正面,越接近 0 表示越负面。0.4 到 0.6 之间划为中性,这个阈值不是固定的,你可以根据实际数据分布调整。如果发现大部分评论都落在 0.45 到 0.55 之间,说明模型区分度不够,可以考虑换模型或者做微调。
注意:SnowNLP 的模型是在电商评论上训练的,对新闻评论的情感判断会有偏差。比如「这个政策真是让人无语」在电商语境下可能被判为负面,但在新闻评论里可能只是表达无奈。课程设计里用没问题,生产环境需要换更专业的模型。
4.3 关键词提取与词云生成
关键词提取用 TF-IDF 或者 TextRank 都行。jieba 自带这两种算法的实现:
import jieba.analyse def extract_keywords(text, topK=20): # TF-IDF 提取 keywords_tfidf = jieba.analyse.extract_tags( text, topK=topK, withWeight=True ) # TextRank 提取 keywords_textrank = jieba.analyse.textrank( text, topK=topK, withWeight=True ) return { "tfidf": keywords_tfidf, "textrank": keywords_textrank }TF-IDF 的核心思想是:一个词在当前文档中出现频率高,但在所有文档中出现频率低,那它就越可能是关键词。TextRank 则是基于词共现图做排序,不依赖外部语料。两种方法各有侧重,实际用的时候可以都跑一遍,对比结果。
词云生成用 wordcloud 库:
from wordcloud import WordCloud import matplotlib.pyplot as plt def generate_wordcloud(keywords, output_path="wordcloud.png"): freq = {word: weight for word, weight in keywords} wc = WordCloud( font_path="simhei.ttf", # 中文字体路径 width=800, height=400, background_color="white", max_words=100 ) wc.generate_from_frequencies(freq) wc.to_file(output_path)font_path必须指定中文字体,否则生成的词云全是方框。Windows 上可以用C:/Windows/Fonts/simhei.ttf,Linux 上需要自己下载一个中文字体文件放到项目目录里。
4.4 LDA 话题模型与热点聚类
如果你想从大量评论里自动发现「大家在讨论什么话题」,LDA 是常用方案。gensim 的实现比较成熟:
from gensim import corpora, models def build_lda_model(docs, num_topics=5, passes=10): # docs 是分词后的文档列表,每个文档是一个词列表 dictionary = corpora.Dictionary(docs) # 过滤极低频和极高频的词 dictionary.filter_extremes(no_below=5, no_above=0.5) corpus = [dictionary.doc2bow(doc) for doc in docs] lda = models.LdaModel( corpus=corpus, id2word=dictionary, num_topics=num_topics, passes=passes, random_state=42 ) return lda, corpus, dictionary def print_topics(lda, num_words=8): for idx, topic in lda.print_topics(num_words=num_words): print(f"话题 {idx}: {topic}")filter_extremes的参数很关键:no_below=5表示出现少于 5 次的词直接丢掉,这些词太罕见没有代表性;no_above=0.5表示出现在超过 50% 文档里的词也丢掉,这些词太普遍没有区分度。num_topics设多少取决于你的数据量,评论几千条的话设 5 到 10 个话题比较合理。passes=10是训练轮数,越大越慢但结果越稳定。
5. 可视化与 Web 交互:从图表到可操作的舆情面板
5.1 情感趋势时间线与热点演化图
舆情分析最有价值的输出之一是「情绪随时间的变化」。把评论按天聚合,算每天的平均情感分:
import pandas as pd import matplotlib.pyplot as plt def plot_sentiment_trend(df): df["date"] = pd.to_datetime(df["comment_time"]).dt.date daily = df.groupby("date")["sentiment_score"].mean().reset_index() plt.figure(figsize=(12, 5)) plt.plot(daily["date"], daily["sentiment_score"], marker="o") plt.axhline(y=0.5, color="gray", linestyle="--", label="中性线") plt.fill_between(daily["date"], 0.5, daily["sentiment_score"], where=daily["sentiment_score"] >= 0.5, alpha=0.3, color="green", label="正面") plt.fill_between(daily["date"], 0.5, daily["sentiment_score"], where=daily["sentiment_score"] < 0.5, alpha=0.3, color="red", label="负面") plt.xlabel("日期") plt.ylabel("平均情感分") plt.legend() plt.tight_layout() plt.savefig("sentiment_trend.png", dpi=150)这段代码的逻辑是:先按日期分组算均值,然后用折线图展示趋势,fill_between把正面和负面区域用颜色区分开。axhline画一条 0.5 的中性参考线,一眼就能看出哪天情绪偏离了中性。
5.2 Flask 接口与前端页面联动
如果项目带 Web 界面,通常用 Flask 做后端。核心是提供几个 API 接口,前端用 AJAX 拿数据渲染图表:
from flask import Flask, jsonify, render_template import sqlite3 app = Flask(__name__) @app.route("/") def index(): return render_template("index.html") @app.route("/api/sentiment_trend") def api_sentiment_trend(): conn = sqlite3.connect("data/news.db") df = pd.read_sql("SELECT * FROM comments", conn) conn.close() df["date"] = pd.to_datetime(df["comment_time"]).dt.strftime("%Y-%m-%d") daily = df.groupby("date")["sentiment_score"].mean().reset_index() return jsonify(daily.to_dict(orient="records")) @app.route("/api/keywords") def api_keywords(): conn = sqlite3.connect("data/news.db") df = pd.read_sql("SELECT content FROM comments", conn) conn.close() all_text = " ".join(df["content"].tolist()) keywords = jieba.analyse.extract_tags(all_text, topK=30, withWeight=True) return jsonify([{"word": w, "weight": round(v, 4)} for w, v in keywords]) if __name__ == "__main__": app.run(debug=True, port=5000)前端页面用 ECharts 或者 Chart.js 调这些接口就行。/api/sentiment_trend返回按天聚合的情感分,/api/keywords返回关键词和权重。前端拿到 JSON 后渲染折线图和词云。
提示:
app.run(debug=True)只适合开发阶段,部署到服务器时要关掉 debug 模式,否则有安全风险。
5.3 报告自动生成
报告生成可以用 Jinja2 模板引擎,把分析结果填进 HTML 模板,再导出 PDF:
from jinja2 import Template REPORT_TEMPLATE = """ <h1>舆情分析报告</h1> <p>分析时间范围:{{ start_date }} 至 {{ end_date }}</p> <p>总评论数:{{ total_comments }}</p> <p>正面评论占比:{{ positive_ratio }}%</p> <p>负面评论占比:{{ negative_ratio }}%</p> <h2>TOP 10 关键词</h2> <ul> {% for kw in keywords[:10] %} <li>{{ kw.word }}(权重:{{ kw.weight }})</li> {% endfor %} </ul> """ def generate_report(data): template = Template(REPORT_TEMPLATE) html = template.render(**data) with open("report.html", "w", encoding="utf-8") as f: f.write(html) return html这个模板很基础,实际项目里可以加上图表嵌入、话题分布、典型评论摘录等内容。导出 PDF 的话用pdfkit或者weasyprint,把 HTML 转成 PDF。
6. 避坑与排查:爬虫被封、编码乱码、模型跑偏的实战记录
6.1 爬虫返回 403 或空数据
现象:脚本跑了几页之后突然返回空列表,或者直接报 403 Forbidden。
原因:请求频率太高被服务端识别为爬虫,或者 User-Agent 被列入了黑名单。
解决:第一,加time.sleep,每请求一页停 1 到 2 秒;第二,准备多个 User-Agent 轮换使用;第三,如果项目里有代理配置,检查代理是否失效。我一般会在爬虫里加一个计数器,每抓 50 页就停 30 秒,模拟人工浏览的节奏。
6.2 中文乱码问题
现象:抓下来的新闻标题和评论全是乱码,或者部分乱码。
原因:网页的编码声明和实际编码不一致。有些页面 header 里写的是gbk,但实际内容是utf-8,反过来也有。
解决:不要完全信任resp.encoding,手动指定:
resp.encoding = resp.apparent_encoding # 自动检测编码 # 或者直接强制 resp.encoding = "utf-8"apparent_encoding是 requests 库自带的编码检测,基于 chardet,大多数情况下能猜对。如果还不行,就手动试gbk和utf-8两个。
6.3 jieba 分词结果不符合预期
现象:分词后出现大量无意义的单字组合,或者专业术语被切碎。
原因:jieba 的默认词典不包含你数据里的专有名词,导致切分错误。
解决:加载自定义词典:
jieba.load_userdict("user_dict.txt")词典格式是每行「词语 词频 词性」,词频和词性可以省略。比如「舆情分析 100 n」这样。把新闻评论里高频出现的专有名词加进去,分词质量会明显提升。
6.4 SnowNLP 情感分全部集中在 0.5 附近
现象:跑完情感分析发现大部分评论的分数都在 0.45 到 0.55 之间,区分度极低。
原因:SnowNLP 的默认模型是在电商评论上训练的,对新闻评论的语义理解不够。另外,短文本本身情感信号就弱。
解决:第一,过滤掉长度小于 5 个字的评论,太短的文本情感判断不可靠;第二,考虑换用基于预训练模型的情感分析方案,比如用transformers加载中文情感分类模型;第三,如果只是课程设计,可以在报告里说明这个局限性,反而是一个加分项。
6.5 LDA 话题模型结果不可解释
现象:训练出来的话题里,每个话题的词看起来都很杂,没有明显的主题。
原因:要么是数据量不够,要么是num_topics设得太多,要么是停用词没过滤干净。
解决:先检查数据量,LDA 至少需要几百篇文档才能出稳定结果。然后调整num_topics,从 3 开始试,逐步增加。最后检查停用词表,把「新闻」「评论」「回复」这类在每条评论里都出现的词加进去。filter_extremes的no_above参数也可以调低一点,比如设 0.3,过滤掉更常见的词。
7. 进阶技巧:用 TF-IDF 加权优化热点排序
课程设计做完基础功能之后,如果想在答辩时多拿几分,可以在热点排序上做文章。默认的关键词提取是按词频或 TF-IDF 权重排序,但舆情场景下,「热度」不只是词频高,还要考虑时间衰减和评论互动量。
我的做法是构造一个综合热度分:
import numpy as np from datetime import datetime, timedelta def compute_hot_score(df, keyword, decay_days=3): """ df: 包含 content, comment_time, likes 的 DataFrame keyword: 要计算热度的关键词 decay_days: 时间衰减窗口(天) """ now = datetime.now() mask = df["content"].str.contains(keyword, na=False) subset = df[mask].copy() if subset.empty: return 0.0 # 时间衰减权重 subset["comment_time"] = pd.to_datetime(subset["comment_time"]) subset["days_ago"] = (now - subset["comment_time"]).dt.days subset["time_weight"] = np.exp(-subset["days_ago"] / decay_days) # 互动量权重(点赞数归一化) max_likes = df["likes"].max() or 1 subset["like_weight"] = 1 + subset["likes"] / max_likes # 综合热度 = 时间权重 × 互动权重 之和 hot_score = (subset["time_weight"] * subset["like_weight"]).sum() return round(hot_score, 2)这段代码的核心思路是:越近的评论权重越高(指数衰减),点赞越多的评论权重越高。decay_days=3表示 3 天前的评论权重降到约 0.37,7 天前的降到约 0.1。这样算出来的热度分,既能反映话题的讨论量,又能体现话题的时效性。
用这个分数对关键词排序,比单纯按词频排要合理得多。答辩的时候老师问「你的热点排序依据是什么」,你可以把时间衰减和互动加权的逻辑讲清楚,比「按词频排的」有说服力。
还有一个实用技巧:把情感分析和热度排序结合起来。一个话题如果热度高但情感分很低,说明是负面舆情在发酵,需要重点关注。反过来,热度高且情感分高,说明是正面话题在传播。这种交叉分析在报告里用散点图展示,横轴热度、纵轴情感分,每个点是一个关键词,一眼就能看出哪些词落在「高热度低情感」的危险区域。
从那以后我每次做文本分析项目,都会先跑一遍基线,把原始结果存下来,再逐步加优化。这样出了问题能快速定位是哪一步引入的偏差,不会改着改着连原始结果长什么样都忘了。希望帮到你。
本文还有配套的精品资源,点击获取