简介:面向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, headers=HEADERS, timeout=10) resp.encoding = "gbk" text = resp.text # JSONP 返回的是 data_callback([...]),需要先提取括号内的 JSON 字符串 match = re.search(r"data_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 不是标准 JSON,json.loads拿到整个响应文本会直接报错,所以先用正则re.search(r"data_callback\((.*)\)", text, re.S)把括号里的内容抠出来,再交给json.loads解析。
HEADERS里的User-Agent和Referer建议保留。网易对没有 UA 的请求会返回 403,Referer 伪装成从新闻首页点进来的请求,能降低被拦的概率。列表接口返回的time字段是 Unix 时间戳,后面做时间序列分析时会用到,这里先原样存下来。
2.3 用评论接口拉取热门评论并分页落库
列表能正常返回之后,第二步是拉评论。评论接口的响应是标准 JSON,里面有comments数组和total总数。注意comments里的每一条评论,除了content正文外,还有vote(点赞数)和replyCount(回复数),这些是后续判断“舆论热度”的重要维度。
import time def fetch_comments(docid, pages=3, delay=0.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, params=params, headers=HEADERS, timeout=10) 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和offset:limit决定每页条数,建议 10 到 20;offset是翻页游标,从 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, key="content"): # 按内容去重,保留点赞数最高的一条 df = comments_df.sort_values("vote", ascending=False) df = df.drop_duplicates(subset=[key], keep="first") 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(path="stopwords.txt"): with open(path, encoding="utf-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_path="pos_words.txt", neg_path="neg_words.txt"): pos_words = set() neg_words = set() with open(pos_path, encoding="utf-8") as f: for line in f: word = line.strip() if word: pos_words.add(word) with open(neg_path, encoding="utf-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, topk=30): """ 从新闻标题列表里提取热点关键词。 news_df 必须包含 title 列。 """ all_titles = " ".join(news_df["title"].tolist()) # allowPOS 限定词性,名词、动词、简称是热点词的主要来源 keywords = jieba.analyse.extract_tags( all_titles, topK=topk, withWeight=True, 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是机构名。新闻标题里“美国”“央行”“华为”这类词才会被保留,而“今天”“最新”“突发”这类词因为词性不符合会被过滤掉。withWeight=True能拿到每个词的权重值,后续画词云时可以用这个权重控制字号大小。
4.2 按小时构建评论量热度曲线
关键词只能告诉你是哪个话题,不能告诉你是升是降。热度趋势需要用时间维度来度量,做法是:把新闻的发布时间和对应评论量放在一起,按小时聚合,得到一条“评论量随时间变化”的曲线。突发新闻的特征是短时间内评论量骤增,明显高于前后时段,这就是舆情热点。
import pandas as pd def build_trend_curve(news_df, comments_df, freq="H"): """ 计算每个时刻的新闻发布量与评论总量。 freq: H=按小时聚合,D=按天聚合 """ # 新闻发布时间戳转 datetime news_df = news_df.copy() news_df["pub_time"] = pd.to_datetime(news_df["time"], unit="s") 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"], unit="s" ) comment_trend = comments_df.set_index("cmt_time").resample(freq).size() else: comment_trend = pd.Series(dtype=int) return news_trend, comment_trend参数freq控制聚合粒度:H是按小时,D是按天。做实时舆情监测建议用小时粒度,做周报月报用天粒度就够。注意pd.to_datetime(..., unit="s")的前提是源数据里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_html="wordcloud.html"): # keywords 是 (词, 权重) 列表 wc = WordCloud() wc.add("", keywords, word_size_range=[20, 100], shape="circle") wc.set_global_opts(title_opts=opts.TitleOpts(title="热点关键词")) wc.render(output_html) def make_trend_line(news_trend, comment_trend, output_html="trend.html"): line = Line() line.add_xaxis(news_trend.index.strftime("%m-%d %H").tolist()) line.add_yaxis("新闻发布量", news_trend.values.tolist(), is_smooth=True) line.add_yaxis("评论量", comment_trend.values.tolist(), is_smooth=True) line.set_global_opts( title_opts=opts.TitleOpts(title="新闻与评论热度趋势"), tooltip_opts=opts.TooltipOpts(trigger="axis"), ) line.render(output_html) def make_sentiment_pie(scores, output_html="sentiment.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_opts=opts.TitleOpts(title="评论情感占比")) pie.render(output_html)三个函数对应三种决策场景:词云用来快速看清议题分布,趋势线用来判断舆情升温还是降温,情感饼图用来看舆论是支持还是反对。word_size_range=[20, 100]控制词云字号范围,权重越大的词字号越大。折线图里trigger="axis"可以让鼠标悬停时同时显示新闻量和评论量两个数值,对比起来更方便。
5. 避坑手册:舆情分析平台从采集到可视化的 5 个高频翻车点
这个平台我前后搭过不下三遍,每一遍都在不同的环节翻过车。下面这 5 个问题是出现频率最高、也最容易被忽视的,每一条都是“现象 -> 原因 -> 解决”的真实记录。
翻车点 1:列表接口返回的 JSONP 解析直接报错
现象:requests.get拿到的响应文本是data_callback([{...}]),直接调json.loads(resp.text)抛出JSONDecodeError。
原因:网易这个接口是 JSONP 协议,服务端把真正的 JSON 包在了一个 JavaScript 函数调用里,浏览器靠这个 callback 实现跨域,但 python 的json库不认识这种格式。
解决:先re.search(r"data_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 秒再试。
翻车点 4:jieba 分词把网络热词切成碎片,词云失去可读性
现象:词云里出现大量“绝”“子”“破”“防”这种单字,热点关键词完全看不出含义。
原因: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_ok=True) 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, pages=2) all_comments.extend(comments) time.sleep(0.5) if new_items: new_items.to_csv(os.path.join(data_dir, "news.csv"), mode="a", header=not os.path.exists("data/news.csv"), index=False) if all_comments: pd.DataFrame(all_comments).to_csv( os.path.join(data_dir, "comments.csv"), mode="a", header=not os.path.exists("data/comments.csv"), index=False ) refresh_report() schedule.every(30).minutes.do(job) while True: schedule.run_pending() time.sleep(60)情感准确率验证是很多人跳过的一步,但恰恰是最该做的一步。常见做法是:随机抽 100 条评论,手工标注成正面/负面/中性,再和算法结果对比,算整体准确率和混淆矩阵。我做完第一版情感分析后,抽了 50 条评论验证,准确率只有 62%,其中一半的错误来自“反讽”和“网络梗”。把“绝绝子建立”这类梗词加入词表、再处理了否定结构后,准确率提升到了 78%。这个数字不算高,但对舆情预警场景已经够用——我只需要知道舆论方向偏正还是偏负,不需要精确到每条评论。
我个人的习惯是每次改完情感词典,就跑一次同样的 100 条验证集,把准确率变化记在代码目录下的accuracy_log.txt里。这个习惯帮我避免了很多次“改了词表以为自己变好了、实际上变差了”的尴尬。最后,如果你也在搭类似的舆情分析平台,不要纠结于模型是否高大上,先把数据链路跑通,再把准确率跑出数字,这个平台才能真正拿去支撑决策。希望帮到你。
本文还有配套的精品资源,点击获取