ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python实战:网易新闻+评论舆情热点分析平台全链路解析

2026/10/1 19:13:40 拓冰建站 浏览量
Python实战:网易新闻+评论舆情热点分析平台全链路解析 简介这是一套面向高校课程设计与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(categorynews, limit30): 抓取新闻列表返回文章ID和标题 # 实际项目中这里替换为对应栏目的列表接口 url fhttps://news.163.com/special/0001386F/{category}.js resp requests.get(url, headersHEADERS, timeout10) 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, page1, page_size20): 抓取单篇文章的评论带顶数 # 评论接口通常需要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), headersHEADERS, paramsparams, timeout10) 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(limit5) 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(rhttp[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, ondoc_id, howleft) 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, topk20): corpus .join(texts) # TF-IDF提取允许词性过滤 keywords jieba.analyse.extract_tags(corpus, topKtopk, withWeightTrue, 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_count0.4, w_vote0.4, w_growth0.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(lower1)) 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, ascendingFalse)MinMaxScaler把三个量纲不同的指标拉到 0 到 1 之间避免评论数几千、顶数几万导致权重失衡。clip(lower1)是防止刚发布的新闻时间差为 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_clusters5): vectorizer TfidfVectorizer( tokenizerlambda x: tokenize(x), max_features2000, min_df2, max_df0.8 ) X vectorizer.fit_transform(texts) km KMeans(n_clustersn_clusters, random_state42, n_init10) 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_keywordsmax_features2000控制向量维度太大跑得慢太小丢信息。min_df2过滤只出现一次的词max_df0.8过滤出现在 80% 以上文档里的词通常是停用词没清干净。n_init10让 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(limit50) all_comments [] for n in news: cmts fetch_comments(n[doc_id], page_size50) 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(fhot_rank_{pd.Timestamp.now().date()}.csv, indexFalse) df_cmt.to_csv(fcomments_{pd.Timestamp.now().date()}.csv, indexFalse) print(f完成: {len(news)} 篇新闻, {len(all_comments)} 条评论) if __name__ __main__: scheduler BlockingScheduler() scheduler.add_job(daily_pipeline, cron, hour8, minute0) scheduler.start()BlockingScheduler适合单机跑cron表达式里hour8, minute0表示每天八点执行。如果你要跑在服务器上建议用BackgroundScheduler配合 systemd 或者 supervisor 做进程守护。输出文件按日期命名方便回溯。可视化这块Streamlit 最省事十几行就能出一个带筛选的表格和柱状图。关键技巧是热度榜不要只显示标题要把「评论数、正面比例、负面比例、Top3 关键词」拼成一列这样一眼就能判断这条新闻值不值得点进去看。情绪趋势用折线图按小时聚合能看出情绪拐点出现在哪个时间窗口。最后说一个我自己的习惯每次跑完 pipeline我会先看热度前五的新闻标题和它们的 Top 关键词如果关键词和标题对不上说明清洗或者分词出了问题先修数据再看结论。这个习惯帮我省了很多「分析结果看起来很美但其实是垃圾进垃圾出」的时间。希望帮到你。本文还有配套的精品资源点击获取