尧图精选

基于Python的舆情热点分析平台:爬虫、情感分析与可视化实战

🕒 发布时间:2026/9/12 13:25:40 📁 来源:尧图网络
简介基于网易新闻与评论的舆情热点分析平台是一套Python毕业设计完整源码集成爬虫抓取、MySQL存储、NLP文本分析与可视化展示适合计算机相关专业学生用于毕业设计、课程设计也适合舆情分析、数据挖掘初学者进行系统学习和二次开发。资源包共1402个文件压缩包大小22.23MB包含Python程序py、pyc、数据库脚本sql、基于Bootstrap、Layui等前端框架的js/css/html资源以及图片和部署说明文档前后端结构完整目录划分清晰。压缩包内带有python部署说明、数据库建表文件和完整的爬虫、分析、展示模块涵盖新闻列表抓取、评论动态加载处理、关键词提取、情感倾向分析与结果图表展示等关键环节可直接运行或按需修改便于深入理解爬虫逆向、数据清洗、NLP热点挖掘等实现细节。目前已有84人学习下载可作为毕业设计选题参考、课程设计项目模板或舆情分析系统的基础源码。1. 从毕设源码到真能演示的舆情平台差在“评论”这条数据流网上搜“基于网易新闻评论的舆情热点分析平台”能找到不少 python 毕业设计源码但很多下载下来只有爬虫脚本和几张截图跑完第一次抓取就不知道下一步该做什么。这个题目真正的分界点不在“爬新闻标题”而在“评论”这条数据流新闻本身只告诉你发生了什么评论的增量、点赞和情绪才反映公众态度。要把两者合并成可排序的热点分数就得把抓取、清洗、分词、情感分析和可视化串成一条完整链路。这也正是这个项目适合 python 入门的同学做综合练习、也适合答辩现场直接演示的原因。下面按搭这套系统时的顺序拆开讲尽量把参数和坑留在代码附近。2. 基于网易新闻评论的舆情爬虫从 docid 到热门评论接口2.1 先摸清新闻 URL 与评论接口的对应关系做 python 爬虫之前先把数据源结构理清。网易新闻正文页 URL 一般是几级目录加文件名例如https://www.news.163.com/24/1101/12/XXXX.html文件名去掉.html得到的就是 docid。评论接口按 docid 组织最常见路径形式是comment.api.163.com/api/json/post/list/new/hot/{boardid}/{docid}返回 JSON里面同时有 commentCount、hotPosts、newPosts 三个关键字段。数据项来源用途docid正文 URL 文件名拼接评论接口boardid从页面源码或 Network 面板反查拼接评论接口commentCount评论接口 JSON热度分基础值hotPosts评论接口 JSON高赞评论内容、投票数newPosts评论接口 JSON按时间排序的评论用于窗口增量boardid 不一定总是cm开头不同频道会有差异所以我会在抓列表页时把 boardid 一起存进数据库而不是在代码里写死。提取 docid 的写法很简单import re def extract_docid(url: str) - str: # 匹配形如 /24/1101/12/ABC123.html 的路径 pattern r/(\d{4})/(\d{4})/(\d{2})/([A-Za-z0-9_])\.html m re.search(pattern, url) if not m: return docid m.group(4) # 少数 URL 会带 _xxx 后缀这里只保留主 id return docid.split(_)[0]正则里年份四位、月份和日期各两位文件名用[A-Za-z0-9_]而不是点号匹配是为了避免把 URL 上的查询参数带进 docid。提取成功后后续所有请求都以这个字符串为键。2.2 用 requests lxml 抓新闻列表按 href 模式筛链接新闻列表页和正文页我都用 requests lxml 处理。网易改版频率不低CSS 类名经常变所以不要依赖某一个 class 选择器而是先枚举页面上所有链接用 href 模式筛出新闻地址。代码如下import requests from lxml import html as lh HEADERS {User-Agent: Mozilla/5.0 ... Chrome/120.0 Safari/537.36} def fetch_news_links(list_url: str): resp requests.get(list_url, headersHEADERS, timeout15) resp.encoding resp.apparent_encoding or utf-8 doc lh.fromstring(resp.text) links [] for a in doc.cssselect(a): href a.get(href) or if re.search(r/\d{4}/\d{4}/\d{2}/[A-Za-z0-9_]\.html, href): title (a.text_content() or ).strip() links.append({ title: title, url: href if href.startswith(http) else https://www.news.163.com href }) return links这段代码没有写死新闻区块的 class只要页面里存在符合条件的链接就能抓到缺点是会混入少量频道页链接所以入库前还要用标题长度和正文容器判断过滤。resp.apparent_encoding是 requests 根据内容猜测编码抓中文页面时能避免大部分乱码问题。正文抓取也按同样原则先找一个常见正文容器找不到就退化成抓所有p文本拼接。常见容器是classpost_body或idcontent这类具体以抓回来的 HTML 为准。拼接时补一个空行分隔避免段落粘连。2.3 热门评论接口offset/limit 分页与字段筛选评论接口返回的不是简单列表而是一个字典最常消费的字段是 commentCount、hotPosts。hotPosts 里每条评论包含 content、voteCount、replyCount 和 user.nickname。写一个拉取函数def fetch_hot_comments(docid: str, boardid: str, offset: int 0, limit: int 30): api fhttps://comment.api.163.com/api/json/post/list/new/hot/{boardid}/{docid} params {offset: offset, limit: limit} resp requests.get(api, headersHEADERS, paramsparams, timeout10) if resp.status_code ! 200: return [], 0 payload resp.json() comments payload.get(hotPosts) or [] total payload.get(commentCount) or 0 clean [ { nickname: c.get(user, {}).get(nickname, ), content: c.get(content, ), vote: c.get(voteCount, 0), reply: c.get(replyCount, 0), create_time: c.get(createTime, 0), } for c in comments if c.get(content) ] return clean, total常用分页参数是 offset 和 limit一次请求建议控制在 30 条以内offset 按 30 递增。这里用payload.get(hotPosts) or []而不是直接索引是因为热门评论为空时服务端可能不返回该字段。createTime 一般是毫秒级时间戳入库前统一除以 1000 转成秒。2.4 抓取节奏、断点与异常处理毕业设计场景不需要多大吞吐单线程加延时就能满足。我会在每两个请求之间加time.sleep(random.uniform(1, 2))。新闻接口对访问频率比评论接口更敏感评论接口偶尔返回空 JSON 也可能只是该新闻评论较少不一定是封禁。更稳的做法是抓取循环里记住上次抓到的 docid 或 createTime每次启动先查库从断点继续避免重复抓整个列表。对 requests 的 RequestException 和超时做三层重试重试间隔按 1 秒、3 秒、10 秒递增。入库时用 docid 做唯一键重复请求同一篇文章只更新评论数不新增记录后面算热度时才不会重复计数。3. 舆情数据的清洗规则与 SQLite/MySQL 存储结构3.1 三张表的设计新闻表、评论表、热点结果表我会先建三张表news 存新闻元数据comment 存评论明细hotspot 存每个时间窗口算出来的热点结果。news 和 comment 之间用 docid 关联hotspot 里冗余一份 keyword 列表方便页面直接展示。建表 SQL 如下CREATE TABLE IF NOT EXISTS news ( id INTEGER PRIMARY KEY AUTOINCREMENT, docid TEXT UNIQUE NOT NULL, title TEXT, url TEXT, channel TEXT, publish_time INTEGER, comment_count INTEGER DEFAULT 0, content TEXT ); CREATE TABLE IF NOT EXISTS comment ( id INTEGER PRIMARY KEY AUTOINCREMENT, comment_id TEXT UNIQUE, docid TEXT, nickname TEXT, content TEXT, vote_count INTEGER DEFAULT 0, reply_count INTEGER DEFAULT 0, create_time INTEGER, sentiment_score REAL DEFAULT NULL ); CREATE TABLE IF NOT EXISTS hotspot ( id INTEGER PRIMARY KEY AUTOINCREMENT, window_start INTEGER, keyword TEXT, heat_score REAL, positive_ratio REAL, emotion_label TEXT, docid TEXT, UNIQUE(window_start, docid, keyword) );news 表的 docid 建立唯一索引这是整个系统的主关联键comment 表同样用 comment_id 去重防止同一条评论被重复抓入造成评论数虚高。hotspot 表的 window_start 是时间窗口起点按小时整点对齐这样前端画趋势图时能直接按时间排序不需要在查询时再做一次聚合。3.2 清洗HTML 标签、表情符、用户和连续标点评论内容不洗一下后续 jieba 分词和情感分析都会受影响。常见需要处理的是残留 HTML 实体、emoji、用户名、连续重复标点。清洗函数可以写成import re def clean_comment(text: str) - str: text re.sub(r[^], , text) # 去掉残留标签 text re.sub(r\w{1,20}, , text) # 去掉 用户 text re.sub(r[\U0001F000-\U0001FAFF], , text) # 去掉 emoji text re.sub(r([。!?,\.])\1, r\1, text) # 压缩连续标点 return text.strip()噪声类型处理方式注意点残留 HTML 标签[^]正则删除要先于其他规则执行用户名\w{1,20}删除部分昵称含下划线需单独补emoji按 Unicode 扩展区过滤删除后可能留空格再 strip 一次连续中文标点压缩成单个防止分词把句子切碎清理完还要做 md5 去重同一用户在不同新闻下复制粘贴的评论并不少见直接把 content 的 md5 作为辅助去重键能明显降低后面情感聚合时的噪音。3.3 为什么先用 SQLite再平滑迁移 MySQL演示环节没有并发写需求时sqlite3 标准库最省事不用额外装服务。但如果你在简历里写了 MySQL最好把数据库操作封装成独立模块。我的做法是 db.py 里只暴露 insert_news、insert_comment、query_hotspots 几个函数SQLite 和 pymysql 各自实现一份。迁移时只需要把连接串换掉import pymysql conn pymysql.connect(hostlocalhost, userroot, password..., charsetutf8mb4)MySQL 的 text 字段默认排序规则对中文 emoji 不友好建库时就指定 utf8mb4。迁移完成后把 news 表的 docid 唯一索引重建一次评论表则按 create_time 建普通索引后面按时间窗口查增量时速度差异会很明显。4. 从 jieba 分词到时间衰减热点评分与情感分析核心模型4.1 热度公式评论量、点赞量、情绪比和时间衰减热点不能只看评论总数。一条 3 万评论的旧闻躺一周不该比一条当天突发、评论只有 3000 的文章更“热点”。我常用的是时间窗口 对数压缩 半衰期衰减的组合heat 0.5 * ln(comment_count 1) 0.3 * ln(vote_total 1) 0.2 * positive_ratio再乘以 exp(-hours_since_publish / HALF_LIFE)。HALF_LIFE 默认取 6意思是 6 小时前新闻的热度会衰减到当前的一半。用对数而不是原始评论数是为了避免头部新闻把长尾完全压没。import math def heat_score(comment_count: int, vote_total: int, positive_ratio: float, hours_since_publish: float, half_life: float 6.0) - float: base 0.5 * math.log(comment_count 1) 0.3 * math.log(vote_total 1) 0.2 * positive_ratio return round(base * math.exp(-hours_since_publish / half_life), 4)三个权重系数不是拍脑袋定的后面校准时能通过 6.1 节的方法继续调。这里先把评论数、赞数、情绪比压到可对比的量纲再乘时间衰减排序时就能直观比较不同时间窗口的新闻。4.2 jieba 分词与 TF-IDF 关键词抽取先过滤“网易”“新闻”中文分词默认用 jieba多轮抓取后积累一批领域词典把“辟谣、通报、回应”这类舆情动词加进 userdict.txt让它们不被拆散。关键词抽取最简单的是 TF-IDFjieba 自带 extract_tagsimport jieba.analyse STOPWORDS {网易, 新闻, 客户端, 视频, 记者, 我们, 你们} def extract_keywords(text: str, topk: int 8): tags jieba.analyse.extract_tags(text, topKtopk, withWeightTrue) result [] for word, weight in tags: if word in STOPWORDS or len(word) 2: continue result.append(word) return resultTF-IDF 对新闻正文里反复出现的报道套话很敏感所以停用词表至少要包含“网易、新闻、客户端、记者、来源、编辑”。想再高一档可以把每天的所有候选关键词用 TextRank 跑一遍再取交集但毕设阶段 extract_tags 已经够展示效果。4.3 SnowNLP 情感得分与情绪占比聚合情感分析直接用 SnowNLP理由是第一不需要训练集第二对短评的效果在小规模数据上可接受。每条评论拿到一个 0 到 1 之间的 sentiments 值按阈值映射成正面、中性、负面情感得分范围标签 0.4负面0.4 ~ 0.6中性 0.6正面聚合到新闻维度就是统计三类占比把正面比例作为热度的情绪分量。实现from snownlp import SnowNLP def sentiment_label(score: float) - str: if score 0.4: return negative if score 0.6: return positive return neutralSnowNLP 对反讽、表情包基本失效这是语料训练决定的不是代码问题。答辩时被问就说“词典模型兜底 阈值可配”后续换 BERT 或大模型只改动这一个函数即可。4.4 同义词合并与相似新闻聚类同一个事件常被多个频道发一遍标题不同但正文段落大量重叠。直接用 docid 去重只能处理完全重复处理不了“同事件不同稿”。最省事的方案是对每篇新闻的关键词集合算 Jaccard 相似度相似度大于 0.4 就归为一簇def jaccard(a: set, b: set) - float: if not a or not b: return 0.0 return len(a b) / len(a | b)把热搜关键词表里每个候选词和已有簇的“簇关键词”比较命中则并入簇并更新时间窗口。这个逻辑放在热点计算的最后一步先算出每条新闻的 heat_score 再聚簇避免聚类把不同窗口的事件混在一起。真实数据里同一事件通常能聚合出 3 到 5 篇新闻热点总分取簇内最大 heat_score。5. 用 Flask ECharts 搭建舆情热点分析平台5.1 按“平台”而不是“脚本”组织代码目录抓取、计算、展示如果全揉在一个文件里答辩时很难讲清楚数据流。我习惯把工程拆成四块spider 负责采集core 负责分词与热度计算db.py 做持久化app.py 只暴露 HTTP 接口。目录如下news_hotspot/ ├── app.py ├── db.py ├── requirements.txt ├── spider/ │ ├── __init__.py │ └── news_spider.py ├── core/ │ ├── __init__.py │ ├── hotspot.py │ └── sentiment.py └── templates/ └── dashboard.html数据流是单向的spider 写库hotspot 定时读库并写 hotspot 表Flask 从库查结果给前端前端用 ECharts 出图。只要不出现页面直接调爬虫函数这种写法后面扩展定时任务就很容易。5.2 后端Flask 路由与 JSON 接口后端接口不需要太花哨热点列表、情感占比、趋势数据三个接口足够撑起页面。核心路由代码from flask import Flask, jsonify, render_template app Flask(__name__) app.route(/) def dashboard(): return render_template(dashboard.html) app.route(/api/hotspots) def api_hotspots(): rows query_hotspots(limit50) return jsonify({code: 0, data: rows}) app.route(/api/sentiment) def api_sentiment(): rows query_sentiment_summary() return jsonify({code: 0, data: rows}) app.route(/api/trend) def api_trend(): rows query_trend() return jsonify({code: 0, data: rows})三个路由分别对应页面首屏、热榜表格、趋势折线图。query_hotspots 返回的字段包含 window_start、keyword、heat_score、docid前端不需要知道底层 SQL 结构。接口主要返回字段前端用途/api/hotspotskeyword, heat_score, window_start热榜表格/api/sentimentpositive, neutral, negative情感饼图/api/trendtime_point, avg_heat趋势折线图5.3 前端ECharts 折线图与热点表格页面用一个自适应布局上半部分是热度趋势折线图下半部分是热点关键词表格再加一个情感占比饼图。折线图的 setOption 片段const chart echarts.init(document.getElementById(trend)); chart.setOption({ xAxis: { type: category, data: timePoints }, yAxis: { type: value, name: 热度 }, series: [{ name: 热点热度, type: line, smooth: true, areaStyle: {}, data: heatValues }] });xAxis 的 data 从接口返回的 window_start 格式化得到series 里的 heatValues 是同一窗口内 top 10 热点的平均值。ECharts 折线图和词云是两个独立插件词云需要额外引入 echarts-wordcloudscale 和 sizeRange 调大一点词语过密时显示效果更好。5.4 定时抓取APScheduler 让平台自己跑起来平台要展示“自动监测”不能靠手动跑脚本。用 APScheduler 的 BackgroundScheduler 挂两个周期任务每 30 分钟抓取一次最新新闻和评论每 15 分钟计算一次热点。核心代码from apscheduler.schedulers.background import BackgroundScheduler scheduler BackgroundScheduler() scheduler.add_job(run_spider, interval, minutes30) scheduler.add_job(compute_hotspot, interval, minutes15) scheduler.start()两个任务间隔不同是对的爬虫太频繁会给新闻接口压力热点计算要等新评论入库后再跑所以给 15 分钟窗口。演示时如果不想等可以临时把分钟参数改成 seconds。6. 舆情热点分析平台的参数调优与效果验证6.1 半衰期与权重系数怎么调才有说服力HALF_LIFE 和三大权重是热度输出的决定性参数。最简单的标定办法是抓取最近 7 天数据对照网易首页的实时热榜把能对上的人工标成 1其余标成 0。然后小步调参先固定 half_life6把权重在 0.5/0.3/0.2 附近来回试以排序结果重合度为评判目标再把 half_life 分别改成 2、4、8、12 看衰减曲线。舆情事件发酵通常在发布后 2 到 4 小时达到峰值所以 half_life 取 4 到 8 都算合理别设成 24 小时以上。6.2 用 NDCG10 验证热点排序质量人工看排序结果不够量化建议给排名指标加一个 NDCG10。先准备真实相关序列 rel正样本为命中的热搜新闻pred 是模型热度排序结果计算公式import numpy as np def ndcg_at_k(pred_relevance: list, k: int 10) - float: pred pred_relevance[:k] dcg sum((2**r - 1) / np.log2(i 2) for i, r in enumerate(pred)) ideal sorted(pred_relevance, reverseTrue)[:k] idcg sum((2**r - 1) / np.log2(i 2) for i, r in enumerate(ideal)) return dcg / idcg if idcg 0 else 0.0每次调参后重算一遍 NDCG把参数组合和分数记在同一张表里比“凭感觉看起来准了”更有说服力。注意这里传的 pred_relevance 是每条结果的真实相关性取值 0 或 1列表顺序按模型排序来取。6.3 部署清单python 版本、编码与 403 处理复现这套系统时建议直接用 python 3.10 或 3.11太老的版本对 snownlp 依赖的 numpy 不友好装完依赖后第一件事是用 VSCode 配置 python 环境把解释器选到项目的 venv 里避免依赖装错位置。文件读写统一用 utf-8否则评论里的繁体字容易在写入 SQLite 时乱码。接口 403 时先看 User-Agent 是否真实浏览器再看是不是抓取频率过高把 sleep 区间拉长并补上浏览器 Cookie 头通常能恢复。把 HALF_LIFE 调成 6 小时后重新抓一天数据再看 NDCG 涨幅这是最容易上手的一条校准路径。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →