Python舆情分析平台实战:从网易新闻评论采集到热点洞察 简介这是一份面向Python学习者、毕业设计及课程设计者的舆情热点分析平台完整项目包以网易新闻及用户评论为研究对象覆盖数据采集、清洗、文本挖掘、情感判别、热点关键词提取、舆情走势分析到可视化展示的完整链路。项目代码结构清晰包含前端页面文件、Python业务逻辑、数据库脚本及示例数据可以帮助读者快速搭起可运行的系统并理解requests、BeautifulSoup、jieba、SnowNLP等库在实际项目中的组合方式。压缩包共1403个文件约23.83MB主要类型包括用于浏览器端交互与展示的js/css/html文件、承担爬虫与算法逻辑的py源码及pyc编译文件另有图片、字体、配置、文档和数据文件等基本覆盖Web端舆情平台所需的各类素材。前端静态部分整合了常用UI框架与可视化组件便于直接复用或二次调整界面风格。已有166人学习下载适合作为课程设计或毕业设计的参考模板也适合希望系统练习Python全栈数据项目的开发者对照学习。1. 网易新闻舆情热点分析平台它解决的痛点不是爬虫而是“看完评论之后怎么办”做舆情监测的人大多有一个共同的痛苦接到一个任务要盯着网易新闻上某个公共事件的热评走向判断舆情是升温还是降温正面声音多还是负面声音多。手工复制粘贴评论、一条条数情绪每天两三个小时就耗在机械操作里还容易漏掉凌晨爆发的那一波评论。做技术的人则痛苦在另一个方向以为写个爬虫把评论抓下来就完事了结果面对几万条文本不知道怎么变成“舆情结论”最后只交出一张Excel表完全没有分析价值。python083基于网易新闻评论的舆情热点分析平台本质上就是把“采集—存储—计算—展示”这四段串起来的小型数据管道定时抓取网易新闻的正文和评论计算热度指数、情感倾向和热词分布再用可视化界面把结果摆出来。这套方案适合两类人一是刚入门python爬虫、想找一个完整落地点的新手二是真想在企业里建舆情监测能力、但不想上来就上大厂数仓的工程师。它不需要多贵的机器一台普通笔记本就能跑技术栈也只有python、requests、SQLite和Flask这几样和免费python源码大全里那些只能跑demo的教学项目最大的区别是——它能长期挂在服务器上自动运行输出的是可以直接汇报的结果。先把这个平台的骨架拆开后面各章讲每一步怎么做。2. 数据管道先跑通抓取网易新闻列表与评论的接口规律和最小实现舆情分析的前提是拿到干净、连续、可追溯的数据。网易新闻的数据源分两层第一层是新闻列表页拿到文章标题、发布时间和唯一ID第二层是评论接口用第一层拿到的ID去换评论内容和用户信息。很多python爬虫初学者在这里犯同一个错误一上来就找现成的爬虫框架却不知道目标站点的接口规律最后要么被反爬拦住要么抓到的数据缺字段、对不上号。其实做舆情平台数据管道不需要写得复杂但必须把接口的节奏和分页机制吃透。2.1 从新闻目录页拿到docid正则抽取与去重网易新闻的PC端目录页和文章页URL格式比较固定文章页的链接里通常带一长串字母数字混合的ID这就是评论接口要用的docid。常见做法是不走浏览器渲染直接用requests拉HTML源码再用正则把docid和标题一起抽出来。注意目录页是增量更新的同一个新闻会出现在多个分类列表里所以抽完必须做去重不然后续抓评论会重复计算热度。import re import requests from urllib.parse import urljoin headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } def fetch_news_list(category_url, seen_ids): resp requests.get(category_url, headersheaders, timeout10) resp.encoding utf-8 html resp.text # 抽取类似 /dr/article/XXXX.html 的链接XXXX是docid pattern re.compile(rhref(/dr/article/([0-9a-zA-Z])\.html)[^]*(.*?)/a, re.S) items [] for full_url, docid, title in pattern.findall(html): title re.sub(r[^], , title).strip() if not title or docid in seen_ids: continue seen_ids.add(docid) items.append({docid: docid, title: title, url: urljoin(https://www.163.com, full_url)}) return items if __name__ __main__: seen set() news fetch_news_list(https://www.163.com/news/, seen) print(f新增新闻 {len(news)} 条共缓存 {len(seen)} 条)这段代码的核心逻辑是先定义浏览器UA头再请求目录页。正则里用了两个捕获组第一个捕获完整链接第二个捕获docid第三个捕获标题。因为目录页的标题里偶尔会夹带HTML标签所以加一步去标签。seen_ids这个集合是本地的去重记忆实际工程里应该换成数据库里查docid是否已存在否则程序重启后缓存就丢了。超时时间设为10秒是防止某个目录页卡住拖垮整个任务。参数说明timeout10对国内站点的响应时间来说是合理的舆情采集多是凌晨跑批网络波动大设成30秒反而会让失败任务挂太久。UA头不要用默认的python-requests网易对裸UA的请求识别度很高。如果你在pycharm配置python环境调试这段代码会发现requests库没装的话会直接报ModuleNotFoundError记得先跑pip install requests。2.2 评论接口的分页游标与字段映射网易新闻的评论接口是独立的不以登录态为必选条件这给舆情采集开了个口子。接口路径一般是/comment/api/v1/products/xxx/threads/{docid}/comments/newList返回的是JSON里面有commentList数组。关键参数有limit和offsetlimit控制每页条数offset是偏移量。但这里有一个新手经常踩的坑直接翻offset翻到几百页后接口会开始返回重复数据。原因后面避坑章节细说先给出稳定策略——评论量在一万以内的新闻一次性拉完再局部去重即可。import requests import json import time COMMENT_API https://comment.api.163.com/api/v1/products/a2869674571f77b5a0867c3d71db5856/threads/{docid}/comments/newList def fetch_comments(docid, max_pages200): all_comments [] offset 0 limit 50 for page in range(max_pages): params { docid: docid, offset: offset, limit: limit, headPic: 1, showLevel: 1, } try: resp requests.get(COMMENT_API.format(dociddocid), paramsparams, headers{ User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15, Referer: fhttps://www.163.com/dy/article/{docid}.html }, timeout10) data resp.json() except Exception as e: print(f第{page}页请求失败: {e}) break comment_list data.get(commentList, []) if not comment_list: break # 没数据了退出分页 all_comments.extend(comment_list) offset limit time.sleep(0.5) # 控制请求节奏别逼太紧 # 安全阀防止死循环 if len(comment_list) limit: break return all_comments if __name__ __main__: rs fetch_comments(C6Q1K9E70526CTFQ) print(f拉到 {len(rs)} 条评论样例字段: {list(rs[0].keys()) if rs else 无})这段代码在评论接口上加了两个细节一是Referer带上文章页地址模拟从文章页跳转过去的正常请求二是把User-Agent换成了iPhone版移动端的反爬力度通常比PC端宽松。time.sleep(0.5)是给接口留出间隔舆情采集不是攻击没必要一秒钟打几十个请求。max_pages200是安全上限单篇新闻评论过万条时200页×50条足够覆盖还不够说明这新闻已经属于超级热点建议直接改走全量导出别继续翻页。字段映射上评论正文在content字段用户昵称在user.nickname点赞数在voteCount评论时间在createTime毫秒级时间戳。楼层归属在parentInfo里顶层评论该字段为空。这些字段在后续分析里都要用抓完最好原样落库不要只存正文。2.3 落库结构为什么我坚持用SQLite中转很多教程喜欢一上来就上MySQL但个人做舆情平台真没必要。网易新闻评论的数据量级一天全量抓下来也就几百MBSQLite单文件就能扛住省去装数据库服务的时间也方便备份迁移。我用三张表news存新闻元数据comments存评论daily_stats存每天算好的聚合指标。这样设计是为了后面重算指标时不用重新爬数据本地有原始数据就是后悔药。CREATE TABLE IF NOT EXISTS news ( docid TEXT PRIMARY KEY, title TEXT, url TEXT, category TEXT, published_at INTEGER, first_seen_at INTEGER ); CREATE TABLE IF NOT EXISTS comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, docid TEXT NOT NULL, content TEXT, nickname TEXT, vote_count INTEGER DEFAULT 0, create_time INTEGER, UNIQUE(docid, content) -- 同一篇新闻下内容相同视为重复 ); CREATE TABLE IF NOT EXISTS daily_stats ( stat_date TEXT NOT NULL, docid TEXT NOT NULL, comment_count INTEGER, hot_index REAL, sentiment_score REAL, top_keywords TEXT, PRIMARY KEY(stat_date, docid) );UNIQUE(docid, content)这个约束很关键。同一用户复制粘贴相同内容会刷屏但舆情分析不应把重复文本算成多条独立观点所以内容去重直接在数据库层面卡死。create_time是毫秒时间戳计算“每小时评论增量”时要用它来分组。daily_stats是预聚合表舆情报告直接读它不至于每次打开页面都重算几万条评论。3. 热点分析的核心计算热度、情感与热词三件套数据抓到库里只是完成了脏活累活舆情分析平台真正的价值体现在把原始评论变成结论。我习惯把结论拆成三个维度热度指数这个事有多大、情感倾向舆论是正面还是负面、热词分布大家在讨论什么。这三个维度在实现时各有各的计算策略也有各自的参数陷阱。3.1 热度指数评论流量的时间衰减加权热度计算最容易犯的错是把评论总量当热度。一个三天前的老热点评论量可能仍然很高但实际舆论关注度已经衰减了。所以热度指数一定要引入时间衰减因子。我的做法是按小时聚合评论数再乘一个随新闻年龄增长的衰减系数。衰减系数用指数函数模拟参数lambda控制衰减速度。import math import sqlite3 from datetime import datetime, timedelta def calc_hot_index(conn, docid, decay_hours4.0, half_life_hours24.0): decay_hours: 近期窗口内不做衰减的小时数防止刚爆出来的新闻被过度抑制 half_life_hours: 热度衰减到一半所需的小时数 now datetime.now() hours_ago_expr f SELECT (CAST(:now_ms AS INTEGER) - create_time) / 3600000.0 AS age_hours, COUNT(*) AS cnt FROM comments WHERE docid :docid GROUP BY age_hours rows conn.execute(hours_ago_expr, {now_ms: int(now.timestamp() * 1000), docid: docid}).fetchall() hot 0.0 for age_hours, cnt in rows: if age_hours decay_hours: weight 1.0 else: # 指数衰减超过窗口后每小时衰减 weight math.exp(-math.log(2) * (age_hours - decay_hours) / half_life_hours) hot cnt * weight return round(hot, 2) if __name__ __main__: conn sqlite3.connect(news.db) print(calc_hot_index(conn, C6Q1K9E70526CTFQ))这里math.log(2)是半衰期公式的常数不是魔法数字。half_life_hours24表示一条评论的影响力每24小时衰减一半decay_hours4是新热点的保护期——刚发布的新闻4小时内评论按全额计权避免一条突发新闻在第一个小时就被衰减系数压得热度不如旧闻。如果你发现某类新闻总是热度偏高或偏低优先调这两个参数。热点爆发快的娱乐新闻可以把decay_hours调到2时政类新闻可以调到6因为时政讨论的持续性更强。3.2 情感判别词典打分和否定词反转情感分析是实现门槛最低但效果争议最大的模块。大规模预训练模型效果固然好但搭建平台时要考虑部署成本和推理速度。常见做法是先用基于词典的评分器兜底跑通流程后再决定是否替换成深度学习模型。词典方案的核心是一个带情感分值的中文词表把正向词记为正分、负向词记为负分累加后得到整句的情感分。难点不在词典本身而在否定词和程度副词的处理。import re # 简易词典生产环境可替换为知网情感词典或BosonNLP词典 POSITIVE_WORDS {给力: 2, 点赞: 2, 支持: 1.5, 欣慰: 1.5, 公道: 1} NEGATIVE_WORDS {失望: -2, 愤怒: -2.5, 担心: -1.5, 寒心: -2, 离谱: -2} NEGATIONS {不, 没, 无, 非, 莫} INTENSIFIERS {很: 1.5, 太: 1.8, 特别: 1.8, 极其: 2.0} def sentiment_score(text): score 0.0 tokens re.findall(r[\u4e00-\u9fa5], text) combined .join(tokens) # 先按标点拆短句避免长句里情感互相抵消 clauses re.split(r[。], combined) for clause in clauses: clause_score 0.0 negate False intensity 1.0 # 分词后按词扫描 for word in re.findall(r[\u4e00-\u9fa5]{2,4}, clause): if word in NEGATIONS: negate not negate elif word in INTENSIFIERS: intensity * INTENSIFIERS[word] elif word in POSITIVE_WORDS: clause_score POSITIVE_WORDS[word] * intensity * ( -1 if negate else 1) elif word in NEGATIVE_WORDS: clause_score NEGATIVE_WORDS[word] * intensity * ( -1 if negate else 1) # 重置否定状态一个短句内否定词只在第一次出现时生效 if word not in NEGATIONS: negate False score clause_score return score if __name__ __main__: for s in [太失望了, 不能支持这种做法, 点赞很欣慰, 离谱到家了]: print(s, sentiment_score(s))这个实现有两个关键设计。第一是把长句按标点拆成短句再分别打分避免“事件本身让人愤怒但处理结果还算公道”这种转折句被算成零分。第二是否定词反转后立刻重置因为中文口语里“不是不支持”这种双重否定按一次反转处理误差更小。INTENSIFIERS里的程度副词会放大或缩小情感分数但要注意放大倍数设得过高会让“有点失望”和“极其失望”拉不开差距。这里1.8是经验值你可以用几组人工标注的句子调一调。词典法的天花板很明显——遇到“这也太6了”这种反讽词典完全失效。所以生产环境建议在词典基础上叠加一个轻量分类器比如朴素贝叶斯把词典打分作为特征之一准确率会明显提升。python数据分析与可视化生态里sklearn库可以直接干这个活训练数据就用你已经人工标注过的历史评论。3.3 热词提取jieba分词的停用词与TF-IDF权重热词提取是舆情报告里最直观的部分但很多人直接把jieba分词结果往界面上一丢结果满屏都是“我们”“你们”“今天”这些无意义词。做热词必须先过滤停用词再用TF-IDF而不是纯词频。纯词频会让“新闻”“记者”这种通用词永远霸榜TF-IDF能让“塌房”“通报”“沸”这类有信息量的词浮出来。import jieba import jieba.analyse def extract_top_keywords(docid, conn, top_k10): 用TF-IDF算法提取top关键词。 jieba.analyse默认以整段文本为语料这里把同篇新闻所有评论拼成大文本。 rows conn.execute( SELECT content FROM comments WHERE docid ?, (docid,) ).fetchall() if not rows: return [] corpus \n.join(r[0] for r in rows if r[0]) # 权重归一化默认基于词频 tags jieba.analyse.extract_tags(corpus, topKtop_k, withWeightTrue) return [(word, round(weight, 4)) for word, weight in tags] if __name__ __main__: conn sqlite3.connect(news.db) for word, w in extract_top_keywords(C6Q1K9E70526CTFQ, conn): print(word, w)extract_tags默认用TF-IDF算法它会自动去掉词频过高但区分度低的词前提是传入的文本足够长。单篇新闻的所有评论拼起来通常几千字语料偏短时TF-IDF效果不稳定这时候可以把多个新闻的评论拼成一个大语料再统一计算。别忘了先加自定义词典jieba.add_word(塌房)能保证这类网络新词不被拆成“塌”和“房”两个字。停用词表我直接用了jieba自带的实际使用建议叠加哈工大停用词表去掉“什么”“怎么”“一个”这类词。热词展示层面python爬虫可视化界面一般用词云但词云的可读性经常被诟病——词大小非线性、形状影响判断。更推荐用横向条形图。pyecharts的Bar组件就能实现输出HTML页面方便直接嵌入到Flask后端。4. 工程化落地定时调度、增量重跑与可视化输出数据管道和分析脚本都跑通了但一个舆情平台要在服务器上7×24小时运转还得解决三个问题怎么定时自动采集、失败任务怎么重跑、非技术同事怎么看结果。4.1 APScheduler定时任务为什么不用系统crontab很多python数据分析与可视化项目的定时调度直接挂crontab但crontab有两个问题任务执行状态没有日志记录失败后不会自动重试。APScheduler是python生态里的调度库支持内存和持久化两种存储最关键的是它能在任务进程里捕获异常配合装饰器做重试。from apscheduler.schedulers.blocking import BlockingScheduler from apscheduler.triggers.cron import CronTrigger import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(message)s) def job_collect_news(): try: from collector import fetch_news_list, save_to_db news fetch_news_list(https://www.163.com/news/, set()) save_to_db(news) logging.info(f采集新闻完成共 {len(news)} 条) except Exception as e: logging.error(f采集新闻失败: {e}) # 可在此处接入企业微信/钉钉机器人告警 if __name__ __main__: scheduler BlockingScheduler() # 每10分钟扫一次新闻列表 scheduler.add_job( job_collect_news, triggerCronTrigger(minute*/10), idcollect_news, misfire_grace_time3600, coalesceTrue ) scheduler.start()misfire_grace_time3600是误火容错时间。如果服务器在任务该执行时正处于睡眠状态恢复后3600秒内仍会补跑本次任务超过就跳过。coalesceTrue是把错过的多次任务合并成一次执行避免大量积压任务同时跑把数据库打爆。调度器选BlockingScheduler是因为舆情平台通常只跑这一个后台进程不需要和web服务共用一个进程。如果你有Flask服务同时跑可以用BackgroundScheduler放进Flask的启动事件里。4.2 增量重跑一天的数据重算多遍而不做重复功舆情分析有个特点同一批评论不同时间看得到的结论不同。凌晨爆发的新闻早上看热度和下午看热度完全不同。所以平台不能只算一次要支持按天重跑。增量重跑的核心是给daily_stats表加主键冲突时更新数据的逻辑def rebuild_daily_stats(conn, stat_date): 重算某天的全部统计指标写入daily_stats表。 支持重复调用已存在的记录直接覆盖。 comments_rows conn.execute( SELECT docid, content, create_time FROM comments WHERE create_time BETWEEN ? AND ? , (f{stat_date} 00:00:00, f{stat_date} 23:59:59)).fetchall() # 按 docid 分组 by_news {} for docid, content, create_time in comments_rows: by_news.setdefault(docid, []).append(content) for docid, contents in by_news.items(): hot calc_hot_index(conn, docid) score sum(sentiment_score(c) for c in contents) / len(contents) keywords extract_top_keywords(docid, conn, top_k8) conn.execute( INSERT INTO daily_stats (stat_date, docid, comment_count, hot_index, sentiment_score, top_keywords) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(stat_date, docid) DO UPDATE SET hot_indexexcluded.hot_index, sentiment_scoreexcluded.sentiment_score, top_keywordsexcluded.top_keywords , (stat_date, docid, len(contents), hot, score, json.dumps(keywords, ensure_asciiFalse))) conn.commit()ON CONFLICT DO UPDATE这段SQL是重跑的关键语法。先按日期范围查评论再在python侧分组聚合最后整批写回。注意top_keywords在库里存的是JSON字符串读取时json.loads还原成列表。这个函数可以放心地一天调用多次第一次跑全量之后跑增量不会产生重复统计。用Flask写一个/api/rebuild?date2025-01-10的接口手动触发也很方便。4.3 可视化选型pyecharts还是ECharts原生可视化是舆情平台的脸面。python数据分析与可视化方向里pyecharts是最省事的方案它把ECharts的JS封装成了python调用后端渲染HTML几行代码就能出一个漂亮的趋势图。但pyecharts生成的HTML是静态的如果更新图表后不重新生成文件页面上看不到最新数据。所以我一般让Flask动态渲染模板把pyecharts生成的图表HTML片段用render_embed()嵌入模板。注意图表数据更新时前端要整体刷新不要试图做局部更新否则容易因为JS变量覆盖出问题。5. 舆情分析平台踩坑记录五个高频翻车点和排查路径这个平台我前后搭过三轮每一轮都在不同的地方翻过车。下面这五个坑是接手这个方向最容易踩到的按“现象→原因→解决”拆开能帮你省下至少一个通宵。5.1 评论翻页翻到第30页开始重复总数永远差一截现象用offset翻页抓评论抓完去重后发现总数比页面显示的数字少了几百条而且后半段抓到的评论和前半段重复。原因网易评论接口会把“热门评论”和“最新评论”混合在一个接口里返回但两部分的排序逻辑不同——热门按点赞数排序、最新按时间排序。offset只是按当前排序方式向后取但排序结果会随着新评论加入而动态变化于是翻到后面就会出现同一批评论换了个位置又被取出来。解决把单篇新闻的评论当成一个快照来抓别按页翻太多次。超过5000条评论的新闻先按点赞数倒序抓前2000条这部分是舆论场的头部声音再按时间正序抓最新的2000条这部分是当前正在发酵的情绪两个集合合并去重基本能覆盖分析口径。5.2 连续采集半小时后接口开始返回503网页却打得开现象爬虫前半小时一切正常随后请求开始大量超时返回503状态码手动打开浏览器访问网易新闻完全没有问题。原因网易对接口维度的频率限制比页面维度严格得多。浏览器访问新闻页面和用户翻评论的节奏是几秒一条爬虫翻页却是每秒两三页接口侧的风控直接触发。解决把评论采集的间隔从0.2秒改成0.81.2秒随机值并保留同一个会话的cookie。代码里requests.session()代替裸requests.get()让连接复用减少重复握手带来的风控信号。遇到503后不要立刻加大并发先sleep 60秒再重试连续三次失败就记录失败任务等下一轮调度自动补采。5.3 情感分析把“太失望了”判成正面现象抽取一批评论人工核对发现否定类情感被明显高估很多“太失望了”“真不行”被判成正面或中性。原因词典里“失望”是负分但“太”这个程度副词的权重乘到了负分上应该让分数更负。问题出在代码把INTENSIFIERS的处理放在了否定反转之后——“太失望了”中“太”先翻倍成-4再被后续逻辑归一化回了-2。根因是词典匹配到“失望”后程度副词的作用域没有正确处理。解决把程度副词的加权作用限定在它所修饰的情感词之前而不是整句累加。具体做法是扫描到情感词时回看它前一个词是否是程度副词是则只对该词加权。另外对“没”“不”的否定反转只生效一次避免“不是不失望”被算成双重否定变正分。5.4 凌晨发布的新闻舆情统计归属错了日期现象统计按天汇总时凌晨1点到2点发布的热点新闻评论数被记在发布当天但有些新闻是前一天晚上发布、第二天凌晨爆发的重跑日报时归属不稳定前后两天数字对不上。原因评论的create_time是毫秒时间戳我直接用它按自然日分组。但服务器时区如果设置成UTC凌晨的时间戳换算成北京时间就变成了前一天造成归属错位。解决统一用北京时间。两处修改第一爬虫入库时就把时间戳转成北京时间字符串再存不要存原始毫秒值第二SQL查询里对create_time用datetime(create_time/1000, unixepoch, 8 hours)做时区转换。这个坑最隐蔽因为它不是每次都出错只有跨到凌晨的边界时刻才暴露。5.5 跑完一天全量采集内存涨了800MB第二天直接卡死现象采集任务连着跑三天后服务器内存占用越来越高最终python进程被系统杀掉。原因评论数据全放在内存列表里一篇热门新闻的评论抓满5000条后列表转存SQLite时没有及时清空引用。python的垃圾回收机制不会立刻回收列表占用的内存尤其是列表里还存着每条的dict内存碎片化严重。解决改成边抓边写的流式落库模式。每抓到500条就executemany写一次SQLite然后清空列表。SQLite的写入性能完全扛得住这种频率单条新闻的评论数据根本不需要在内存里攒齐。6. 进阶自动生成舆情日报用勾稽关系验证指标可信度平台跑顺之后最后一步是把分析结果变成别人能直接用的东西——舆情日报。日报不需要花哨但必须满足三个条件数字能对上、结论能验证、导出即汇报。我习惯用python-docx在服务器上直接生成Word版日报标题、日期范围、Top5热点事件表、每篇的热度趋势、情感得分分布、热词TOP10。from docx import Document from docx.shared import Pt import sqlite3, json, datetime def generate_daily_report(stat_date): conn sqlite3.connect(news.db) rows conn.execute( SELECT d.docid, n.title, d.comment_count, d.hot_index, d.sentiment_score, d.top_keywords FROM daily_stats d JOIN news n ON d.docid n.docid WHERE d.stat_date ? ORDER BY d.hot_index DESC LIMIT 10 , (stat_date,)).fetchall() doc Document() doc.add_heading(f舆情日报 {stat_date}, level0) for rank, (docid, title, cnt, hot, score, kw) in enumerate(rows, 1): keywords json.loads(kw) p doc.add_paragraph() run p.add_run(f{rank}. {title}热度 {hot}评论 {cnt}) run.font.size Pt(12) doc.add_paragraph(f情感指数{偏正面 if score 0.3 else 偏负面 if score -0.3 else 中性}{score:.2f}) doc.add_paragraph(f热词{/.join(w for w, _ in keywords)}) doc.save(freport_{stat_date}.docx) return freport_{stat_date}.docx这本身就是一个极好的验证工具日报里每个热度指数都能反查回原始评论——点开对应新闻的评论列表人工核对前20条看情感判断是否合理。如果十篇里八篇明显判断错了先别怀疑技术指标回去查情感词典的覆盖度多半是这个月网络新词又换了。我这边跑过一段时间后发现一个反直觉的现象评论量最少的新闻情感指数反而经常极端。原因很简单一条评论的新闻情感分完全被那一条的措辞带偏。现在我把评论量少于30条的新闻单独打一个“样本不足”标签不参与情感排行只保留热度值这个改动让日报被领导质疑的次数直接降了下来。最后说一个我自己的习惯所有指标都保留原始计算中间值不直接覆盖入库。比如热度指数算完先存hot_index_raw再存归一化后的hot_index_rank这样每次日报生成都是一个可追溯的快照不会被“我昨天看的舆情数据怎么变了”这种问题问住。舆情数据分析的价值不在模型多高级而在每一个数字都能经受追问。希望这些思路能帮你在自己的平台上少踩几个坑。本文还有配套的精品资源点击获取