ARTICLE DETAIL

建站实战干货

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

Python实战:网易新闻评论舆情热点分析平台全流程

2026/10/1 19:13:40 拓冰建站 浏览量
Python实战:网易新闻评论舆情热点分析平台全流程 简介这是一套面向高校课程设计与Python进阶学习者的舆情分析实战项目源码围绕网易新闻及其用户评论构建完整的数据采集与分析链路帮助读者理解从爬虫抓取、文本清洗到热点识别与情感判断的全流程实现。项目涵盖新闻标题与评论的定期抓取、热点话题实时追踪、基于NLP的情感倾向分析、趋势预测、关键词提取、可视化展示、用户自定义监测以及自动报告生成等模块技术栈涉及Scrapy或BeautifulSoup、jieba、Gensim、Pandas、NumPy与Matplotlib等常用库并可选配SQLite、MySQL或MongoDB存储数据。压缩包为zip格式整体约23.84MB包内文件类型以项目源码、配置说明及数据样例为主目录按功能模块划分便于对照学习与二次开发。目前已有160人学习下载适合需要完成课程设计、积累NLP与爬虫实战经验或搭建舆情监控原型的读者参考借鉴。1. 从一份网易新闻评论数据说起舆情热点分析平台到底在分析什么很多人第一次听到「舆情热点分析平台」脑子里浮现的是满屏词云和折线图但真正动手做的时候第一个卡住的地方往往不是可视化而是数据从哪来、评论怎么和新闻对上、热点到底怎么定义。我拿到的这个项目标题是「python项目基于网易新闻评论的舆情热点分析平台」核心链路其实就三件事把网易新闻的正文和评论抓下来把评论里的情绪和关键词算出来再把「哪条新闻在什么时间点被讨论得最凶」这件事用图表讲清楚。它适合两类人一类是正在找 python 爬虫 数据分析 可视化完整练手项目的入门者另一类是想给自己业务加一个「舆情监控」模块的后端或数据工程师。前者能顺着走完采集、清洗、分析、展示全流程后者能直接拿走评论去重、时间窗口聚合、情感阈值这几块能落地的逻辑。下面我按实际做项目的顺序拆不按教科书的章节顺序。2. 数据采集层网易新闻正文与评论怎么稳定拿下来2.1 先搞清楚要抓哪几个字段别上来就写爬虫网易新闻的页面结构这几年改过好几轮但对我们做舆情分析来说真正需要的字段是固定的新闻 ID、标题、正文、发布时间、来源、评论总数以及每条评论的评论 ID、用户昵称可脱敏、评论内容、点赞数、评论时间、所在楼层。少一个「评论时间」后面做时间窗口热点就废了少一个「点赞数」就分不清是「很多人说」还是「一个人刷屏」。我一般会先建一张宽表把字段定死再反推采集逻辑。字段定不下来就写爬虫最后一定是边写边改血泪经验。字段名类型用途是否必采news_idstr新闻唯一标识关联评论是titlestr热点标题展示是contenttext正文分词、关键词提取是publish_timedatetime时间窗口聚合是comment_idstr评论去重是comment_texttext情感分析、词频是like_countint热度权重是comment_timedatetime热点爆发点定位是user_nickstr可脱敏后做水军识别否2.2 用 requests 正则/JSON 解析抓正文的最小可用脚本网易新闻的正文多数在静态 HTML 里评论走的是异步接口返回 JSON。下面这段是我常用的最小采集骨架先跑通单条新闻再扩到列表页。import requests import re import json import time 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_detail(news_url): 抓取单条新闻正文与基础信息 resp requests.get(news_url, headersHEADERS, timeout10) resp.encoding utf-8 html resp.text # 标题多数在 h1 里 title_match re.search(rh1[^]*(.*?)/h1, html, re.S) title title_match.group(1).strip() if title_match else # 发布时间常见在 meta 或特定 class 的 div 中 time_match re.search(r(publishTime|ptime)\s*:\s*([^]), html) publish_time time_match.group(2) if time_match else # 正文网易常用 post_body 包裹 body_match re.search(rdiv[^]*classpost_body[^]*(.*?)/div, html, re.S) content re.sub(r[^], , body_match.group(1)) if body_match else return { title: title, publish_time: publish_time, content: content.strip(), crawl_time: datetime.now().isoformat() } if __name__ __main__: demo_url https://news.163.com/xxxxxxxx.html # 替换为实际新闻链接 data fetch_news_detail(demo_url) print(json.dumps(data, ensure_asciiFalse, indent2)) time.sleep(1) # 控制频率别把人家服务器打挂逻辑说明fetch_news_detail只做一件事——把一条新闻的标题、时间、正文抽出来。正则里的post_body是网易正文常见的容器 class如果页面改版优先改这里。time.sleep(1)是硬性礼貌单机采集建议不低于 1 秒批量跑的时候用随机 1~3 秒更稳。参数说明timeout10防止某个请求卡死整个队列resp.encoding utf-8必须显式设置否则中文会乱码re.S让.能匹配换行正文里换行很多不加会截断。2.3 评论接口的分页与去重别让同一条评论进两次库评论接口通常带page和pageSize两个参数返回 JSON 里有comments数组和total。我一般用「评论 ID 集合」做去重而不是靠页码因为翻页过程中如果有新评论插入页码会错位。def fetch_comments(news_id, max_page50): 分页抓取评论按 comment_id 去重 seen set() all_comments [] for page in range(1, max_page 1): api fhttps://comment.api.163.com/api/v1/products/a2869674571f77b5a0867c3d71db5856/threads/{news_id}/comments/newList?offset{(page-1)*30}limit30 try: resp requests.get(api, headersHEADERS, timeout10) data resp.json() except Exception as e: print(fpage {page} failed: {e}) break comments data.get(comments, {}) if not comments: break for cid, item in comments.items(): if cid in seen: continue seen.add(cid) all_comments.append({ comment_id: cid, comment_text: item.get(content, ), like_count: item.get(vote, 0), comment_time: item.get(createTime, ), user_nick: item.get(user, {}).get(nickname, ) }) time.sleep(1.5) return all_comments逻辑说明接口返回的comments是一个以评论 ID 为 key 的字典直接遍历items()就能拿到 ID 和内容。用seen集合去重比事后在数据库里distinct更省资源。offset按 30 递增limit30是常见上限改大不一定生效。参数说明max_page50是保护性上限防止死循环time.sleep(1.5)比正文采集稍长因为评论接口调用更频繁vote字段就是点赞数有些版本叫against或support以实际返回为准。3. 数据清洗与存储把脏评论变成能算的输入3.1 评论清洗的四个必做动作原始评论里混着大量噪声表情符号、某人、URL、重复刷屏、纯数字。不清洗直接做情感分析准确率会被拉低一大截。我一般按顺序做四件事去 HTML 标签、去 URL、去 和话题标签、过滤长度小于 4 个字的评论。最后一步会误伤「支持」「反对」这种短评但为了整体信噪比值得。import re def clean_comment(text): if not text: return text re.sub(r[^], , text) # 去 HTML text re.sub(rhttps?://\S, , text) # 去 URL text re.sub(r[\w\u4e00-\u9fa5-], , text) # 去 text re.sub(r#.#, , text) # 去话题 text re.sub(r\s, , text).strip() # 压缩空白 return text def is_valid(text): return len(text) 4 and not text.isdigit()逻辑说明clean_comment是纯函数方便单测。is_valid把纯数字和过短评论挡在分析层之外。实际跑的时候我会先统计一下被过滤掉的比例如果超过 30%说明阈值太严得放宽到 3 个字。3.2 用 SQLite 做本地存储轻量、够用、方便复现这个项目的数据量通常在几十万条评论以内SQLite 完全扛得住而且不用装数据库服务对新手最友好。建两张表news和comment用news_id关联。import sqlite3 def init_db(db_pathyuqing.db): conn sqlite3.connect(db_path) cur conn.cursor() cur.execute( CREATE TABLE IF NOT EXISTS news ( news_id TEXT PRIMARY KEY, title TEXT, content TEXT, publish_time TEXT, source TEXT ) ) cur.execute( CREATE TABLE IF NOT EXISTS comment ( comment_id TEXT PRIMARY KEY, news_id TEXT, comment_text TEXT, like_count INTEGER, comment_time TEXT, user_nick TEXT ) ) cur.execute(CREATE INDEX IF NOT EXISTS idx_comment_news ON comment(news_id)) conn.commit() return conn逻辑说明comment_id设为主键天然去重idx_comment_news索引让「按新闻查评论」快一个数量级。publish_time和comment_time存文本方便直接比较如果要做复杂时间运算再转 datetime。参数说明db_path默认当前目录部署时改成绝对路径索引在数据量超过 1 万条后效果明显别省。4. 舆情分析核心情感打分、关键词与热点时间窗口4.1 情感分析选型SnowNLP 够用但要知道它的边界做中文舆情入门最省事的是 SnowNLP装完就能用不用训练。它的sentiments返回 0~1 的分数越接近 1 越正面。缺点是对方言、反讽、网络梗几乎无感所以我在项目里会加一层「关键词修正」命中负面词表就减分命中正面词表就加分。from snownlp import SnowNLP POS_WORDS [支持, 点赞, 太好了, 给力, 暖心] NEG_WORDS [垃圾, 恶心, 愤怒, 失望, 抵制] def sentiment_score(text): if not text: return 0.5 base SnowNLP(text).sentiments for w in POS_WORDS: if w in text: base min(1.0, base 0.1) for w in NEG_WORDS: if w in text: base max(0.0, base - 0.15) return round(base, 4)逻辑说明base是 SnowNLP 原始分词表修正幅度我设成正面 0.1、负面 -0.15负面权重更高因为舆情里负面信号更值得警惕。词表要按你的领域改通用词表在垂直场景下会失灵。参数说明min和max防止分数越界round保留四位存库时省空间。4.2 关键词提取与热点时间窗口聚合关键词用 jieba 的analyse.extract_tags就够取 Top 20。热点时间窗口我按「每小时评论数」聚合再算一个「热度值 评论数 × 平均点赞数」这样能把「讨论多但没人赞」和「讨论少但高赞」区分开。import jieba.analyse from collections import defaultdict from datetime import datetime def extract_keywords(texts, topk20): merged .join(texts) return jieba.analyse.extract_tags(merged, topKtopk, withWeightTrue) def hourly_heat(comments): 按小时聚合热度 bucket defaultdict(lambda: {count: 0, like_sum: 0}) for c in comments: try: t datetime.fromisoformat(c[comment_time]) except Exception: continue key t.strftime(%Y-%m-%d %H:00) bucket[key][count] 1 bucket[key][like_sum] c.get(like_count, 0) result [] for hour, v in sorted(bucket.items()): avg_like v[like_sum] / v[count] if v[count] else 0 result.append({ hour: hour, count: v[count], heat: round(v[count] * avg_like, 2) }) return result逻辑说明extract_keywords把所有评论拼成一个大文本再提词适合看整体舆论焦点如果要看单条新闻的关键词就按news_id分组后再调。hourly_heat里的heat是复合指标单纯看评论数会漏掉「高赞少评」的爆点。参数说明topK20是经验值太多会稀释重点strftime的格式要和comment_time实际格式对齐不对齐就全进不了桶这是最常见的翻车点。5. 可视化与避坑把分析结果讲成一张能看的图5.1 用 Pyecharts 出三张核心图可视化我选 Pyecharts因为它生成的 HTML 可以直接嵌到 Flask 页面里不用前端写太多。三张图必做情感分布饼图、每小时热度折线图、关键词词云。from pyecharts.charts import Pie, Line, WordCloud from pyecharts import options as opts def sentiment_pie(pos, neu, neg): c Pie() c.add(, [(正面, pos), (中性, neu), (负面, neg)]) c.set_global_opts(title_optsopts.TitleOpts(title情感分布)) return c.render_embed() def heat_line(hourly_data): c Line() c.add_xaxis([d[hour] for d in hourly_data]) c.add_yaxis(热度, [d[heat] for d in hourly_data]) c.set_global_opts( title_optsopts.TitleOpts(title每小时舆情热度), xaxis_optsopts.AxisOpts(name时间), yaxis_optsopts.AxisOpts(name热度值) ) return c.render_embed()逻辑说明render_embed()返回 HTML 字符串直接塞进模板变量。情感分布的三档划分分数 0.6 算正面 0.4 算负面中间算中性阈值可以按业务调。参数说明add_xaxis的列表长度要和add_yaxis一致不一致会报错时间轴如果太密用AxisOpts(axislabel_optsopts.LabelOpts(rotate45))旋转标签。5.2 避坑与常见问题排查现象一评论抓回来全是乱码。原因响应编码没设对requests 默认可能按 ISO-8859-1 解。解决显式resp.encoding utf-8如果还乱就试resp.apparent_encoding。现象二翻页抓到重复评论去重后数量对不上总数。原因接口在翻页期间有新评论插入页码偏移。解决改用offset递增而不是页码并用评论 ID 集合去重别信total。现象三SnowNLP 对反讽评论打分完全反了。原因模型训练语料偏新闻对网络反讽无感。解决加负面词表修正或者对高赞评论做人工抽检把明显错的挑出来做规则补丁。现象四热度折线图某个小时突然爆高点进去发现是刷屏。原因同一用户短时间大量评论。解决在聚合前按user_nick做频次限制比如同一用户同一小时超过 20 条就降权或剔除。现象五SQLite 写入几十万条后查询变慢。原因没建索引或事务太大。解决comment_id主键 news_id索引批量插入用executemany并每 1000 条 commit 一次。6. 进阶技巧把「热点」从静态报表变成可追踪的信号做到上面那一步你已经有一个能跑的平台了但它还是「事后报表」。真正有价值的是把它变成「信号追踪」给每条新闻算一个热度基线当某小时热度超过基线 3 倍时触发告警。我一般用滑动窗口算基线窗口取过去 24 小时阈值系数取 2.5~3太低会天天告警太高会漏掉真热点。def detect_burst(hourly_data, window24, factor3.0): 滑动窗口检测热度突增 alerts [] for i in range(window, len(hourly_data)): history [d[heat] for d in hourly_data[i-window:i]] baseline sum(history) / len(history) if history else 0 current hourly_data[i][heat] if baseline 0 and current baseline * factor: alerts.append({ hour: hourly_data[i][hour], heat: current, baseline: round(baseline, 2), ratio: round(current / baseline, 2) }) return alerts逻辑说明window24表示用过去 24 小时做基线适合日级舆情如果是突发事件监控窗口缩到 6 小时更灵敏。factor3.0是经验阈值我试过 2.0 会误报太多4.0 又会漏3.0 在多数场景下平衡得比较好。参数说明baseline 0的判断必须有否则冷启动阶段会除零ratio存下来方便事后复盘阈值设得对不对。还有一个我踩过的坑别把「评论数」直接当热度。有些新闻评论少但每条都几百赞实际影响力比评论多但全是「路过」的新闻大得多。复合指标count × avg_like是我试过最稳的简化方案如果你有转发数据再加一层转发权重会更准。最后说个习惯每次调完阈值或词表我都会把前一周的数据重跑一遍对比告警列表和人工判断的差异差太多就回去改参数。这个项目不难难的是让分析结果真的能被人用起来而不是生成一堆没人看的图。希望帮到你。本文还有配套的精品资源点击获取