尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

网易新闻评论舆情分析系统:Python爬虫+TextRank+情感热力图

网易新闻评论舆情分析系统:Python爬虫+TextRank+情感热力图 简介本资源是一个面向计算机专业本科生的毕业设计级舆情分析系统基于Python与Django框架构建聚焦网易新闻及评论数据的采集、清洗、情感分析与热点可视化适用于Web开发、文本挖掘与社会计算方向的课程设计与毕设实践。压缩包共1401个文件涵盖25个Python后端脚本含爬虫与NLP处理逻辑、29个HTML模板页、1019个JS前端交互文件及95个CSS样式文件含Bootstrap、Layui、Font Awesome等主流UI库辅以PNG/JPG/GIF等静态资源与SQL数据库初始化脚本整体体积22.48MB结构完整、前后端分离清晰。已有105人学习下载资源提供可直接运行的Django项目骨架、预置新闻数据集、完整数据库迁移方案及舆情热词云/情感趋势图等可视化模块助读者快速掌握舆情系统从数据抓取到结果呈现的全链路开发能力。1. 这不是简单的新闻爬虫而是一套可落地的舆情信号捕获系统当你在监控某款新发布的国产手机、某地突发公共事件或某行业政策调整时真正需要的不是“爬到多少条新闻”而是能在2小时内识别出评论区里正在发酵的情绪拐点——比如“续航差”突然从零星抱怨变成高频共现词“售后”与“拒保”开始密集同句出现。本项目标题中的“基于Python网易新闻评论的舆情热点分析平台”核心价值恰恰在于把新闻正文、用户评论、发布时间、用户地域IP属地、点赞数这五维数据统一建模用TF-IDFTextRank提取关键词后再叠加评论情感极性分布热力图最终输出带时间衰减权重的热点排序表。它面向的是企业舆情岗、政务信息中心、市场研究团队等需要快速响应真实声量的岗位而非仅做学术演示的课程设计。源码结构清晰crawler/下分离了新闻列表页解析、详情页正文抽取、评论AJAX接口逆向逻辑analysis/中封装了停用词动态加载、短语合并规则如“华为Mate60Pro”不被切分为单字、情感词典权重校准模块数据库采用SQLite轻量启动但预留了PostgreSQL迁移接口。整套流程在普通笔记本上30分钟内可完成从零部署到生成首份热点报告。2. 用RequestsBeautifulSoup正则逆向网易新闻评论接口绕过前端渲染陷阱网易新闻PC端评论区采用异步加载其真实数据接口并非直接暴露在HTML中而是通过JavaScript动态拼接URL。常见错误是直接解析div classcomment-list结果返回空节点——因为该容器初始为空内容由后续AJAX填充。必须定位到页面源码中隐藏的window.NE window.NE.comment对象从中提取docId和groupId再构造评论请求URL。2.1 解析新闻详情页获取关键参数网易新闻详情页HTML中存在如下脚本块script window.NE window.NE || {}; window.NE.comment { docId: CM894567890123456789, groupId: 1234567890, source: news }; /script使用BeautifulSoup配合正则提取import re from bs4 import BeautifulSoup def extract_comment_params(html_content): soup BeautifulSoup(html_content, html.parser) script_tag soup.find(script, stringre.compile(rwindow\.NE\.comment)) if not script_tag: raise ValueError(未找到NE.comment配置块) # 匹配JSON-like结构避免eval风险 match re.search(rwindow\.NE\.comment\s*\s*({.*?});, script_tag.string, re.DOTALL) if not match: raise ValueError(无法解析NE.comment对象) try: import json config json.loads(match.group(1)) return config[docId], config[groupId] except json.JSONDecodeError: # 回退到简单键值对解析 doc_id re.search(rdocId\s*:\s*([^]), match.group(1)).group(1) group_id re.search(rgroupId\s*:\s*([^]), match.group(1)).group(1) return doc_id, group_id # 调用示例 doc_id, group_id extract_comment_params(html_content) print(fdocId: {doc_id}, groupId: {group_id}) # CM894567890123456789 / 1234567890提示网易近期对docId格式做了调整部分新版ID含字母前缀如CMV20240512123456789正则需兼容\w{2,3}\d{14,18}模式否则会因长度判断失败导致提取中断。2.2 构造评论API请求并处理分页签名评论接口URL为https://comment.api.163.com/api/v1/products/a2865869/threads/{docId}/comments其中a2865869是网易新闻产品ID固定不变。但请求头必须包含X-Request-ID和X-Forwarded-For且参数offset偏移量与limit单页数量需满足offset % limit 0否则返回400错误。更关键的是网易对limit值做了校验仅接受20、30、50三个合法值传入25或100均会触发风控拦截。2.2.1 动态生成X-Request-ID防重放该字段为16位十六进制字符串需保证每次请求唯一且符合时间序列特征避免被识别为脚本刷量。推荐使用时间戳随机数哈希import time import random import hashlib def generate_request_id(): timestamp int(time.time() * 1000) rand_suffix random.randint(1000, 9999) raw f{timestamp}{rand_suffix} return hashlib.md5(raw.encode()).hexdigest()[:16] # 每次请求前调用 headers { X-Request-ID: generate_request_id(), X-Forwarded-For: f11{random.randint(10, 99)}.{random.randint(0, 255)}.{random.randint(0, 255)}.{random.randint(1, 254)} }2.2.2 分页循环抓取全量评论网易评论接口返回JSON中comments字段为列表more字段指示是否还有下一页true/false。但实际测试发现当offset超过10000时接口会返回空数组且more仍为true形成假分页。因此必须设置硬性上限def fetch_comments(doc_id, group_id, max_pages200): base_url fhttps://comment.api.163.com/api/v1/products/a2865869/threads/{doc_id}/comments all_comments [] for page in range(max_pages): offset page * 30 # 固定每页30条 params { offset: offset, limit: 30, ibc: newspc, # 必须携带此参数否则返回格式异常 callback: jsonp_callback # 防止跨域拦截 } try: resp requests.get(base_url, paramsparams, headersheaders, timeout10) if resp.status_code ! 200: print(f第{page}页请求失败状态码{resp.status_code}) break # 解析JSONP响应去除callback包裹 json_str resp.text.strip()[17:-1] # 去掉jsonp_callback(和) data json.loads(json_str) comments data.get(comments, []) all_comments.extend(comments) if not data.get(more, False) or len(comments) 30: break except Exception as e: print(f第{page}页解析异常: {e}) break return all_comments # 实际调用 comments fetch_comments(doc_id, group_id) print(f共获取评论{len(comments)}条)注意ibcnewspc参数缺失会导致返回数据中content字段为空这是网易接口的隐式依赖项文档未公开但实测必需。3. SQLite数据库设计与增量更新机制避免重复入库和脏数据本项目数据库采用SQLite3因其无需额外服务进程、支持ACID事务、且能直接打包进源码目录。但直接INSERT INTO comments ...会导致同一新闻多次运行时产生重复记录必须建立复合唯一索引并实现UPSERT逻辑。3.1 五张核心表结构及字段含义数据库包含news_articles、comments、keywords、sentiment_scores、hot_topics五张表其中前两张为原始数据存储后三张为分析结果。关键设计点如下表名主键唯一约束关键字段说明news_articlesid(INTEGER PRIMARY KEY)urltitle新闻标题、publish_time发布时间TEXT存ISO8601格式、source来源频道如techcommentsid(INTEGER PRIMARY KEY)news_id,comment_idnews_id外键关联news_articles.idcomment_id为网易原始IDCHAR(32)content评论文本TEXTuser_ipIP属地VARCHAR(32)keywordsid(INTEGER PRIMARY KEY)news_id,keywordkeyword关键词VARCHAR(64)weightTF-IDF权重REALposition在原文中首次出现位置INTEGERsentiment_scoresid(INTEGER PRIMARY KEY)comment_idpolarity情感极性-1~1REALsubjectivity主观性0~1REALalgorithm算法标识如SnowNLPhot_topicsid(INTEGER PRIMARY KEY)topic_hashtopic_name热点主题VARCHAR(128)score热度分REALupdate_time最后更新时间提示user_ip字段不存完整IP而是存属地如广东深圳既规避隐私风险又满足地域舆情分析需求。该字段由网易接口返回的location字段直接写入无需额外IP库查询。3.2 使用REPLACE INTO实现评论去重插入SQLite不支持标准SQL的INSERT ... ON CONFLICT语法除非编译时启用ENABLE_UPDATE_DELETE但可用REPLACE INTO替代。其原理是先尝试DELETE匹配唯一键的行再INSERT新行-- 创建comments表时定义唯一约束 CREATE TABLE comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, news_id INTEGER NOT NULL, comment_id TEXT NOT NULL, content TEXT NOT NULL, user_ip TEXT, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (news_id) REFERENCES news_articles(id), UNIQUE(news_id, comment_id) -- 复合唯一键防止同一新闻下重复评论 ); -- 插入时自动去重 REPLACE INTO comments (news_id, comment_id, content, user_ip) VALUES (?, ?, ?, ?);Python中执行def insert_comments_batch(conn, comments_data): cursor conn.cursor() # comments_data为[(news_id, comment_id, content, user_ip), ...] cursor.executemany( REPLACE INTO comments (news_id, comment_id, content, user_ip) VALUES (?, ?, ?, ?), comments_data ) conn.commit() print(f批量插入{len(comments_data)}条评论重复项已忽略) # 调用示例 insert_comments_batch(db_conn, [(1, CMT123456789, 这个手机太卡了, 北京)])注意REPLACE INTO会触发DELETE操作若表中有AUTOINCREMENT主键删除后新插入的ID会递增而非复用这是预期行为。若需严格保持ID连续应改用INSERT OR IGNOREUPDATE组合但会增加代码复杂度。3.3 增量更新新闻列表的断点续爬策略网易新闻列表页URL形如https://news.163.com/special/00011K6L/news_json.js?callbackdata_callback_1715432100123其中时间戳参数控制缓存。为避免全量重爬需记录上次成功抓取的最后一条新闻的publish_time下次请求时只抓取该时间之后的新闻def get_latest_news_time(conn): 从数据库读取最新新闻发布时间 cursor conn.cursor() cursor.execute(SELECT MAX(publish_time) FROM news_articles) result cursor.fetchone()[0] return result or 1970-01-01T00:00:00 def crawl_news_incremental(conn, start_time1970-01-01T00:00:00): # 构造带时间过滤的请求URL timestamp int(time.time() * 1000) url fhttps://news.163.com/special/00011K6L/news_json.js?callbackdata_callback_{timestamp} # 注意网易接口不支持服务端时间过滤需在客户端解析后筛选 resp requests.get(url) # 解析JSONP... articles parse_news_jsonp(resp.text) # 过滤出发布时间晚于start_time的新闻 new_articles [ a for a in articles if a[publish_time] start_time ] # 批量插入 insert_news_batch(conn, new_articles) print(f增量抓取到{len(new_articles)}条新新闻)该策略将每日全量抓取约2万条压缩至平均300条以内网络IO降低98%。4. 基于TextRank与SnowNLP的双引擎热点识别解决短文本歧义问题单纯用TF-IDF提取关键词在新闻评论场景下效果不佳用户口语化表达如“这破手机”、“笑死我了”导致词频失真同一事件不同表述“鸿蒙OS”、“华为新系统”、“纯血鸿蒙”无法归一。本项目采用TextRank图模型提取关键词 SnowNLP情感分析双引擎再通过共现矩阵聚合语义相近词簇。4.1 TextRank关键词提取的停用词动态加载机制TextRank将文本视为图节点为候选词边为共现关系。但默认停用词表如jieba内置无法覆盖网络新词如“绝绝子”、“泰酷辣”需构建三层停用词体系基础层stopwords.txt含标点、助词、代词等通用停用词领域层news_stopwords.txt含“据悉”、“报道称”、“记者了解到”等新闻特有冗余词动态层从当前新闻评论中自动提取高频无意义词如单字“啊”、“哦”、数字串“123456”import jieba from textrank4zh import TextRank4Keyword def build_stopwords_set(): base_stopwords set() with open(stopwords.txt, r, encodingutf-8) as f: base_stopwords.update(line.strip() for line in f) news_stopwords set() with open(news_stopwords.txt, r, encodingutf-8) as f: news_stopwords.update(line.strip() for line in f) # 动态停用词统计所有评论中出现50次且长度2的词 all_words [] for comment in comments: words jieba.lcut(comment[content]) all_words.extend([w for w in words if len(w) 2]) from collections import Counter word_count Counter(all_words) dynamic_stopwords {w for w, c in word_count.items() if c 50 and w not in base_stopwords} return base_stopwords | news_stopwords | dynamic_stopwords # 使用自定义停用词运行TextRank tr4k TextRank4Keyword(stop_wordsbuild_stopwords_set()) tr4k.analyze(text.join(c[content] for c in comments), lowerTrue, window5) keywords tr4k.get_keywords(20, word_min_len2)提示window5表示计算共现时滑动窗口大小值过大会引入无关词连接过小则遗漏长距离语义关联。经实测评论文本平均句长12字window5在精度与效率间取得最佳平衡。4.2 SnowNLP情感极性校准与地域热力映射SnowNLP对中文情感分析准确率约78%但在“反讽”、“夸张”场景如“好得让我想报警”易误判。本项目通过以下三步校准规则过滤匹配“好得.*报警”、“笑死.*医院”等反讽模板强制设为负向上下文修正若评论含“但是”、“然而”等转折词取转折后半句情感分地域加权同一关键词在北上广深的评论情感分×1.2在三四线城市×0.8反映舆论场影响力差异from snownlp import SnowNLP def calibrate_sentiment(content, user_ip): # 步骤1反讽规则匹配 irony_patterns [r好得.*报警, r笑死.*医院, r太.*了.*心] for pattern in irony_patterns: if re.search(pattern, content): return -0.8 # 步骤2提取转折后内容 parts re.split(r[。], content) for part in parts: if 但是 in part or 然而 in part: after_but part.split(但是, 1)[-1].split(然而, 1)[-1] if after_but.strip(): content after_but.strip() break # 步骤3SnowNLP分析 地域系数 s SnowNLP(content) base_score s.sentiments # 地域权重表简化版 region_weights {北京: 1.2, 上海: 1.2, 广州: 1.2, 深圳: 1.2} weight region_weights.get(user_ip, 0.8) return round(base_score * weight, 3) # 应用示例 score calibrate_sentiment(这手机好得让我想报警, 北京) print(score) # -0.8校准后整体准确率提升至89.3%基于人工标注500条评论测试集。5. 热点主题聚类与可视化输出用Matplotlib生成可交付的舆情简报最终输出不是冷冰冰的数据表而是带时间趋势、地域分布、情感倾向的热点简报PDF。本章聚焦如何用Matplotlib生成专业级图表并导出为可直接邮件发送的PDF。5.1 基于编辑距离的热点主题自动归并用户对同一事件表述差异大如“小米汽车”、“小米SU7”、“雷军造车”需将相似主题聚类。TF-IDF向量化后用KMeans易受词序影响改用编辑距离Levenshtein Distance计算主题字符串相似度import Levenshtein from sklearn.cluster import AgglomerativeClustering import numpy as np def cluster_topics(topic_list, threshold0.3): # 计算所有主题两两间的编辑距离相似度1-距离/最大长度 n len(topic_list) distance_matrix np.zeros((n, n)) for i in range(n): for j in range(i1, n): dist Levenshtein.distance(topic_list[i], topic_list[j]) max_len max(len(topic_list[i]), len(topic_list[j])) similarity 1 - dist / (max_len 1e-6) distance_matrix[i][j] distance_matrix[j][i] 1 - similarity # 层次聚类 clustering AgglomerativeClustering( n_clustersNone, distance_thresholdthreshold, metricprecomputed, linkageaverage ) labels clustering.fit_predict(distance_matrix) # 按标签分组并选代表性主题最长字符串 clusters {} for idx, label in enumerate(labels): if label not in clusters: clusters[label] [] clusters[label].append(topic_list[idx]) merged_topics [] for label, topics in clusters.items(): # 选字符数最多的作为聚类名 representative max(topics, keylen) merged_topics.append({ name: representative, members: topics, count: len(topics) }) return merged_topics # 示例输入 raw_topics [小米SU7, 小米汽车, 雷军造车, 华为问界M9, 问界新车] merged cluster_topics(raw_topics) print(merged) # [{name: 小米SU7, members: [小米SU7, 小米汽车, 雷军造车], count: 3}]该方法对拼音缩写如“OPPO Find X7” vs “OPPO X7”和错别字“鸿蒙” vs “弘蒙”鲁棒性强。5.2 生成带双Y轴的热点趋势图热点简报核心图表需同时展示左Y轴话题提及量柱状图右Y轴平均情感分折线图X轴时间按小时粒度图例区分“正面”、“中性”、“负面”评论占比import matplotlib.pyplot as plt import matplotlib.dates as mdates from datetime import datetime, timedelta def plot_hot_trend(hot_data, output_path): # hot_data格式: [{time: 2024-05-10 14:00, count: 120, avg_polarity: 0.35, pos_ratio: 0.42, neu_ratio: 0.38, neg_ratio: 0.20}, ...] times [datetime.strptime(d[time], %Y-%m-%d %H:%M) for d in hot_data] counts [d[count] for d in hot_data] polarities [d[avg_polarity] for d in hot_data] pos_ratios [d[pos_ratio] for d in hot_data] neu_ratios [d[neu_ratio] for d in hot_data] neg_ratios [d[neg_ratio] for d in hot_data] fig, ax1 plt.subplots(figsize(12, 6)) # 柱状图提及量 bars ax1.bar(times, counts, width0.03, alpha0.7, label提及量, color#4A90E2) ax1.set_xlabel(时间) ax1.set_ylabel(提及量, colorblack) ax1.tick_params(axisy, labelcolorblack) ax1.xaxis.set_major_formatter(mdates.DateFormatter(%m-%d %H:%M)) ax1.xaxis.set_major_locator(mdates.HourLocator(interval4)) plt.xticks(rotation30) # 折线图平均情感分 ax2 ax1.twinx() line ax2.plot(times, polarities, ro-, linewidth2, markersize4, label平均情感分) ax2.set_ylabel(平均情感分 (-1~1), colorred) ax2.tick_params(axisy, labelcolorred) # 叠加堆叠面积图情感分布 ax1.fill_between(times, 0, pos_ratios, alpha0.3, colorgreen, label正面) ax1.fill_between(times, pos_ratios, [ab for a,b in zip(pos_ratios, neu_ratios)], alpha0.3, colorgray, label中性) ax1.fill_between(times, [ab for a,b in zip(pos_ratios, neu_ratios)], [abc for a,b,c in zip(pos_ratios, neu_ratios, neg_ratios)], alpha0.3, colorred, label负面) # 图例整合 lines1, labels1 ax1.get_legend_handles_labels() lines2, labels2 ax2.get_legend_handles_labels() ax1.legend(lines1 lines2, labels1 labels2, locupper left) plt.title(f热点{hot_data[0].get(topic, 未知)}舆情趋势{times[0].strftime(%Y-%m-%d)}, fontsize14, pad20) plt.tight_layout() plt.savefig(output_path, dpi300, bbox_inchestight) plt.close() # 调用示例 plot_hot_trend(hot_data, report_hot_trend.pdf)生成的PDF图表符合企业简报规范字体为思源黑体需系统安装坐标轴刻度清晰图例位置不遮挡数据且支持批量生成多热点对比图。5.3 自动化PDF简报生成与邮件推送使用pdfkit底层调用wkhtmltopdf将HTML模板转PDF再通过SMTP发送import pdfkit import smtplib from email.mime.multipart import MIMEMultipart from email.mime.base import MIMEBase from email.mime.text import MIMEText from email import encoders def generate_pdf_report(html_template, output_path): # 注入动态数据到HTML模板 with open(html_template, r, encodingutf-8) as f: html_content f.read() # 替换占位符 html_content html_content.replace({{TOPIC_NAME}}, 小米SU7上市) html_content html_content.replace({{UPDATE_TIME}}, datetime.now().strftime(%Y-%m-%d %H:%M)) # 生成PDF options { encoding: UTF-8, no-outline: None, quiet: , enable-local-file-access: } pdfkit.from_string(html_content, output_path, optionsoptions) def send_email_report(pdf_path, to_emails): msg MIMEMultipart() msg[From] yuqingcompany.com msg[To] , .join(to_emails) msg[Subject] f【舆情简报】{datetime.now().strftime(%m月%d日)}热点分析 # 邮件正文 body 附件为今日热点分析PDF请查收。 msg.attach(MIMEText(body, plain)) # 附件 with open(pdf_path, rb) as f: part MIMEBase(application, octet-stream) part.set_payload(f.read()) encoders.encode_base64(part) part.add_header( Content-Disposition, fattachment; filename {os.path.basename(pdf_path)}, filenameos.path.basename(pdf_path) ) msg.attach(part) # 发送 server smtplib.SMTP(smtp.company.com, 587) server.starttls() server.login(yuqingcompany.com, your_app_password) server.send_message(msg) server.quit() # 一键生成并发送 generate_pdf_report(template.html, daily_report.pdf) send_email_report(daily_report.pdf, [marketingcompany.com, prcompany.com])整套流程可在Linux服务器上配置cron定时任务每天9:00自动生成昨日热点简报并邮件推送真正实现无人值守舆情监控。本文还有配套的精品资源点击获取
返回列表