简介:一份基于网易新闻与评论的舆情热点分析平台完整工程包,面向Python课程设计、毕业设计及数据科学初学者,旨在解决从舆论数据采集、清洗、情感判断到热点趋势可视化的全流程实践问题。包体共1403个文件,以JS、CSS、HTML等前端资源为主,兼顾Python源码、pyc编译文件、CSV与SQL示例数据,以及说明文档,压缩包约23.83MB,目录结构便于按模块检索。目前已有166人学习,资源在描述中清晰拆解了网络爬虫、jieba分词、SnowNLP情感分析、TF-IDF关键词提取、Flask/Django Web整合等知识点,并使用了Bootstrap、Layui、Font Awesome等前端框架构建展示界面。学习者可从中获得一套可运行的完整项目骨架,理解前后端配合方式,也能借鉴其数据清洗与时间序列分析方法,快速搭建自己的舆情分析或文本挖掘原型。
1. 拿网易新闻练手的舆情热点分析平台:从爬虫到可视化的完整闭环
如果你正在找 Python 课程设计或毕业设计的选题,又不想做一个烂大街的学生管理系统,那么基于网易新闻加评论的舆情热点分析平台是一个性价比很高的方向。它一条链路串起了爬虫、文本清洗、情感分析、关键词提取、时间序列分析和 Web 可视化,几乎把 Python 数据处理的常用技能全过了一遍。这个项目不是只把新闻标题抓下来就结束,而是要把“评论情感”和“热点关键词”落成图表,最终在浏览器里看到一个可以交互的分析界面。对于想证明自己具备 Python 全栈数据分析能力的人来说,这份源码值得拆开看一遍,甚至直接改造成自己的毕设。
我拆这个项目的时候最深的感受是:它的难点不在某一个环节,而在环节之间的衔接——爬虫拿到的是杂乱 HTML,清洗后才能做分词,分词结果才能喂给情感分析,分析结果又得存进数据库供 Web 层调用。任何一个环节掉链子,后面的图表就是空的。下面我按实际开发顺序把整个项目拆开讲,每一章都给到能直接复用的代码和参数说明,以及我在复现过程中踩过的坑。
2. 数据采集层:用 requests 加 BeautifulSoup 把网易新闻和评论拉下来
2.1 为什么选网易新闻:反爬门槛适中,评论区信息密度高
网易新闻的页面结构相对规整,不像微博那样需要登录授权,也不像知乎那样有严格的风控,但也不是完全没有反爬措施。它的新闻列表页是服务端渲染的,直接用 requests 就能拿到完整 HTML,而评论数据是异步加载的,需要找到对应的 JSON 接口。这种“一半静态、一半动态”的组合恰好适合用来练习爬虫的两种常见策略。
另外网易新闻的评论区质量在中文互联网里算是比较高的,跟帖往往能反映真实的舆情态度,这对情感分析环节来说是非常有价值的语料。如果换成纯官方新闻稿,评论都是“点赞”“支持”,情感分析就失去了区分度。选这个数据源,能让情感分布图呈现出有意义的波动,而不是一条直线。
我在复现的时候首选 requests 加 BeautifulSoup 的组合。Selenium 虽然能解决动态加载,但启动浏览器实例的开销大、速度慢,能用接口拿数据就不必上浏览器模拟。
2.2 新闻列表页的抓取:请求头伪装与解析逻辑
import requests from bs4 import BeautifulSoup def fetch_news_list(category_url, page=1): headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/119.0.0.0 Safari/537.36", "Referer": "https://news.163.com/", } params = {"page": page} resp = requests.get(category_url, headers=headers, params=params, timeout=10) resp.encoding = "utf-8" soup = BeautifulSoup(resp.text, "lxml") news_items = [] for li in soup.select("ul.news_list li"): title_tag = li.select_one("a") if title_tag is None: continue title = title_tag.get_text(strip=True) link = title_tag.get("href") news_items.append({"title": title, "url": link}) return news_items这段代码里最关键的是 headers 字典。User-Agent 必须伪装成真实浏览器的标识,否则网易的 WAF 会直接返回 403 或验证码页面。Referer 字段用来模拟从网易主页跳转过来的访问行为,某些反爬策略会校验这个字段。resp.encoding = "utf-8"这行不能漏,网易页面虽然声明了 utf-8,但有时候响应头里的 charset 是缺失的,不手动指定的话中文标题会乱码。
选择器ul.news_list li是我抓当前网易新闻首页结构时验证过的,如果你的复现环境里页面改版了,可以用开发者工具先确认列表容器的 class 名再调整。这里的一个经验是:先用一小段测试脚本打印 soup 的前 3000 个字符,确认解析结构没有变化,再跑全量抓取。
2.3 评论数据的异步接口:构造参数与 JSON 解析
import requests def fetch_comments(news_id, page=1): comment_url = "https://comment.api.163.com/api/v1/products/a2869674571f77b5a0867c3d71db5856/threads/{}/comments" params = { "ibc": "newspc", "limit": "30", "offset": str((page - 1) * 30), "showLevelThreshold": "0", "headLimit": "1", "tailLimit": "2", } headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/119.0.0.0 Safari/537.36", "Referer": f"https://news.163.com/{news_id}.html", } resp = requests.get(comment_url.format(news_id), params=params, headers=headers, timeout=10) data = resp.json() comments = [] for key, val in data.get("comments", {}).items(): content = val.get("content", "") vote_count = val.get("voteCount", 0) create_time = val.get("createTime", "") comments.append({ "news_id": news_id, "content": content, "vote_count": vote_count, "create_time": create_time, }) return comments评论接口是网易新闻的一个 JSON API,它需要新闻的 docid 作为参数。新闻列表页里的跳转链接通常就包含这个 ID,我的做法是在抓列表时直接用正则从 URL 里提取 docid,存进数据库,再逐个请求评论接口。
这段代码里有几个参数要说明一下。limit控制每页评论条数,最大可以调到 50,但调太大响应时间会明显变长,我一般保持 30。offset是分页偏移量,(page - 1) * 30这个计算公式要注意,offset 的单位是条数而不是页码。ibc参数是固定的,表示评论板块标识,这个值在不同产品线里不一样,如果是抓网易号(自媒体)的评论,接口地址里的 products 参数要换。
解析时用的是data["comments"],这是一个字典,key 是评论 ID,val 里包含了 content、voteCount、createTime、floor 等字段。这里有一个坑:不是每条评论都有 voteCount 字段,被折叠的评论可能没有这个 key,所以要用val.get("voteCount", 0)而不是val["voteCount"],否则会在中途崩掉。
2.4 抓取策略:请求间隔、失败重试与断点续传
整套爬虫如果一口气跑完,大概率会在中途被服务器断开连接。我一般会加一个简单的间隔控制,每抓一条新闻的评论后 sleep 1 到 2 秒,并且把已经抓过的 news_id 记录在 SQLite 里,这样即使程序中断,下次启动也只抓新增的数据。
至于数据存储,这个项目我推荐直接用 SQLite 而不是 MySQL。原因很简单,单机学习场景不需要部署独立的数据库服务,sqlite3 是 Python 内置模块,零配置就能用。表结构我设计了 three 张,分别是 news(存储新闻标题、链接、docid、抓取时间)、comments(存储评论正文、点赞数、发布时间、新闻外键)、analysis(存储后续分析的结果数据)。后续 Flask 查询统计指标时,SQLite 的读写速度完全够用。
3. 文本清洗与中文分词:把脏数据变成可分析的语料
3.1 HTML 标签与噪声文本的清理策略
抓下来的评论数据比新闻标题脏得多,emoji、HTML 实体、无意义字符、广告文本混在一起。我见到最常见的翻车情况是:有人直接拿原始评论去做词云,结果高频词全是“http”“转发”“哈哈”这类噪声。所以在分词之前必须做一轮清洗,顺序很重要。
我的清洗顺序是:先去 HTML 标签和实体(用正则或 BeautifulSoup 的 get_text),再替换中文标点,然后过滤掉长度小于 2 的评论,最后用停用词表做一轮过滤。示例如下:
import re def clean_text(raw_text): # 去掉 HTML 标签和转义实体 text = re.sub(r"<[^>]+>", "", raw_text) text = re.sub(r"&[a-zA-Z]+;", "", text) # 统一替换中文标点为空格 text = re.sub(r"[,。!?、;:""''《》()【】——…·]", " ", text) # 去掉 URL 和邮箱 text = re.sub(r"https?://\S+|www\.\S+", "", text) # 去掉全是空白字符的片段 text = re.sub(r"\s+", " ", text).strip() return text注意第三行正则里的标点列表,""在正则里如果处理不当会引起匹配范围错误,我在实际代码里用的是 Unicode 范围匹配[\u3000-\u303f\uff00-\uffef]来覆盖全角标点,效果更稳。清洗完成后建议统计一下每条评论的净化前后长度差,如果长度差超过 60%,说明这条评论以噪声为主,可以直接丢弃。
3.2 jieba 分词的参数配置:自定义词典与停用词
import jieba # 加载自定义词典,提升对新闻专有名词的分词准确率 jieba.load_userdict("user_dict.txt") stopwords = set() with open("stopwords.txt", "r", encoding="utf-8") as f: for line in f: stopwords.add(line.strip()) def tokenize(text): words = jieba.lcut(text) return [w for w in words if w not in stopwords and len(w.strip()) > 1]user_dict.txt 里每一行是一个自定义词,格式是“词 词频 词性”,其中词频可以不填,例如:
特斯拉 10 nz 张雪峰 10 nz 淄博烧烤 10 nz加载自定义词典的目的是让 jieba 把“杭州亚运会”当成一个整体切出来,而不是切成“杭州”“亚运会”。分词完成后过滤停用词这一步很关键,我用的停用词表大约 2000 个词,除了一般的“的、了、和、是”之外,还手动加了一批舆情场景里的高频噪声词,比如“网易”“新闻”“小编”“网友”这些在评论语境里没有分析价值的词。注意在新词表里,如果你改的是新闻分类维度,nz这类词性标记可以让 jieba 在并行分词模式下更稳定,不填也不会有大问题。
3.3 数据预处理流程的完整管道
实际项目里我不建议一行一行在 notebook 里跑,而是把整个清洗过程封装成一个类,输入原始评论列表,输出清洗后的结构化 DataFrame。
| 阶段 | 输入 | 处理方式 | 输出 |
|---|---|---|---|
| 原始抓取 | requests 响应 | JSON 解析字段提取 | 评论列表 |
| 文本清洗 | 原始评论 | 去标签、去 URL、去空行 | 干净文本 |
| 分词过滤 | 干净文本 | jieba 分词 + 停用词过滤 | 词列表 |
| 统计归档 | 词列表 | Counter 计数 | 词频字典 |
4. 情感分析与关键词提取:从文本里挖出舆情态度
4.1 基于 SnowNLP 的评论情感判定思路
情感分析是舆情平台的核心模块。这个项目里最常用的方案是 SnowNLP,它是一个纯 Python 实现的中文文本处理库,内置了训练好的情感分类模型,用法非常简单,适合课程设计快速出效果。它的情感分值范围是 0 到 1,越接近 1 表示越正面,越接近 0 表示越负面。
from snownlp import SnowNLP def analyze_sentiment(text): s = SnowNLP(text) return s.sentiments # 返回 0.0 ~ 1.0 的情感值把情感值按阈值切分:大于 0.6 标记为正向,小于 0.4 标记为负向,中间是中性。这个阈值可以根据自己的数据分布微调。我跑网易新闻评论的经验是,直接用 0.6 和 0.4 的对称阈值会比较合理,因为网易评论区整体偏负向吐槽,如果用 0.5 作为单一分界,会把大量中性和轻微吐槽全部算成负面,导致情感分布图失真。
SnowNLP 的好处是开箱即用,但它的模型是在电商购物评论上训练的,对新闻时政领域的词汇判断会有一点偏差。如果你对准确率有更高的要求,可以再用 sklearn 的朴素贝叶斯在人工标注的小样本上做一个分类器,不过对课程设计来说,SnowNLP 的产出已经够看了。
4.2 基于 TF-IDF 和 TextRank 的新闻热点关键词提取
关键词提取的常用算法是 TF-IDF 和 TextRank。TF-IDF 的原理是过滤掉常见词,保留那些在某篇文章里出现得多但语料库里出现得少的高信息量词;TextRank 则是把句子里的词看成图节点,计算词之间的共现权重,迭代收敛后得到排名。
我一般两个都会跑一遍,然后取交集。这样做的原因是 TF-IDF 容易漏掉一些出现频率不高但是语义核心的词,TextRank 则可能把多个相同领域的高频词都排到前面,缺少区分度。两者取交集可以在直观感受上取得一个平衡。
from jieba.analyse import extract_tags, textrank def extract_keywords(texts, topk=20): full_text = " ".join(texts) tfidf_tags = extract_tags(full_text, topK=topk) textrank_tags = textrank(full_text, topK=topk) overlap = [t for t in tfidf_tags if t in textrank_tags] # 不足 topk 时用 TF-IDF 结果补齐 return list(dict.fromkeys(overlap + tfidf_tags))[:topk]extract_tags底层调用了 jieba 分词后的 TF-IDF 算法,它会自动把停用词和低频词过滤掉,所以传入的 texts 可以不做额外清洗。但要注意textrank函数在 Python 3.8 以上的环境里偶尔会报误告警,提示没有安装 numpy,实际是 jieba 版本兼容问题,升级 jieba 到最新版即可。
关键词提取结果的准确性还取决于语料规模。只拿 50 条新闻去做热点提取,出来的词肯定是散的;我建议至少累积 500 篇新闻和对应评论再跑这个话题分析,这样关键词才有统计意义。
4.3 按时间维度的情感趋势聚合
舆情分析不能只看整体情感占比,更要看情感随时间的变化趋势。这里我用 pandas 按天分组,计算每天的平均情感值,输出一个时间序列。
import pandas as pd def sentiment_trend(df): df["date"] = pd.to_datetime(df["create_time"], unit="s") df["date"] = df["date"].dt.date trend = df.groupby("date")["sentiment"].agg(["mean", "count"]).reset_index() trend.columns = ["date", "avg_sentiment", "comment_count"] return trend注意create_time字段从网易接口返回的是 Unix 秒级时间戳,必须先用pd.to_datetime(..., unit="s")转成 datetime,再提取日期。如果直接当字符串 groupby,会把同一天的评论拆成不同组,趋势图就会变成锯齿状。聚合后可以用一个简单的移动平均再做一次平滑,避免因为某天评论量太少导致均值剧烈抖动。
5. 避坑手册:复现这个项目时最容易翻车的几个地方
5.1 反爬拦截与请求头伪装
现象:用 requests 请求网易新闻列表页,返回的不是预期的 HTML,而是一个包含滑块验证码的页面或者 403 状态码。
原因:网易的 WAF 会对没有浏览器标识的请求直接拦截,尤其是短时间内高频访问同一个栏目时,触发频率限制的概率很高。
解决:每次请求必须带上完整的 User-Agent,包括浏览器版本和系统信息,不能只写Python-requests/2.x。其次控制请求频率,新闻列表页每页间隔 2 秒,评论接口每次请求至少间隔 1.5 秒。如果还是被拦截,可以尝试更换 User-Agent 为一个移动端的标识,或者用代理 IP 池轮换。
5.2 评论接口返回空数据的根因
现象:评论接口返回的 JSON 里comments字段是空字典,明明页面底下能看见评论。
原因:新闻的 docid 传错了。网易新闻有两个 ID,一个是文章本身的 docid,一个是评论系统对应的 commentid,二者不一定相同。如果你是从新闻 URL 里提取的 ID,需要确认它同时能作为评论接口的 thread 参数。
解决:先用浏览器开发者工具在评论区抓一次网络请求,看正在请求的评论接口 URL 里 thread 参数长什么样,把那个值提取出来作为标准。另外注意头条新闻如果没有开放评论,接口也会返回空,这是正常现象,在代码里跳过即可。
5.3 jieba 分词把专有名词切碎
现象:词云里出现“淄博”“烧烤”分列两个词,或者“张雪峰”被切成“张”“雪峰”。
原因:j ieba 默认词典是通用的,对新闻里的人物名和热点专有名词覆盖不够。
解决:用jieba.add_word()或者在自定义词典文件里维护一个不断增补的列表。我跑这个项目的时候维护了一个包含实时热点词的文件,每轮抓取前先检查有没有新热点词需要加入。对于课程设计来说,提前把当前新闻语料中出现频率高的专有名词手动加进词典就够了。
5.4 中文字体缺失导致可视化图表乱码
现象:matplotlib 生成的情感趋势图,横坐标的日期没问题,但图例和标题的中文全部显示为方框。
原因:Linux 或部分 Windows 精简环境里 matplotlib 找不到可用的中文字体。
解决:在绘图前显式指定中文字体:
import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "WenQuanYi Zen Hei"] plt.rcParams["axes.unicode_minus"] = False第一行设置字体优先列表,第二个参数解决坐标轴上负数显示成方框的问题。如果服务器上连中文字体都没装,还需要先安装 fonts-wqy-zenhei 之类的字体包,装完记得删除 matplotlib 的字体缓存目录再重启。
5.5 SQLite 并发写入导致的 database is locked
现象:爬虫正在写入评论数据,Flask 后端同时查询统计结果,偶发报错database is locked。
原因:SQLite 同一时间只允许一个写连接,长事务会把数据库文件锁住。
解决:在 sqlite3.connect 时传入timeout=10提高锁等待时间,并且写完立即 commit。不要把连接做成全局的,每个线程独立开连接,用完即关。如果爬虫是多线程的,考虑限制线程数最多 5 个,或者把所有写操作放进一个队列由一个消费者串行执行。
6. Flask 搭建可视化看板:把分析结果用图表展示出来
6.1 后台接口设计:统计查询与前端交互
整个平台的最终形态是一个 Web 页面,用户可以看到热点关键词、情感分布比例、趋势折线图等信息。Flask 在这个项目里承担的是后端 API 角色,它从 SQLite 中读分析结果,按前端请求的类型返回 JSON。
我设计了三个核心接口。一个是获取整体舆情统计概览,包括新闻总数、评论总数、平均情感值、正向负向中性占比。一个是获取指定时间范围的热点关键词 Top 榜,前端拿到后渲染词云。还有一个是获取情感趋势数据,前端绘制折线图。接口返回的是 JSON,前端用 jquery 发 ajax 请求,再调用 ECharts 渲染图表。
6.2 ECharts 图表配置:词云与情感趋势折线图
前端展示我用的是 ECharts,它对中文图表支持完善,词云图用 echarts-wordcloud 扩展实现。情感趋势折线图的配置相对简单,核心是把后端的数组映射成 ECharts 的 xAxis 和 series 数据。
$.get("/api/trend", function (resp) { var chart = echarts.init(document.getElementById("trendChart")); chart.setOption({ xAxis: { type: "category", data: resp.dates }, yAxis: { type: "value", name: "平均情感分值" }, series: [{ data: resp.values, type: "line", smooth: true, areaStyle: { opacity: 0.2 } }] }); });这个请求对应后端的/api/trend接口。数据格式上,resp.dates 是日期字符串数组,resp.values 是浮点数数组。smooth 设为 true 可以让曲线更顺滑,减少因为是逐日统计带来的锯齿感。词云图需要额外引入 echarts-wordcloud 的 JS 文件,配置里的关键字段是wordCloud.sizeRange,它控制词的大小范围,值越大越夸张,我用的是[12, 60],新闻关键词热度差异明显的话效果更好。
6.3 端到端验证方式:整条链路跑通的标准
复现完这个平台后,我习惯按三个指标来验证它是否真的可交付使用。
第一是数据量验证。抓取完成后再查一次数据库,确认新闻表和评论表非空,且评论条数不少于 1000 条,否则后面的统计图表可能因为数据太少而显得不具说服力。
第二是接口验证。直接浏览器访问/api/trend,确认返回的 JSON 中 dates 和 values 长度一致,且数值范围在 0 到 1 之间。如果 values 全是 0.5 或者全是 1,说明情感分析环节出问题了,回查 SnowNLP 是否对空文本做了处理。
第三是页面巡检。用浏览器打开首页,看词云是否渲染成正常的中文词而非方框,看折线图是否有数据点,点击时间筛选按钮确认图表会重新请求接口并更新。这三步全过了,项目就算真正跑通了。
我在交付这个课程设计的时候,还会专门留一个 requirements.txt 文件,把依赖版本锁定到能跑的版本区间,因为 jieba、snownlp 这几个库的版本差异真的会影响分词结果和情感分值。从那以后我每次复现这类数据分析项目,都强制先把依赖环境用虚拟环境隔离好,版本不一致的问题比代码逻辑问题更隐蔽,也更浪费时间。希望这份拆解能帮你在自己的课程设计或毕业设计里少走几个弯路。
本文还有配套的精品资源,点击获取