资讯动态

基于Python的舆情热点分析平台:从网易新闻爬虫到情感可视化

发布时间:2026/9/24 0:49:19 来源:尧图企业网站定制
简介面向Python课程设计与毕业设计的一站式舆情热点分析平台源码完整覆盖从网易新闻及评论抓取、数据清洗、中文分词、停用词过滤、情感分析、关键词提取到时间序列分析与可视化展示的典型数据科学流程。资源共1403个文件约23.83MB其中js/css/html文件用于搭建交互前端py/pyc文件承载爬虫、分析与后端服务逻辑csv/sql提供样例数据与建表脚本另含bootstrap、layui等前端框架样式便于直接改造界面。已有166人学习。入手后可对照源码梳理requests、jieba、SnowNLP、sklearn、matplotlib等库的协作方式理解舆情热度计算与情感判别的实现细节并借助前端模板快速生成可视化看板。对于需要快速跑通舆情分析场景并完成毕设展示的同学是兼具完整性与可扩展性的参考资料。1. 舆情热点分析平台到底在解决什么问题做舆情分析的人最烦的一件事就是热点和数据对不上。早上还在全网讨论的事件中午评论风向就变了等到你手工汇总完新闻和评论热点已经凉了一半。“python083基于网易新闻评论的舆情热点分析平台”这个项目核心就是解决这个时效性问题用 python 爬虫定时抓取网易新闻的实时榜单和新闻正文再把每篇文章底下的用户评论一并拉下来经过文本清洗、情感打分和热点聚类最后输出一张能看到“什么话题在升温、舆论是正面还是负面”的可视化看板。对于正在学 python 数据分析与可视化、或者被领导临时安排“搭一个网络舆情分析原型”的从业者来说这是一个可以直接改改就能用的脚手架。它不追求大而全的分布式架构而是用最朴素的 requests jieba pyecharts 组合把一条完整的数据链路跑通。2. 新闻采集用 requests 拉取网易新闻列表与评论接口整个平台的地基是数据而网易新闻恰好提供了一套不需要登录就能访问的公开接口。很多人一上来就写 scrapy 爬全站这是没必要的。舆情分析只需要“热点新闻”和“围绕热点产生的评论”所以采集目标其实很明确一份随时更新的新闻列表加上每篇新闻下的前几页热门评论。2.1 确定目标数据榜单页、详情页与评论接口的 URL 结构开始写代码之前先花十分钟手工确认三个 URL 长什么样。网易新闻的滚动新闻接口是一个 JSONP 地址返回的是用data_callback(...)包裹的 JSON 数组里面包含新闻标题、docid、发布时间等字段。docid 是一篇文章的唯一编号这个编号非常重要因为评论接口需要拿它拼 URL。详情页 URL 有两类格式一类是https://www.163.com/dy/article/{docid}.html另一类是https://news.163.com/{日期}/{docid}.html。不管哪一类评论接口只认 docid 本身所以从列表接口拿到 docid 后不需要再去详情页解析直接就能请求评论。评论接口的完整格式是https://comment.api.163.com/api/v1/products/a2869674571f77b5a0867c3d71db5856/threads/{docid}/comments/newList。注意a2869674571f77b5a0867c3d71db5856是网易新闻客户端的 product key这个值是固定的不同页面共用同一个 key。接口通过offset参数翻页每页默认返回 10 条评论limit可以适当调大但不要超过 30否则会被限流。2.2 用 requests 抓取新闻列表并解析出详情链接明确 URL 结构之后先用最小代码验证列表接口能否正常返回。下面是拉取新闻列表的核心代码也是整个采集流程的第一步。import requests import re import json import pandas as pd HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://news.163.com/ } def fetch_news_list(): url https://temp.163.com/special/00804KVA/cm_yaowen.js resp requests.get(url, headersHEADERS, timeout10) resp.encoding gbk text resp.text # JSONP 返回的是 data_callback([...])需要先提取括号内的 JSON 字符串 match re.search(rdata_callback\((.*)\), text, re.S) if not match: raise ValueError(列表接口返回格式异常可能是页面结构变了) data json.loads(match.group(1)) rows [] for item in data: rows.append({ title: item.get(title, ).strip(), docid: item.get(docid, ), url: item.get(url, ), time: item.get(time, ), comment_count: item.get(commentcount, 0) }) return pd.DataFrame(rows) if __name__ __main__: df fetch_news_list() print(df.head())这段代码里有一个关键处理resp.encoding gbk。网易的滚动新闻页面用的是 GBK 编码如果不指定requests 会根据响应头猜测编码大概率猜成 ISO-8859-1中文标题直接乱码。另外JSONP 不是标准 JSONjson.loads拿到整个响应文本会直接报错所以先用正则re.search(rdata_callback\((.*)\), text, re.S)把括号里的内容抠出来再交给json.loads解析。HEADERS里的User-Agent和Referer建议保留。网易对没有 UA 的请求会返回 403Referer 伪装成从新闻首页点进来的请求能降低被拦的概率。列表接口返回的time字段是 Unix 时间戳后面做时间序列分析时会用到这里先原样存下来。2.3 用评论接口拉取热门评论并分页落库列表能正常返回之后第二步是拉评论。评论接口的响应是标准 JSON里面有comments数组和total总数。注意comments里的每一条评论除了content正文外还有vote点赞数和replyCount回复数这些是后续判断“舆论热度”的重要维度。import time def fetch_comments(docid, pages3, delay0.5): base_url (https://comment.api.163.com/api/v1/products/ a2869674571f77b5a0867c3d71db5856/threads/ {}/comments/newList).format(docid) all_comments [] for offset in range(0, pages * 10, 10): params { ibc: newspc, limit: 10, showLevelThreshold: 72, headLimit: 1, tailLimit: 2, offset: str(offset) } try: resp requests.get(base_url, paramsparams, headersHEADERS, timeout10) data resp.json() except Exception as e: # 单页失败不中断整个抓取记录后跳过 print(offset{} 请求失败: {}.format(offset, e)) continue for cmt in data.get(comments, []): all_comments.append({ docid: docid, content: cmt.get(content, ), vote: cmt.get(vote, 0), reply_count: cmt.get(replyCount, 0), comment_time: cmt.get(createTime, ) }) time.sleep(delay) # 控制请求频率避免触发反爬 return all_commentsshowLevelThreshold这个参数是控制是否展示“神回复”楼层级别的按 72 传即可。headLimit和tailLimit是控制头部和尾部置顶评论数量的保持默认值就行。真正需要调整的是limit和offsetlimit决定每页条数建议 10 到 20offset是翻页游标从 0 开始每次加上limit的值。这里的time.sleep(delay)是血泪经验换来的。最初我写这个采集脚本时没有限速一口气请求 50 页评论结果第 20 页开始连续返回空的comments数组实际上是 IP 被临时限流了。后来把每页间隔调到 0.5 秒再也没出现过中途断流的情况。如果采集量特别大建议把delay加到 1 秒以上。3. 文本清洗与情感分析把评论变成可计算的情绪数值舆情分析的核心不是爬数据而是把非结构化的中文文本变成可计算的数值。网易新闻评论区里真正干净的中文句子其实不多有复制粘贴的刷屏内容有夹杂英文缩写和表情符号的短句还有“绝绝子”“集美”这类网络新词。如果直接拿原始文本去做词频统计和情感打分结果会非常离谱。3.1 清洗评论去重、去标签、去杂讯评论清洗是第一道工序主要做三件事去重、去 HTML 标签、去无意义字符。网易评论区经常出现同一条内容被反复刷屏的情况比如“前排围观”出现几十次如果不做去重词频统计会被这种无效文本带偏。import re def clean_comment(text): if not text or not isinstance(text, str): return # 去掉 HTML 标签评论里偶尔会出现 br 之类的换行标记 text re.sub(r[^], , text) # 只保留中文、英文、数字和常用标点其他字符统一替换为空格 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9。、\\《》【】], , text) # 合并连续空白字符 text re.sub(r\s, , text).strip() return text def deduplicate_comments(comments_df, keycontent): # 按内容去重保留点赞数最高的一条 df comments_df.sort_values(vote, ascendingFalse) df df.drop_duplicates(subset[key], keepfirst) return df清洗时特别注意不要把英文全部丢掉。像“GDP”“AI”“CEO”这类词在新闻评论里高频出现如果正则里只保留中文这些词会变成“”“”等于是把信息丢了。所以正则里保留了a-zA-Z0-9。另外去重时按vote排序后再drop_duplicates能保证重复内容里保留点赞数最高的那条也就是最受认同的那条表达。3.2 jieba 分词与自定义词典让“绝绝子”不再被切碎清洗完之后就是分词。python 生态里 jieba 是中文分词的事实标准用法简单但默认词典对网络新词很不友好。比如“绝绝子”会被切成“绝/绝/子”“集美”会被切成“集/美”这种切法直接毁掉后续的词频统计。import jieba # 自定义词典文件每行一个词格式词 词频 词性 USER_DICT userdict.txt def load_stopwords(pathstopwords.txt): with open(path, encodingutf-8) as f: return set(line.strip() for line in f if line.strip()) def tokenize(text, stopwords): words jieba.lcut(text) # 过滤停用词、单字词、纯数字 token result [] for w in words: w w.strip() if not w: continue if w in stopwords: continue if len(w) 1 and not w.isdigit(): continue if w.isdigit(): continue result.append(w) return result jieba.load_userdict(USER_DICT)userdict.txt是纯文本文件一行一个词可选附带词频和词性。我在实际项目中会手动把“绝绝子”“破防”“集美”“yyds”这类网络热词加进去每行写词就行词频不写 jieba 也能自动处理。stopwords.txt就是常见的停用词表包括“的”“了”“是”“在”“我”“你”这类无情绪含义的虚词。分词结果的过滤规则里有一条容易被忽略len(w) 1的单字要过滤因为中文里绝大多数单字没有独立的情感意义留着只会增加噪声。3.3 情感打分词典法 否定词处理情感分析是舆情平台里最容易“看起来在跑实际结果没法看”的环节。深度学习模型精度高但环境依赖重还要准备标注数据词典法虽然朴素但胜在可控、可解释、能快速上线。常见做法是维护一个情感词典把评论里正面词和负面词的得分累加同时处理否定词和程度副词。def load_sentiment_dict(pos_pathpos_words.txt, neg_pathneg_words.txt): pos_words set() neg_words set() with open(pos_path, encodingutf-8) as f: for line in f: word line.strip() if word: pos_words.add(word) with open(neg_path, encodingutf-8) as f: for line in f: word line.strip() if word: neg_words.add(word) return pos_words, neg_words POS_WORDS, NEG_WORDS load_sentiment_dict() NEGATORS {不, 没, 无, 非, 莫, 勿, 别, 没有, 不是, 不太} DEGREE_WORDS { 非常: 2.0, 特别: 2.0, 极其: 2.5, 太: 1.8, 很: 1.5, 有点: 0.7, 稍微: 0.6, 比较: 1.2, } def sentiment_score(text): words jieba.lcut(text) score 0.0 degree 1.0 negated False for i, w in enumerate(words): if w in DEGREE_WORDS: degree DEGREE_WORDS[w] elif w in NEGATORS: negated True elif w in POS_WORDS: base 1.0 * degree score -base if negated else base degree, negated 1.0, False elif w in NEG_WORDS: base 1.0 * degree score - -base if negated else base degree, negated 1.0, False return score这个打分函数的逻辑是遍历分词结果遇到程度副词先记录倍率遇到否定词做标记遇到情感词时先看前面有没有否定词有则取反。例如“不太好”的得分是-1 * 1.5 -1.5而不是简单地把“好”算成正面。词表文件准备是这里的隐形工作量pos_words.txt和neg_words.txt可以用公开的知网情感词典做种子再手动补充新闻评论里常见的“无语”“恶心”“气愤”“暖心”“感动”“给力”等词。注意词典法对反讽和隐喻几乎无解。“这操作真棒”里的“棒”会被算成正面但语境是讽刺。这个边界要心里有数后面第 5 章会专门讲怎么评估和补救。4. 热点识别与可视化从词频到趋势曲线的落地路径数据清洗和情感打分都做完之后就到了舆情分析的产出环节。这一章解决两个问题一是“现在大家都在聊什么”二是“这个话题的热度是在上升还是下降”。前者靠关键词提取后者靠时间序列聚合。4.1 用 TF-IDF 提取热点主题关键词提取热点关键词最简单的方式是直接用jieba.analyse.extract_tags它内部实现了 TF-IDF 算法。为什么不用单纯的词频统计因为词频高的往往是“新闻”“网易”“评论”这类在语料里普遍出现的词而 TF-IDF 会给那些只在某几篇文章里高频出现的词更高权重这才是真正的热点特征。import jieba.analyse def extract_hot_topics(news_df, topk30): 从新闻标题列表里提取热点关键词。 news_df 必须包含 title 列。 all_titles .join(news_df[title].tolist()) # allowPOS 限定词性名词、动词、简称是热点词的主要来源 keywords jieba.analyse.extract_tags( all_titles, topKtopk, withWeightTrue, allowPOS(ns, n, vn, v, nr, nt) ) return keywords hot_topics extract_hot_topics(news_df) for word, weight in hot_topics: print(word, round(weight, 4))allowPOS这个参数值得单独说明。ns是地名n是名词vn是动名词v是动词nr是人名nt是机构名。新闻标题里“美国”“央行”“华为”这类词才会被保留而“今天”“最新”“突发”这类词因为词性不符合会被过滤掉。withWeightTrue能拿到每个词的权重值后续画词云时可以用这个权重控制字号大小。4.2 按小时构建评论量热度曲线关键词只能告诉你是哪个话题不能告诉你是升是降。热度趋势需要用时间维度来度量做法是把新闻的发布时间和对应评论量放在一起按小时聚合得到一条“评论量随时间变化”的曲线。突发新闻的特征是短时间内评论量骤增明显高于前后时段这就是舆情热点。import pandas as pd def build_trend_curve(news_df, comments_df, freqH): 计算每个时刻的新闻发布量与评论总量。 freq: H按小时聚合D按天聚合 # 新闻发布时间戳转 datetime news_df news_df.copy() news_df[pub_time] pd.to_datetime(news_df[time], units) news_trend news_df.set_index(pub_time).resample(freq).size() # 评论时间字段如果是时间戳也做同样转换 comments_df comments_df.copy() if comment_time in comments_df.columns: comments_df[cmt_time] pd.to_datetime( comments_df[comment_time], units ) comment_trend comments_df.set_index(cmt_time).resample(freq).size() else: comment_trend pd.Series(dtypeint) return news_trend, comment_trend参数freq控制聚合粒度H是按小时D是按天。做实时舆情监测建议用小时粒度做周报月报用天粒度就够。注意pd.to_datetime(..., units)的前提是源数据里time是 Unix 秒级时间戳如果拿到的是毫秒级unit要改成ms这个细节容易踩坑。4.3 用 pyecharts 输出词云、折线图与情感占比数据算完之后必须落成图表否则分析结果没法汇报。pyecharts 是 python 数据分析与可视化里最适合中文场景的工具词云和折线图几行代码就能出成品而且输出 HTML 文件可以直接在浏览器里打开不需要额外部署。from pyecharts.charts import WordCloud, Line, Pie from pyecharts import options as opts def make_wordcloud(keywords, output_htmlwordcloud.html): # keywords 是 (词, 权重) 列表 wc WordCloud() wc.add(, keywords, word_size_range[20, 100], shapecircle) wc.set_global_opts(title_optsopts.TitleOpts(title热点关键词)) wc.render(output_html) def make_trend_line(news_trend, comment_trend, output_htmltrend.html): line Line() line.add_xaxis(news_trend.index.strftime(%m-%d %H).tolist()) line.add_yaxis(新闻发布量, news_trend.values.tolist(), is_smoothTrue) line.add_yaxis(评论量, comment_trend.values.tolist(), is_smoothTrue) line.set_global_opts( title_optsopts.TitleOpts(title新闻与评论热度趋势), tooltip_optsopts.TooltipOpts(triggeraxis), ) line.render(output_html) def make_sentiment_pie(scores, output_htmlsentiment.html): pos sum(1 for s in scores if s 0) neg sum(1 for s in scores if s 0) neu len(scores) - pos - neg pie Pie() pie.add(情感分布, [(正面, pos), (负面, neg), (中性, neu)]) pie.set_global_opts(title_optsopts.TitleOpts(title评论情感占比)) pie.render(output_html)三个函数对应三种决策场景词云用来快速看清议题分布趋势线用来判断舆情升温还是降温情感饼图用来看舆论是支持还是反对。word_size_range[20, 100]控制词云字号范围权重越大的词字号越大。折线图里triggeraxis可以让鼠标悬停时同时显示新闻量和评论量两个数值对比起来更方便。5. 避坑手册舆情分析平台从采集到可视化的 5 个高频翻车点这个平台我前后搭过不下三遍每一遍都在不同的环节翻过车。下面这 5 个问题是出现频率最高、也最容易被忽视的每一条都是“现象 - 原因 - 解决”的真实记录。翻车点 1列表接口返回的 JSONP 解析直接报错现象requests.get拿到的响应文本是data_callback([{...}])直接调json.loads(resp.text)抛出JSONDecodeError。原因网易这个接口是 JSONP 协议服务端把真正的 JSON 包在了一个 JavaScript 函数调用里浏览器靠这个 callback 实现跨域但 python 的json库不认识这种格式。解决先re.search(rdata_callback\((.*)\), text, re.S)提取括号内的内容再json.loads。注意正则里的re.S标志必须加因为返回内容可能跨行。另外接口里的callback参数值不是固定的有些接口叫callback有些叫data_callback写代码前先看一眼原始响应。翻车点 2评论接口的 docid 用错导致永远返回空列表现象详情页能正常打开但评论接口请求返回的comments数组永远是空的total也是 0。原因网易评论系统认的 docid 是纯字符串 ID而详情页 URL 里的 docid 可能带有.html后缀或者你直接拿页面 URL 去拼了评论接口。我犯过的具体错误是从https://www.163.com/dy/article/ABCD1234.html里取 ID 时用了url.split(/)[-1]结果拿到的是ABCD1234.html直接拼进评论接口当然查不到数据。解决从列表接口的docid字段取值它是干净的纯 ID。如果必须从详情页 URL 解析做一次os.path.splitext把后缀剥掉。翻车点 3裸 requests 请求被网易反爬临时限流现象脚本运行前 2 分钟正常之后所有请求返回状态码 403或者 HTML 变成一个验证页。原因网易的反爬策略不是一刀切而是对频繁请求的 IP 做临时限流。只设置User-Agent不够Referer也不能省。另一个常见诱因是请求频率太高没有设置time.sleep。解决HEADERS 里同时带上User-Agent、Referer、Accept-Language三个字段评论分页请求之间至少间隔 0.5 秒再加一个简单的失败重试逻辑遇到 403 就退避 30 秒再试。翻车点 4jieba 分词把网络热词切成碎片词云失去可读性现象词云里出现大量“绝”“子”“破”“防”这种单字热点关键词完全看不出含义。原因jieba 的默认词典基于新闻语料训练对近两年流行的网络缩写和饭圈用语覆盖不足。“破防”这个标准词组在默认词典里会被切成“破/防”。解决建一个userdict.txt把“破防”“绝绝子”“集美”“yyds”“无语子”这些词按一行一个加进去然后jieba.load_userdict(User_DICT)。要特别注意修改自定义词典后必须重启 python 进程load_userdict在同一个进程里重复调用没问题但如果用 Jupyter Notebook改完词典文件要重新加载内核。翻车点 5情感分析把否定句判成相反情绪现象一条“这质量也太差了吧”被情感模型判成中性甚至正面“差”被识别成负面词的逻辑生效了但“太”这个程度副词放大了得分方向还是对的。真正的翻车案例是“不建议大家买”分词结果是“不/建议/大家/买”情感词典里“建议”没有权重“买”也没有权重整条评论得分 0但人工读一下就知道这是负面。原因词典法只对有明确情感倾向的实词打分对“建议”“认为”“觉得”这类不直接带情绪的动词无感导致否定结构“不 中性动词 实体动作”无法被捕捉。解决在情感打分前先做一个规则预处理把“不建议”“不推荐”“不要买”“千万别”这类组合命中的短语直接映射为强负面权重。这个映射可以直接放在NEGATORS处理之前用字符串匹配if cur_word in {建议, 推荐, 买} and prev_word in NEGATORS的方式处置。提示以上 5 个坑不是一次性解决的。我通常每修完一个坑就补一条注释到代码里下次复现时能省掉大量排查时间。6. 进阶定时增量采集 用人工标注验证情感准确率平台能跑通只是第一步真正要投入日常使用还差两件事一是让它每天自动跑而不是手动执行二是验证情感打分的准确率到底有多少否则辛辛苦苦算出来的舆情报告没人敢信。定时采集可以交给系统自带的 cron 或者 python 的schedule库。我最常用的做法是写一个run.py统一调度先增量拉新闻列表再对新增的 docid 拉评论最后重新生成图表。增量逻辑很简单每次采集前把现有的 docid 集合加载进来列表接口返回的 docid 如果已经存在就跳过。这样脚本每分钟跑一次也不会造成重复采集对上游接口的压力也小。import schedule import time def job(): data_dir data os.makedirs(data_dir, exist_okTrue) seen load_seen_docids(os.path.join(data_dir, docids.txt)) news_df fetch_news_list() new_items news_df[~news_df[docid].isin(seen)] all_comments [] for docid in new_items[docid]: comments fetch_comments(docid, pages2) all_comments.extend(comments) time.sleep(0.5) if new_items: new_items.to_csv(os.path.join(data_dir, news.csv), modea, headernot os.path.exists(data/news.csv), indexFalse) if all_comments: pd.DataFrame(all_comments).to_csv( os.path.join(data_dir, comments.csv), modea, headernot os.path.exists(data/comments.csv), indexFalse ) refresh_report() schedule.every(30).minutes.do(job) while True: schedule.run_pending() time.sleep(60)情感准确率验证是很多人跳过的一步但恰恰是最该做的一步。常见做法是随机抽 100 条评论手工标注成正面/负面/中性再和算法结果对比算整体准确率和混淆矩阵。我做完第一版情感分析后抽了 50 条评论验证准确率只有 62%其中一半的错误来自“反讽”和“网络梗”。把“绝绝子建立”这类梗词加入词表、再处理了否定结构后准确率提升到了 78%。这个数字不算高但对舆情预警场景已经够用——我只需要知道舆论方向偏正还是偏负不需要精确到每条评论。我个人的习惯是每次改完情感词典就跑一次同样的 100 条验证集把准确率变化记在代码目录下的accuracy_log.txt里。这个习惯帮我避免了很多次“改了词表以为自己变好了、实际上变差了”的尴尬。最后如果你也在搭类似的舆情分析平台不要纠结于模型是否高大上先把数据链路跑通再把准确率跑出数字这个平台才能真正拿去支撑决策。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价