☰
Python实战:网易新闻+评论舆情热点分析平台全链路解析
2026/10/1 19:13:39 网站建设 项目流程

简介:这是一套面向高校课程设计与Python进阶学习者的舆情分析实战项目源码,围绕网易新闻及其用户评论构建完整的数据采集与分析链路,帮助读者理解从爬虫抓取到情感研判的全流程实现。项目涵盖新闻标题与评论的定期抓取、热点话题实时追踪、基于NLP的情感倾向判断、趋势预测、关键词提取以及图表与时间线可视化,并支持自定义监测词、结果订阅和自动生成舆情报告,技术栈涉及Scrapy或BeautifulSoup、jieba、Gensim、Pandas、NumPy及Matplotlib等。压缩包为zip格式,整体约23.84MB,文件总数与类型明细上游暂未提供,但按项目结构可预期包含Python源码、依赖配置与数据存储相关文件。目前已有160人学习下载,适合需要课程设计参考、NLP入门实践或舆情监控原型搭建的读者,可据此快速理解模块划分、数据流转与可视化呈现方式,并在此基础上做二次开发与功能扩展。

1. 网易新闻加评论做舆情热点:这套 Python 方案到底解决什么问题

你手头有一份「python项目基于网易新闻+评论的舆情热点分析平台.zip」,第一反应大概率是:这玩意儿能跑起来吗,跑起来之后能看出什么名堂?我先把结论摆在这——它解决的不是「爬多少数据」的问题,而是「怎么把新闻正文和评论区放在一起看」的问题。单看新闻标题,你只能知道媒体在说什么;单看评论,你只能知道情绪大概偏哪边;两者一交叉,才能定位到「哪条新闻正在被讨论、讨论的焦点是什么、情绪往哪个方向走」。这套平台适合三类人:想拿一个完整 Python 项目练手的入门者、需要快速搭一个舆情监控原型的运营或产品、以及想把爬虫和数据分析串起来的中级开发者。它不追求工业级吞吐,追求的是链路完整、能本地跑通、能改。

2. 先想清楚数据从哪来:网易新闻与评论的采集边界

2.1 为什么选网易而不是别的源

做舆情分析,数据源的选择直接决定后面所有环节的天花板。网易新闻的特点是:新闻页结构相对稳定、评论区有独立的加载接口、评论带有「顶」数可以当热度权重。这三点对舆情分析很关键——结构稳定意味着爬虫维护成本低,独立评论接口意味着你可以只抓评论不抓正文,顶数意味着你能做加权排序而不是简单计数。

常见做法是先从新闻列表页拿到一批文章 ID,再逐篇请求详情页和评论接口。这里有个容易翻车的地方:列表页的排序会变,今天抓到的热榜和明天抓到的完全不是一批。所以我在实际项目里一般会加一个「抓取时间戳」字段,后面做趋势对比时按时间窗口切片,而不是拿两次抓取结果直接比。

2.2 采集层的最小可用代码

下面这段是采集层的骨架,重点看它怎么把新闻和评论分开存、怎么控制请求节奏。

import requests import time import json from datetime import datetime HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://news.163.com/" } def fetch_news_list(category="news", limit=30): """抓取新闻列表,返回文章ID和标题""" # 实际项目中这里替换为对应栏目的列表接口 url = f"https://news.163.com/special/0001386F/{category}.js" resp = requests.get(url, headers=HEADERS, timeout=10) resp.encoding = "utf-8" # 列表数据通常是JSONP或JSON,需按实际返回格式解析 data = json.loads(resp.text.strip().lstrip("data=").rstrip(";")) articles = [] for item in data[:limit]: articles.append({ "doc_id": item.get("docid"), "title": item.get("title"), "url": item.get("url"), "fetch_time": datetime.now().isoformat() }) return articles def fetch_comments(doc_id, page=1, page_size=20): """抓取单篇文章的评论,带顶数""" # 评论接口通常需要doc_id和分页参数 api = "https://comment.api.163.com/api/v1/products/a2869674571f77b5a0867c3d71db5856/threads/{}/comments/newList" params = {"offset": (page - 1) * page_size, "limit": page_size} resp = requests.get(api.format(doc_id), headers=HEADERS, params=params, timeout=10) result = resp.json() comments = [] for c in result.get("comments", {}).values(): comments.append({ "doc_id": doc_id, "content": c.get("content", ""), "vote": c.get("vote", 0), "create_time": c.get("createTime"), "user_location": c.get("user", {}).get("location", "") }) return comments if __name__ == "__main__": news = fetch_news_list(limit=5) for n in news: print(f"抓取: {n['title']}") cmts = fetch_comments(n["doc_id"]) print(f" 评论数: {len(cmts)}") time.sleep(1.5) # 控制频率,避免触发风控

这段代码的逻辑是:先拿列表,再逐篇拿评论,每条评论保留顶数、时间和用户地区。参数上,limit控制列表条数,page_size控制单次评论拉取量,time.sleep(1.5)是请求间隔。实际跑的时候你会发现,评论接口经常返回空或者只返回前几条,这时候要检查offset是否被限制、doc_id是否有效。我一般会先手动请求一次看返回结构,再决定解析字段。

提示:采集频率不要低于 1 秒一次,评论接口对高频请求比较敏感,连续快速请求容易拿到空数据。

3. 把新闻和评论拼成一张分析表:清洗与特征工程

3.1 中文文本清洗的四个必做动作

原始评论里混着表情符号、@某人、URL、重复刷屏。不清洗直接做分词,结果会很难看。我一般做四件事:去 URL、去 @、去纯符号、去重复。去重复不是简单 drop_duplicates,而是按「同一用户在同一文章下发相似内容」来判重,否则正常讨论也会被误删。

import re import pandas as pd def clean_text(text): if not isinstance(text, str): return "" text = re.sub(r"http[s]?://\S+", "", text) # 去URL text = re.sub(r"@[\w\u4e00-\u9fa5-]+", "", text) # 去@ text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9,。!?、;:]", "", text) # 保留中文和常用标点 text = re.sub(r"\s+", " ", text).strip() return text def build_analysis_table(news_list, comments_list): df_news = pd.DataFrame(news_list) df_cmt = pd.DataFrame(comments_list) df_cmt["clean_content"] = df_cmt["content"].apply(clean_text) df_cmt = df_cmt[df_cmt["clean_content"].str.len() > 2] # 过滤过短评论 # 按doc_id聚合评论数和总顶数 agg = df_cmt.groupby("doc_id").agg( comment_count=("content", "count"), total_vote=("vote", "sum"), avg_vote=("vote", "mean") ).reset_index() merged = df_news.merge(agg, on="doc_id", how="left") merged[["comment_count", "total_vote", "avg_vote"]] = merged[["comment_count", "total_vote", "avg_vote"]].fillna(0) return merged, df_cmt

清洗函数里,正则的顺序很重要:先去 URL 再去 @,最后做字符过滤。如果顺序反了,URL 里的字母会被保留下来。聚合的时候用groupby("doc_id")把评论数和顶数汇总到新闻维度,这样一条新闻就同时有标题、评论量、总热度。fillna(0)是防止没有评论的新闻在后续计算里变成 NaN。

3.2 分词与关键词提取的参数怎么定

中文分词用 jieba 是常规选择,但停用词表要自己补。网易评论里「小编」「网易」「新闻」这些词出现频率极高,不剔除会污染关键词结果。我一般会加载一份基础停用词表,再手动加 20 到 30 个领域词。

import jieba import jieba.analyse STOPWORDS = set(["的", "了", "是", "在", "我", "有", "和", "就", "不", "人", "都", "一", "一个", "上", "也", "很", "到", "说", "要", "去", "你", "会", "着", "没有", "看", "好", "自己", "这", "小编", "网易"]) def extract_keywords(texts, topk=20): corpus = " ".join(texts) # TF-IDF提取,允许词性过滤 keywords = jieba.analyse.extract_tags(corpus, topK=topk, withWeight=True, allowPOS=("n", "vn", "v")) return [(w, round(s, 4)) for w, s in keywords if w not in STOPWORDS] def tokenize(text): words = jieba.lcut(text) return [w for w in words if w not in STOPWORDS and len(w) > 1]

extract_tags的allowPOS参数控制保留哪些词性,n是名词、vn是动名词、v是动词。如果你只想要名词,就写("n",)。topK设 20 到 30 比较合适,太少看不出主题,太多会混入噪音。分词后的tokenize函数用于后续做词频统计或者喂给聚类模型。

4. 热点怎么算出来:从词频到情绪到聚类

4.1 热度排序的三个维度

舆情热点不是单纯按评论数排。我一般用三个维度加权:评论量、总顶数、评论增长速度。评论量代表参与度,顶数代表认同度,增长速度代表爆发性。三个维度归一化后加权求和,权重可以按场景调——做突发事件监控就把增长速度权重调高,做日常舆情日报就把评论量和顶数调高。

from sklearn.preprocessing import MinMaxScaler def compute_hot_score(df, w_count=0.4, w_vote=0.4, w_growth=0.2): scaler = MinMaxScaler() df["comment_count_norm"] = scaler.fit_transform(df[["comment_count"]]) df["total_vote_norm"] = scaler.fit_transform(df[["total_vote"]]) # 增长速度用评论数除以发布到抓取的时间差(小时) df["growth"] = df["comment_count"] / (df["hours_since_publish"].clip(lower=1)) df["growth_norm"] = scaler.fit_transform(df[["growth"]]) df["hot_score"] = (w_count * df["comment_count_norm"] + w_vote * df["total_vote_norm"] + w_growth * df["growth_norm"]) return df.sort_values("hot_score", ascending=False)

MinMaxScaler把三个量纲不同的指标拉到 0 到 1 之间,避免评论数几千、顶数几万导致权重失衡。clip(lower=1)是防止刚发布的新闻时间差为 0 导致除零。权重三个数加起来等于 1,调的时候保持这个约束。

4.2 情绪分析用 SnowNLP 还是自己训

SnowNLP 是入门最省事的选择,它对中文评论的情绪判断在 0 到 1 之间,大于 0.5 偏正面,小于 0.5 偏负面。但它对网络用语和反讽识别很差,比如「呵呵」它可能判成中性。我的做法是:用 SnowNLP 做初筛,把明显正负面的挑出来,边界样本(0.4 到 0.6 之间)单独看,必要时人工标注一批做微调。

from snownlp import SnowNLP def sentiment_analyze(text): if not text or len(text) < 3: return None s = SnowNLP(text) score = s.sentiments if score > 0.6: label = "positive" elif score < 0.4: label = "negative" else: label = "neutral" return {"score": round(score, 4), "label": label} # 对清洗后的评论批量打标 df_cmt["sentiment"] = df_cmt["clean_content"].apply(sentiment_analyze) df_cmt["sent_score"] = df_cmt["sentiment"].apply(lambda x: x["score"] if x else None) df_cmt["sent_label"] = df_cmt["sentiment"].apply(lambda x: x["label"] if x else None)

SnowNLP的sentiments属性直接返回概率值,不需要额外训练。批量处理时注意,空文本和超短文本要跳过,否则会报错或者给出无意义结果。打完标之后,按doc_id聚合正面和负面比例,就能看出哪条新闻的评论区情绪最极端。

4.3 用 TF-IDF 加 KMeans 做话题聚类

关键词提取是看「大家在说什么词」,聚类是看「大家在说几件事」。把评论分词后用 TF-IDF 向量化,再跑 KMeans,能把评论分成若干话题簇。K 值怎么定?我一般先跑 3 到 8 个簇,看轮廓系数和实际可解释性,选那个「每簇都能用一句话概括」的 K。

from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.cluster import KMeans def cluster_comments(texts, n_clusters=5): vectorizer = TfidfVectorizer( tokenizer=lambda x: tokenize(x), max_features=2000, min_df=2, max_df=0.8 ) X = vectorizer.fit_transform(texts) km = KMeans(n_clusters=n_clusters, random_state=42, n_init=10) labels = km.fit_predict(X) # 输出每簇的关键词 terms = vectorizer.get_feature_names_out() cluster_keywords = {} for i in range(n_clusters): center = km.cluster_centers_[i] top_idx = center.argsort()[-10:][::-1] cluster_keywords[i] = [terms[j] for j in top_idx] return labels, cluster_keywords

max_features=2000控制向量维度,太大跑得慢,太小丢信息。min_df=2过滤只出现一次的词,max_df=0.8过滤出现在 80% 以上文档里的词(通常是停用词没清干净)。n_init=10让 KMeans 多跑几次取最优,避免局部最优。聚类结果里每簇取中心向量最大的 10 个词作为该话题的标签。

5. 避坑与排查:这套链路最容易翻车的五个地方

5.1 评论抓回来是空的

现象:请求评论接口返回 200,但comments字段为空或者只有一两条。原因通常是doc_id不对、接口参数变了、或者请求头缺少 Referer。解决:先用浏览器开发者工具抓一次真实请求,对比 URL 和参数;确认doc_id是评论系统用的 ID 而不是新闻页 URL 里的 ID;补上 Referer 和 Cookie(如果需要)。

5.2 分词结果全是「的了吗呢」

现象:关键词提取出来全是停用词。原因:停用词表没加载或者加载了但没生效。解决:检查STOPWORDS是否在tokenize和extract_tags两处都做了过滤;jieba 的extract_tags有自己的停用词机制,需要单独设置jieba.analyse.set_stop_words("stopwords.txt")。

5.3 情绪分析结果和直觉完全相反

现象:明显负面的评论被判成正面。原因:SnowNLP 训练语料偏电商评论,对新闻评论的讽刺、反问识别差。解决:对边界样本做人工复核;或者用标注数据微调一个简单分类器;至少要在报告里注明「情绪分数仅供参考,极端值需人工确认」。

5.4 聚类跑出来每簇都差不多

现象:KMeans 分出来的簇关键词高度重叠。原因:TF-IDF 向量太稀疏或者 K 值选得不对。解决:增大max_features、调整min_df和max_df;先用轮廓系数扫一遍 K 值;如果评论本身话题集中,减少 K 值比硬分更合理。

5.5 热度排序被一条旧新闻霸榜

现象:一条几个月前的新闻因为评论数累积很高,一直排在热点第一。原因:没有做时间衰减。解决:在热度公式里加时间衰减因子,比如hot_score * exp(-hours_since_publish / 24),让旧新闻的热度随时间指数下降。

注意:时间衰减的系数要根据你的分析周期调,做日报用 24 小时,做周报用 72 小时,没有万能值。

6. 让平台能持续跑:定时任务与可视化落地技巧

这套东西跑一次不难,难的是每天自动跑、跑完能一眼看出变化。我一般用 APScheduler 做定时,每天早上八点抓一次,抓完直接算热度、情绪、聚类,结果写进 SQLite 或者 CSV,再用 Streamlit 或 Pyecharts 出一个单页看板。

from apscheduler.schedulers.blocking import BlockingScheduler import pandas as pd def daily_pipeline(): news = fetch_news_list(limit=50) all_comments = [] for n in news: cmts = fetch_comments(n["doc_id"], page_size=50) all_comments.extend(cmts) merged, df_cmt = build_analysis_table(news, all_comments) merged = compute_hot_score(merged) df_cmt["sent_score"] = df_cmt["clean_content"].apply( lambda x: SnowNLP(x).sentiments if len(x) > 2 else None ) merged.to_csv(f"hot_rank_{pd.Timestamp.now().date()}.csv", index=False) df_cmt.to_csv(f"comments_{pd.Timestamp.now().date()}.csv", index=False) print(f"完成: {len(news)} 篇新闻, {len(all_comments)} 条评论") if __name__ == "__main__": scheduler = BlockingScheduler() scheduler.add_job(daily_pipeline, "cron", hour=8, minute=0) scheduler.start()

BlockingScheduler适合单机跑,cron表达式里hour=8, minute=0表示每天八点执行。如果你要跑在服务器上,建议用BackgroundScheduler配合 systemd 或者 supervisor 做进程守护。输出文件按日期命名,方便回溯。

可视化这块,Streamlit 最省事,十几行就能出一个带筛选的表格和柱状图。关键技巧是:热度榜不要只显示标题,要把「评论数、正面比例、负面比例、Top3 关键词」拼成一列,这样一眼就能判断这条新闻值不值得点进去看。情绪趋势用折线图按小时聚合,能看出情绪拐点出现在哪个时间窗口。

最后说一个我自己的习惯:每次跑完 pipeline,我会先看热度前五的新闻标题和它们的 Top 关键词,如果关键词和标题对不上,说明清洗或者分词出了问题,先修数据再看结论。这个习惯帮我省了很多「分析结果看起来很美但其实是垃圾进垃圾出」的时间。希望帮到你。

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

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

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

立即咨询