简介:面向Python课程设计、毕业设计及大数据入门学习者的舆情热点分析平台源码,以网易新闻及评论为数据源,完整覆盖网络爬虫、数据清洗、jieba分词、SnowNLP情感分析、关键词提取、时间序列分析与可视化,展示从数据采集到分析呈现的全链路项目实践。压缩包共1403个文件,约23.83MB,文件构成以js/css/html前端页面、py源码及pyc辅助模块为主,另有png/jpg/gif图片素材,以及pdf、docx、pptx说明文档、csv数据文件和sql数据库脚本;前端资源与静态文件占比较高,便于部署运行,Python代码与数据库脚本则提供可调试的分析流程,目录结构清晰,便于检索和二次开发。目前已有166人浏览学习。通过该项目可复现新闻与评论的抓取、去噪、分词、情感倾向判断、热点演进等关键环节,掌握requests、Scrapy、pandas、matplotlib、Flask等工具的项目级组合用法,适合课程设计、毕业设计或想建立完整数据分析实战经验的学习者,也适合需要独立完成答辩演示或深入了解全栈实现细节的使用者。
1. 从网易新闻到舆情热点:这个毕设项目到底做了什么
做课程设计或毕业设计时,最怕的不是技术难,而是题目太大、不知道从哪里下手。这个“python083基于网易新闻+评论的舆情热点分析平台”项目,把数据抓取、文本处理、情感分析、数据可视化、Web 展示这几个环节串成了一条完整链路——从网易新闻页面取数据,清洗成结构化表格,再算热度和情感倾向,最后用图表和页面把结论呈现出来。它适合两类人:一类是正在选题的 Python 方向学生,想找一个能覆盖爬虫、数据分析、前端展示的完整案例;另一类是刚接触舆情分析的开发者,想看看一个不算复杂的平台是如何从零搭起来的。整份资源里包含的是工程源码和前端静态资源,目录结构清晰,能直接照着改和跑。
2. 爬虫层拆解:requests 拿页面、BeautifulSoup 抽字段、playwright 补动态评论
2.1 先理解网易新闻页面的结构,再写抓取代码
舆情分析的第一步是拿到新闻正文和用户评论。网易新闻的频道页是服务端渲染的 HTML,标题、发布时间、评论数都在 li 标签里;而正文页的评论区是异步加载的,直接 GET 拿不到评论内容。这个项目的做法是分层处理:正文列表用 requests 抓,评论部分走另一条通道。
常见做法是先用 requests 库请求频道页 URL,带上 User-Agent 伪装成浏览器。服务端返回 HTML 后,用 BeautifulSoup 配合 lxml 解析器去定位新闻条目的链接和标题。解析时一般通过 find_all 按 class 或标签属性过滤,把每条新闻的 href 和标题抽出来,再拼接成完整的新闻 URL 列表。
import requests from bs4 import BeautifulSoup headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } def fetch_news_list(channel_url): resp = requests.get(channel_url, headers=headers, timeout=10) resp.encoding = "utf-8" soup = BeautifulSoup(resp.text, "lxml") news_items = [] for li in soup.select("ul.news-list li"): # 按实际页面结构调整选择器 title_node = li.find("a") if title_node and title_node.get("href"): news_items.append({ "title": title_node.get_text(strip=True), "url": title_node["href"] }) return news_items这段代码的逻辑是:发请求拿到频道页 HTML,指定 utf-8 编码避免中文乱码;然后用 CSS 选择器选中新闻列表,逐个取出标题和链接。参数方面要注意 timeout 设了 10 秒,防止个别请求卡死整个抓取流程;选择器里的 news-list 不是固定值,实际运行时需要按目标页面微调。抓完列表后,下一步是对每条新闻 URL 发请求拿正文。
解析正文时,要把正文段落从 p 标签中提取出来,拼接成干净的文本。同时记录发布时间和评论数。评论数是后面算舆情热度的重要输入,但它在 HTML 里是数字文本,需要做格式化处理,去掉“参与”“条”这类干扰词。
def parse_article(url): resp = requests.get(url, headers=headers, timeout=10) resp.encoding = "utf-8" soup = BeautifulSoup(resp.text, "lxml") title = soup.find("h1").get_text(strip=True) paras = soup.select("div.post_body p") content = "".join(p.get_text(strip=True) for p in paras) pub_time = soup.find("span", class_="pub_time") comment_count = soup.find("span", class_="comment-count") return { "title": title, "content": content, "pub_time": pub_time.get_text(strip=True) if pub_time else "", "comment_count": comment_count.get_text(strip=True) if comment_count else "0" }这里有几个值得留意的细节。首先是正文选择器,不同频道页的正文容器类名不一样,有的叫 post_body,有的叫 article,跑之前先手动打开一个页面用浏览器开发者工具确认。其次是发布时间的格式,网易新闻返回的是“2025-01-15 10:30:00”这种字符串,入库前最好统一转成 datetime 类型,方便后期做时间序列分析。第三是评论数字段,抓下来可能带“参与”字样,清洗时要用正则只保留数字。
2.2 评论区的动态加载:requests 抓不到的,用 playwright 补
网易新闻的评论不是一次性返回全部,而是按楼层异步加载。requests 拿到的 HTML 里只有第一页评论,甚至什么都没有。如果只用 requests,评论数据会缺失一大截。这个项目里处理动态加载的方式是引入浏览器自动化工具,在数据量不大时这是一个稳妥的选择。
import re from playwright.sync_api import sync_playwright def fetch_comments(url, max_scroll=5): comments = [] with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page(user_agent=headers["User-Agent"]) page.goto(url, timeout=30000) for _ in range(max_scroll): page.mouse.wheel(0, 3000) page.wait_for_timeout(1500) items = page.locator("div.comment-item") for i in range(items.count()): text = items.nth(i).inner_text().strip() comments.append(text) browser.close() return comments这个函数的作用是模拟真实用户滚屏操作。页面加载后连续向下滚动,触发评论区的懒加载请求,等新评论渲染出来后再用 locator 定位评论容器逐一提取。参数 max_scroll 控制滚动次数,次数越多抓到的评论越多,但耗时也线性增加;1500 毫秒的等待是为了给异步请求留出渲染时间。如果目标新闻评论很多,可以把滚动次数调大,或改成按评论数动态判断是否继续滚。
注意 playwright 是独立库,需要额外安装,首次使用还要执行 playwright install 下载浏览器内核。同类方案里也可以换成 Selenium,但 playwright 的 API 更简洁,处理滚屏和等待内置方法更顺手,个人更推荐。
2.3 数据持久化:先落 CSV 还是先进 SQLite
抓下来的数据在内存里是列表套字典的结构,不落盘就白白浪费了。这个项目提供了两条存储路径:一条是存成 CSV,方便直接用 pandas 读;另一条是写入 SQLite,方便后续按时间查询和增量更新。毕设阶段我建议两者都做,CSV 留给调试,SQLite 留给 Web 端查询。
import pandas as pd import sqlite3 def save_to_sqlite(news_list, db_path="weibo_hot.db"): conn = sqlite3.connect(db_path) df = pd.DataFrame(news_list) df.to_sql("news", conn, if_exists="append", index=False) conn.close()pandas 的 DataFrame.to_sql 方法是把抓取结果写入数据库的最快路径。if_exists="append" 表示每次运行时追加新数据,配合时间去重可以实现增量抓取。SQLite 不需要额外启动数据库服务,Python 自带 sqlite3 驱动,很适合单人单机的毕设项目。正式跑批量任务时,我会在每次写入前先检查标题是否已存在,避免重复数据堆积。
3. 文本处理与情感分析:jieba 分词、SnowNLP 情感打分与 sklearn 建模
3.1 中文分词的落地姿势:停用词表比分词器更影响结果
拿到新闻标题和评论后,文本是长句子,计算机没法直接计算词频,得先分词。中文分词常用 jieba,它对新闻和评论这类短文本效果不错。但分词只是第一步,真正影响分析质量的是停用词表。像“的”“了”“一个”“我们”这类词,分词后大量出现,不滤掉会占据词频榜前几名,导致热点词全是废话。
import jieba import re stopwords = set() with open("stopwords.txt", "r", encoding="utf-8") as f: for line in f: word = line.strip() if word: stopwords.add(word) def clean_and_cut(text): text = re.sub(r"[\s\u3000]+", "", text) text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9]", "", text) words = jieba.lcut(text) return [w for w in words if w not in stopwords and len(w) > 1]正则先把空白和特殊字符去掉,再只保留中文、英文和数字,这样做能避免分词器把表情符号和 HTML 残留当成有效词。len(w) > 1 过滤掉单字词,这类词大多无实际含义。停用词表可以从网上下载通用中文停用词表,也可以自己往里面加领域无关词。评论里常见的“哈哈”“666”“顶”这类网络用语要不要滤掉得看需求——如果是分析热点,建议保留“666”这种有情感倾向的词;如果是提取主题词,就滤掉。
3.2 情感分析:SnowNLP 做无标注快速打分,sklearn 做有标注模型
情感分析是这个平台的核心输出之一。对每条评论判断是正面、负面还是中性,然后统计整体情感分布。在没有任何标注数据的情况下,最省事的方案是用 SnowNLP 库,它内置了训练好的中文情感模型,直接对文本打 0 到 1 之间的情感分。
from snownlp import SnowNLP def sentiment_score(text): s = SnowNLP(text) return s.sentiments # 越接近1越正面,越接近0越负面这个接口简单到几乎没有学习成本,但它的模型是通用语料训练的,对新闻评论这种带大量网络用语的文本,准确率只能说凑合。毕设展示完全够用,论文里如果做效果评估,可以手动标注两百条评论算一下准确率,一般能到 70% 上下。
想要更高准确率,就得自己训练模型。做法是先人工标注一批评论为正面或负面,再用 sklearn 的朴素贝叶斯分类器训练。其中关键一步是把文本转成向量,最简单的方案是 CountVectorizer 加 TF-IDF。
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.model_selection import train_test_split def train_sentiment_model(df): vectorizer = TfidfVectorizer(max_features=5000, ngram_range=(1, 2)) X = vectorizer.fit_transform(df["comment"].astype(str)) y = df["label"] # 0负面 1正面 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) clf = MultinomialNB() clf.fit(X_train, y_train) return vectorizer, clf这段代码的逻辑是先用 TF-IDF 把评论文本转成稀疏向量,max_features 限制特征数量,ngram_range=(1, 2) 同时考虑单词和相邻词组,能捕捉“不好”和“不好看”这类短语差异。朴素贝叶斯作为基线分类器,训练快、可解释性强,对短文本分类是够用的。random_state=42 固定随机种子,确保每次运行切分一致。实际训练时要注意标注数据的分布,正负样本别太失衡,否则模型会偏向多数类。
3.3 从评论到舆情结论:情感聚合与正负占比
情感分析不能只算单条评论,要把同一条新闻的所有评论聚合成一个舆情结论。这个聚合逻辑是:统计评论总数、正负中评论数量、计算正面占比,再结合评论数和新闻本身的属性,给出一条新闻的情感倾向标签。
def aggregate_sentiment(comments): total = len(comments) if total == 0: return {"neg": 0, "pos": 0, "neu": 0, "ratio": 0.5} scores = [sentiment_score(c) for c in comments] neg = sum(1 for s in scores if s < 0.4) pos = sum(1 for s in scores if s > 0.6) neu = total - neg - pos return { "neg": neg, "pos": pos, "neu": neu, "ratio": pos / total }0.4 和 0.6 这两个阈值不是固定死的,SnowNLP 输出的分数分布偏向中间值,很多中性评论得分在 0.45 到 0.55 之间。我调参时发现把阈值收紧到 0.35 和 0.65 会减少中性误判。不过阈值设得越紧,被归为中性的评论越多,具体看你想让图表呈现怎样的分布。这个函数返回的字典会合并到新闻记录里,作为后续可视化的输入。
4. 关键词提取与热度计算:TF-IDF 找热点、时间序列看趋势、ECharts 画图表
4.1 TF-IDF 关键词提取与热度综合排序
舆情热点的“热”体现在多个维度:新闻本身是否被大量报道、评论数量是否快速增长、情感是正向爆发还是负面集中。这个项目的做法是先用 TF-IDF 从标题和正文中提取关键词,再结合评论数、情感分和发布时间计算综合热度分。
from sklearn.feature_extraction.text import TfidfVectorizer def extract_keywords(docs, top_k=10): vectorizer = TfidfVectorizer(token_pattern=r"\b[\w\u4e00-\u9fa5]+\b", max_features=3000) tfidf_matrix = vectorizer.fit_transform(docs) feature_names = vectorizer.get_feature_names_out() keywords = [] for i in range(tfidf_matrix.shape[0]): row = tfidf_matrix.getrow(i).toarray()[0] top_indices = row.argsort()[-top_k:][::-1] keywords.append([feature_names[idx] for idx in top_indices if row[idx] > 0]) return keywordstoken_pattern 参数直接传入中文匹配模式,避免 TF-IDF 默认的英文分词逻辑把整句中文当成一个 token。max_features=3000 限制字典大小,防止矩阵过稠密。返回的每个文档对应一串关键词,这些词可以按评论数加权后汇总成全局热点词榜。如果发现关键词里有“网易”“新闻”这种出现频率极高的词,把它们加进停用词表再跑一次。
综合热度分的计算没有统一标准,常见的做法是加权求和:评论数取对数后乘 0.4,正面评论占比乘 0.3,新闻时效性(离当前时间越近越高)乘 0.3。
import time def hot_score(article, now_ts): comment_score = min((article["comment_count"] + 1) ** 0.5, 100) sentiment_score_value = article["sentiment_ratio"] pub_ts = time.mktime(time.strptime(article["pub_time"], "%Y-%m-%d %H:%M:%S")) freshness = max(0, 1 - (now_ts - pub_ts) / (24 * 3600 * 3)) return 0.4 * comment_score + 0.3 * sentiment_score_value * 100 + 0.3 * freshness这个公式里,评论数开平方是为了抑制爆款新闻的评论数量级差异,避免个别几万评论的新闻把所有其他新闻压下去。新鲜度的分母是 3 天,超过 3 天的新闻新鲜度归零,符合舆情的短时效特性。这三个权重不是定死的,实际跑出来如果发现热度榜被评论数主导,就把 0.4 调低,把情感分的权重调高。
4.2 时间序列分析:pandas 重采样看舆情涨落
舆情分析不仅要看某一时刻的热度,还要看热度随时间的变化趋势。这个项目把新闻和评论按时间分桶,统计每个时间窗口内的评论数和情感变化。pandas 的 resample 是完成这个任务最直接的工具。
import pandas as pd def trend_analysis(df, freq="1H"): df["pub_time"] = pd.to_datetime(df["pub_time"]) df.set_index("pub_time", inplace=True) hourly = df.resample(freq).agg({ "comment_count": "sum", "sentiment_ratio": "mean" }).fillna(0) return hourlyresample 按小时聚合评论总量和平均情感分,生成一条完整的舆情趋势线。freq 参数改成 "6H" 或 "1D" 可以看更粗粒度的趋势。做时间序列时有个坑:原始数据的时间格式必须统一,如果 CSV 里混了“2025-01-15”和“2025/01/15”两种格式,pd.to_datetime 会解析失败。我的习惯是抓取时就先用正则把时间格式统一成一种。
4.3 可视化图表的选用逻辑
这个项目的前端资源里有 bootstrap.css、fullcalendar.css、font-awesome.css、layui.css,目录结构指向的是一个以 Flask 为后端、Bootstrap 为框架、ECharts 为图表的 Web 页面。可视化环节的关键不只是画出图,而是选对图。
- 热点词云:展示高频关键词,适合做首页横幅。用 Python 的 wordcloud 生成图片后传给前端 img 标签。
- 情感占比饼图:展示正负中比例,适合单条新闻详情页。
- 热度折线图:展示时间序列趋势,适合总览页。
- 热榜柱状图:按热度分排序的新闻列表,适合用 ECharts 的 bar 或表格。
词云生成的后端代码很简单,把全局关键词按权重输入即可:
from wordcloud import WordCloud def generate_wordcloud(keyword_weights, output_path): wc = WordCloud( font_path="msyh.ttc", width=1200, height=800, background_color="white", max_words=100 ) wc.generate_from_frequencies(keyword_weights) wc.to_file(output_path)font_path 必须指向一个中文字体文件,Linux 服务器上常见坑是没装中文字体,结果词云全是方块。Windows 下可以填微软雅黑的路径;Linux 下要么安装 fonts-wqy-zenhei,要么把字体文件放到项目目录里指定相对路径。前端展示时,词云图直接作为静态图片引用,不需要额外配置。
5. 舆情平台避坑指南:动态加载、中文乱码与数据漂移的五个实战教训
5.1 分类页 HTML 结构与预期不一致,选择器捞不到数据
现象:跑新闻列表抓取,返回的空列表,一条新闻都没抓到。
原因:不同频道的页面结构不同,有的频道列表用了分页加载,首页只有前几条内容;有的频道改版后 class 名变了,写死的 ul.news-list 选择器匹配不到新结构。还有一个常见原因是网络请求被重定向到了登录页,解析出来的是登录表单而不是新闻列表。
解决:先在浏览器里手动打开目标频道页,右键检查确认当前页面的新闻容器标签到底是什么。我一般会写一段调试代码,把页面 HTML 保存到本地文件里,然后反复调整选择器直到匹配成功。另外检查 requests 响应的状态码,如果发现 302 跳转,说明触发了网页端反爬策略,需要补充 Cookie 或改用浏览器自动化方案。
5.2 评论抓取数量远低于页面显示数
现象:页面显示“参与评论 2000+”,代码只抓到几十条评论。
原因:网易新闻的评论是懒加载的,页面初始只渲染第一屏。如果滚动太快,触发限流后请求直接返回空数据;或者评论模块需要登录才能查看更多,未登录状态只能拿到前几条。
解决:把 playwright 的滚动等待时间从 1500 毫秒提升到 2500 毫秒,每次滚动后检查评论数量是否增长,连续三次没增长就停止滚动。如果是登录限制,需要在浏览器里手动登录一次,然后复用登录后的 Cookie。另外注意评论区可能有“热门评论”“最新评论”两个 Tab,切换 Tab 会重新加载数据,代码里要先固定点击“最新评论”再开始滚动。
5.3 中文乱码,入库后全是“锟斤拷”或问号
现象:从 SQLite 里面查询,标题和评论内容呈现出乱码,部分文本变成一排问号。
原因:requests 拿到的响应可能使用了 gzip 压缩,解码前没有正确处理;或者 requests 自动推断的编码是 ISO-8859-1,导致中文被错误解码。数据库层面则是连接 SQLite 时没有设定 utf-8,写入时数据就已经出错了。
解决:请求时明确设置 Accept-Encoding 头,拿到响应后先检查 resp.encoding,如果检测结果不是 utf-8 就手动指定。写入 SQLite 前确认数据在 Python 侧是正常的 str 类型,不要提前 encode。连接数据库时执行 PRAGMA encoding = 'UTF-8',从源头保证编码一致。这类乱码问题一旦混进库里,后面训练模型时全是脏数据,做再多清洗也补不回来。
5.4 情感分析结果整体偏向中性,图表失去区分度
现象:饼图里中性评论占了 80% 以上,正面和负面几乎分不出来。
原因:SnowNLP 的分数集中在 0.4 到 0.6 之间,直接用 0.5 为分界会把大量弱情感评论归为中类。新闻评论中真正强烈的情绪表达占比本来就低,更多是“看了”“路过”这类无感情内容。
解决:两种思路。第一种是调整阈值,把正面的阈值降到 0.55,负面的阈值升到 0.45,扩大正负的判定范围。第二种是把中性倾向明显的评论直接过滤掉,只统计有明显正负倾向的评论。我做课设时用第二种方案,展示效果更好,因为图表上能明显看到正负对比,而不是一堆灰色。
5.5 爬虫连续运行一段时间后请求全部超时
现象:前两轮抓取正常,跑到第三轮开始大量超时,错误集中在连接超时和 read timeout。
原因:被目标服务器的频率限制策略识别了。requests 库的请求频率过高,同一个 IP 的访问量在短时间内超过阈值,服务器开始拒绝服务。代码里没有随机延时和重试机制,导致一个连接失败后就连锁失败。
解决:在循环请求中加随机 sleep,时间落在 2 到 5 秒之间,打破固定的请求间隔。同时给 requests 加 retry 策略,用 urllib3 的 Retry 模块对超时和 5xx 错误做两次重试,重试间隔递增。另外用 session 复用 TCP 连接,避免每请求一个连接造成额外开销。这些手段不能完全绕开反爬,但能大幅降低封禁概率。
6. 把平台跑通:Flask 应用挂载分析结果,三分钟看完整流程
项目的前端资源文件已经齐备,剩下的任务是把数据和页面串起来。这个平台的核心逻辑不复杂:启动时读数据库里的新闻和评论分析结果,在 Flask 路由里渲染模板,把热点榜、情感分布和趋势图传给前端。我习惯把整套流程封装成三个接口,避免把所有代码堆在一个文件里。
from flask import Flask, render_template, jsonify import sqlite3 import pandas as pd app = Flask(__name__) def load_data(): conn = sqlite3.connect("news_hot.db") df = pd.read_sql_query("SELECT * FROM news ORDER BY hot_score DESC LIMIT 20", conn) conn.close() return df @app.route("/") def index(): df = load_data() return render_template("index.html", news_list=df.to_dict(orient="records")) @app.route("/api/trend") def trend(): conn = sqlite3.connect("news_hot.db") df = pd.read_sql_query("SELECT pub_time, sentiment_ratio FROM news", conn) conn.close() df["pub_time"] = pd.to_datetime(df["pub_time"]) trend_data = df.set_index("pub_time").resample("6H").mean().reset_index() return jsonify(trend_data.to_dict(orient="records"))两个路由各自负责一件事:首页路由直接渲染模板,API 路由返回 JSON 数据供前端 ECharts 调用。Flask 默认的模板目录是 templates,前端资源放在 static 目录下,bootstrap.css、layui.css 这些文件按原路径放进去就能被自动加载。调试时用 app.run(debug=True) 开启热重载,改完前端文件不用重启服务。
跑通整套流程的最后一步是验证数据是否合理。我拿到分析结果后有一个固定动作:抽查热度榜前三条新闻,手动打开原页面看评论是否对得上,情感分是否和直观感受一致。如果某条新闻正文几乎没有文字、只有图片或视频,爬虫会抓到空内容,这种数据应该直接过滤掉,否则会拉低整个样本的质量。
整条链路里还有个隐藏的体验优化点:把抓取和分析这两个耗时环节做成手动触发,而不是每次打开页面都重新爬。启动时先执行一次抓取脚本,后续只读数据库,这样页面响应能控制在秒级。如果需要定时更新,就设置一个定时任务,每天固定时段跑一次抓取和分析。
我从这个项目里学到的最重要一件事是:舆情分析平台的价值不在算法有多高级,而在数据链路是否完整可靠。从那以后,每次做这类项目我都强制自己先打通一条最简单的数据通路,从一条新闻、一条评论开始,确认每个环节输出正确,再逐渐扩大规模。这个习惯帮我省掉了大量返工时间,也希望帮到你。
本文还有配套的精品资源,点击获取