微博热点舆情聚类实战:从爬虫清洗到TF-IDF与KMeans的完整链路
简介面向对Python文本挖掘与舆情分析感兴趣的学习者资源以微博热点话题为对象完整提供了从数据采集、分词处理到聚类分析的项目源码与配套数据。核心依赖包括jieba分词、pandas数据处理、scikit-learn机器学习、matplotlib可视化与requests网络请求适合用于入门自然语言处理、掌握中文分词与文档聚类流程。压缩包仅50KB共9个文件包含5个Python脚本、2个文本数据文件、1个说明文档和1个CSV数据集脚本分工清晰涵盖爬虫采集、TF-IDF计算、矩阵构建与可视化等环节便于对照学习。目前已有209人浏览学习结合源码与数据可直观理解舆情热点聚类的完整实现路径从数据抓取、分词清洗到结果可视化环环相扣。项目整体虽轻量但流程完整特别适合快速掌握聚类分析每一步的计算逻辑也可作为课程设计或研究入门参考为后续扩展情感分析、主题模型等方向留下清晰改造空间。1. 用微博热点做舆情聚类难的不是爬数据而是让文本变成能算的东西舆情聚类分析这个方向很多人的第一反应是“先去爬微博”结果卡在登录、封 IP、验证码上三天没进展。真正做过一轮之后你会发现微博热点话题的采集反而是整个流程里最不费脑子的部分——数据拿到手之后分词、去停用词、向量化、调聚类参数每一步都在消耗你的耐心。尤其是热点话题这种短文本本身长度短、口语化严重、网络新词多直接套用标准 NLP 流程很容易跑出一个“看似收敛、实际全挤在一个簇里”的结果。这个项目标题里“源码数据”的组合对应的是一条完整可复现的链路拿到微博热点话题数据做中文文本清洗转成 TF-IDF 向量再用聚类算法分簇最后输出每个簇的代表词和热度分布。它适合谁一类是刚开始接触文本挖掘的学生或转行开发者想找一个不是豆瓣影评、不是新闻稿的实战项目另一类是做舆情分析或社交媒体运营的从业者需要从一堆话题里快速看出哪些事件在抱团发酵。前者能学到完整流程后者能直接拿到一套可以改数据源就跑的分析脚本。2. 微博热点数据从哪来三种采集方案的选型与拆解2.1 先判断手里有没有现成数据再决定要不要写爬虫标题里“源码数据”意味着这份项目大概率自带一份已经抓好的微博数据。拿到项目之后第一件事不是读代码而是看数据文件的格式和字段。常见的格式有两种CSV 和 JSON。CSV 一般长这样id,created_at,text,reposts_count,comments_count,attitudes_count,hot_word 1001,2024-11-20 14:23:01,某明星新电影发布首支预告,1234,892,5678,电影预告 1002,2024-11-20 14:25:44,这部电影的造型设计太惊艳了,456,233,1200,电影造型JSON 则往往是微博 API 或爬虫抓取时未处理的原始返回结构嵌套了一层“user”“retweeted_status”之类的对象。我一般会先写一段极简的预览脚本确认字段名和缺失情况再决定后续清洗策略import pandas as pd df pd.read_csv(weibo_hot.csv, encodingutf-8) print(df.shape) print(df.columns.tolist()) print(df.head(3).to_string()) # 检查缺失值占比超过 20% 的字段后面大概率用不上 print(df.isnull().mean().sort_values(ascendingFalse))这段代码本身没什么技术含量但它的价值在于让你在动手前心里有底——数据是干净规整的还是带了一堆爬虫异常记录。注意encoding参数微博数据的 CSV 用utf-8读取报错时改成gbk或utf-8-sig再试这是最常见的一个坑。如果你手里没有这份数据只想先跑通流程常见做法是自己构造一份小样本数据来验证聚类效果把后面要讲的流程跑通后再去采集真实数据。用pandas构造几百条微博文本做联调成本远低于一上来就攻坚采集。2.2 官方 API 和爬虫的取舍没有绝对方案只有当前条件下的最优解微博开放平台的历史 API 几乎全部收紧了权限个人开发者能拿到的基础接口非常有限数据时效性和完整性也打了折扣。项目如果标注“源码数据”大概率用的是基于网页版的爬虫方案。业界常见的实现思路是模拟登录拿到 Cookie带 Cookie 请求热点话题列表页或搜索页解析返回的 HTML 或 JSON 数据。一个相对温和、可以跑通全流程的代码框架如下import requests import json import time # 说明此处仅演示请求与解析结构实际使用需遵守目标平台服务条款 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Cookie: 你的登录凭证 # 从浏览器开发者工具中复制勿硬编码在正式代码里 } def fetch_hot_topic(page1): 拉取某一页的热门话题数据返回解析后的结构化记录 url https://weibo.com/ajax/statuses/hot_band params {page: page} resp requests.get(url, headersheaders, paramsparams, timeout10) # 正常返回是 JSON 结构直接解析即可 data resp.json() records [] for item in data.get(data, {}).get(band_list, []): if not item.get(note): continue # 跳过空话题 records.append({ hot_word: item.get(note), num: item.get(num), # 热度值 category: item.get(category), raw_text: item.get(raw_text) or item.get(note) }) return records if __name__ __main__: all_records [] for p in range(1, 3): # 先抓两页验证 all_records.extend(fetch_hot_topic(p)) time.sleep(2) # 控制请求频率别把自己玩进风控名单 print(len(all_records))这段代码里值得解释的参数有四个。timeout10是请求超时时间微博接口偶尔会慢设短了容易误判失败设长了请求会堆积。page是分页参数热点话题的返回顺序是按热度降序排列的。time.sleep(2)是请求间隔写死 2 秒是一个兼顾效率和风控的折中值。headers里的Cookie会有有效期爬的数量大或运行时间长要设计成从配置读取、过期后手动更新。关于采集需要提醒一句不同平台的接口结构会随时调整代码里解析的字段名可能某天就失效了。如果你的目标是快速跑通舆情聚类分析流程建议优先用项目自带数据爬虫只是补充手段不是主菜。2.3 多关键词增量采集让数据不止于“热点榜”这一个入口热点榜数据有个问题它在某个时间点只能反映当前最热的事件而对一个话题的“生命周期”刻画不足。做舆情聚类分析时我一般还会按热点词去搜相关微博把单条话题的内容铺开。这里只需要把请求地址替换成搜索接口同时增加一个关键词参数即可。def search_weibo(keyword: str, page: int 1): 按关键词搜索微博返回文本列表 url https://weibo.com/ajax/statuses/search params { q: keyword, page: page, sort: time # 按时间排序舆情分析一般关注最新进展 } resp requests.get(url, headersheaders, paramsparams, timeout10) data resp.json() texts [item.get(text_raw, ) for item in data.get(data, {}).get(list, [])] return texts # 用法示例把热搜榜上的前 5 个词各拉 3 页数据 # hot_words [r[hot_word] for r in all_records[:5]] # for w in hot_words: # for pg in range(1, 4): # texts search_weibo(w, pg) # # 存入本地文件或数据库这里的sort参数是舆情分析里的关键选择——按热度排序会丢过程、按时间排序才能看到话题的发酵路径。我一般会同时抓两种排序结果分析时分开看。text_raw是微博的原始文本字段注意这个字段里可能包含 HTML 标签和转义字符后面清洗时要处理。3. 从微博文本到聚类输入分词、去停用词和 TF-IDF 向量化的完整链路3.1 清洗规则怎么定去掉的内容和保留的内容同等重要微博短文本的清洗有它的特殊性。新闻语料清洗主要去掉空白和标点微博文本则要处理 用户、话题标签、URL、表情符号和“转发微博”这类冗余文字。一个小技巧不要用正则去匹配所有特殊字符而是先想清楚你要保留什么。中文分析中汉字、数字和部分中英文标点是有意义的、#、URL 是没意义的。下面是我常用的清洗函数import re def clean_weibo_text(text: str) - str: 清洗微博文本去 URL、用户、话题符号、多余空白 # 去掉 http(s) 链接 text re.sub(rhttps?://\S, , text) # 去掉 用户 text re.sub(r[\w\u4e00-\u9fa5\-], , text) # 去掉 #话题# 两端的井号但仍保留内部文字 text re.sub(r#([^#])#, r\1, text) # 去掉 HTML 标签和实体 text re.sub(r[^], , text) text re.sub(r[a-zA-Z];, , text) # 合并多个空格 text re.sub(r\s, , text).strip() return text # 示例 sample 转发微博 #电影首发# 这部电影太棒了电影官博 https://t.cn/abc123 print(clean_weibo_text(sample)) # 输出: 转发微博 电影首发 这部电影太棒了清洗函数的顺序是有讲究的。先去掉 URL 再处理 是因为链接中可能包含 字符先处理链接可以避免误伤。#话题#的处理不是直接删掉而是保留文字因为话题本身携带了关键主题信息对聚类是有用的。HTML 标签处理放在后面是因为某些接口返回的text_raw字段里会有a之类的标签包裹超链接先解 URL 再解标签顺序更稳定。注意严格说text_raw通常是纯文本字段HTML 标签多见于text字段两个字段采集时都可以拿到清洗时视情况保留哪个需要自己确认。3.2 jieba 分词与用户词典热点词是舆情聚类的生命线微博舆情聚类的分词不能直接裸用 jieba 默认模型。原因在于热点话题里充满了人名、作品名、品牌名和网络新词比如某明星的缩写、某综艺的简称默认词典压根没有。业界常规做法是准备一个自定义词典把从热点榜上收集到的话题词、相关实体名放进去再配合 jieba 加载import jieba # 把热点词和相关词写入词典文件 custom_words [某明星, 电影首发, 数据要素, 低空经济] for w in custom_words: jieba.add_word(w, freq10000) # freq 是词频权重越大越容易被切成一个词 text 某明星新电影首发预告引发全网热议 seg_list jieba.cut(text, cut_allFalse) print( / .join(seg_list)) # 输出: 某明星 / 新电影 / 首发 / 预告 / 引发 / 全网 / 热议freq参数的设定逻辑是相对的默认词典里常见词的频率大约在 1 万到 10 万之间自定义词给 1 万基本能保证不被切开。太高会导致误切比如把“明星新”也合并了。如果你在项目里看到jieba.load_userdict(userdict.txt)的调用说明作者把词典外置了那样维护成本更低不需要改代码直接往文件里加词就行。分词之后还有两个细节。一是去掉单字词单个汉字在聚类中几乎不携带可区分的信息二是保留“否定词动词”的组合结构比如“不支持”“不会去”单独切开会把语义完全搞反。处理方式可以是提前做规则替换把常见否定组合换成下划线连接的形式。stopwords set() with open(stopwords_cn.txt, encodingutf-8) as f: for line in f: stopwords.add(line.strip()) def tokenize(text: str) - list: cleaned clean_weibo_text(text) words jieba.cut(cleaned, cut_allFalse) result [] for w in words: w w.strip() if len(w) 2: # 去掉单字和空字符串 continue if w in stopwords: # 去掉停用词 continue if w.isdigit() and len(w) 4: # 去掉疑似时间戳的纯数字 continue result.append(w) return result # 使用df[tokens] df[text].apply(tokenize)单字过滤的阈值不是一成不变的如果聚类结果发现“剧”“片”“人”这类单字频繁出现在多个簇的主题词里说明它们带信息量可以单独保留。停用词表的来源很多GitHub 上有几个经典的中文停用词库但建议在通用停用词基础上追加一批微博特有词比如“转发微博”“分享图片”“收起全文”这类结构化冗余内容。3.3 TF-IDF 的参数不只是默认值min_df、max_df 和 ngram_range 怎么调文本变成向量这一步项目源码里大概率用到了TfidfVectorizer。这个类的参数里min_df和max_df是最容易被忽略但影响最大的两个。默认值min_df1表示所有在语料中出现至少一次的词汇都进入词表这会导致向量维度极高、噪声极大微博短文本里很多词语只出现一两次它们对聚类没有任何帮助。from sklearn.feature_extraction.text import TfidfVectorizer # 把分词结果重新拼成空格分隔的字符串 df[clean_text] df[tokens].apply(lambda x: .join(x)) vectorizer TfidfVectorizer( min_df3, # 至少在 3 篇文档中出现过过滤长尾噪声 max_df0.6, # 在超过 60% 的文档中都出现过滤“全场通用”的泛词 ngram_range(1, 2),# 词和二元词组都保留 sublinear_tfTrue # 对 tf 做 log 变换弱化高频词优势 ) tfidf_matrix vectorizer.fit_transform(df[clean_text]) print(tfidf_matrix.shape) # (样本数, 特征数)关于这组参数我的做法和经验值是样本量在几千条时min_df3到min_df5是比较稳的区间样本量过万可以进一步提高到min_df10。max_df0.6意味着如果一个词出现在 60% 以上的微博里这个词大概率是“今天”“一个”“真的”这类泛化词去掉它聚类才会出现区分度。ngram_range(1, 2)的意义在于“电影首发”这种两个词组合起来才有具体含义的短语光看单字词可能切碎了语义。调大 ngram 后特征维度会爆炸如果发现矩阵太大跑不动优先降 ngram 而不是把min_df调高。有个常见误用是只调用fit_transform不看中间结果。实际上打印出每类特征的代表词会帮你快速诊断参数是否合理。用下面的代码把向量映射回词语feature_names vectorizer.get_feature_names_out() # 打印 TF-IDF 值最高的 10 个特征看看它们是否真的有意义 dense tfidf_matrix.toarray()[0] top_indices dense.argsort()[-10:][::-1] print([feature_names[i] for i in top_indices])4. 聚类算法怎么选KMeans、MiniBatchKMeans 与 LDA 主题模型的对比与实践4.1 KMeans 是舆情聚类的默认起点但不是终点做过文本聚类的人都知道KMeans 是默认选择——因为它快、可控、有成熟的评估指标。微博热点舆情这个场景更是如此数据量级通常在几万条以内特征向量是稀疏的 TF-IDFKMeans 跑起来非常轻松。项目源码里大概率也是从 KMeans 开始。实现本身不复杂from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score # 用肘部法则先粗选 K 值范围 k_range range(2, 15) scores [] for k in k_range: km KMeans(n_clustersk, random_state42, n_init10) labels km.fit_predict(tfidf_matrix) score silhouette_score(tfidf_matrix, labels) scores.append(score) print(fK{k}, silhouette_score{score:.4f}) # 选择轮廓系数最高的 K best_k k_range[scores.index(max(scores))] print(f最佳 K 值: {best_k})轮廓系数的取值逻辑要讲清楚范围从 -1 到 1越接近 1 说明簇内紧凑、簇间分离0 附近说明样本在两个簇的边界上。实际舆情数据中轮廓系数在 0.15 到 0.3 之间已经算可用如果超过 0.4 反而要怀疑是不是数据人工构造的。文本聚类不是类别明确的分类任务微博话题之间天然有交叉。n_init10这个参数容易被忽略。KMeans 的初始质心是随机的n_init表示用 10 组不同初始质心分别迭代、取最优结果。默认值是 10但如果数据量大可以降到 5 提升速度。random_state42是为了结果可复现这对调试很重要——如果没有固定随机种子同一份代码每次跑出来的聚类结果都不同排查问题时你会疯掉的。4.2 数据量大时换 MiniBatchKMeans参数怎么调不降质微博数据如果做了一个月以上的持续采集或者热点话题的所有相关微博都抓全数据量很可能超过几十万条。这时候 KMeans 每次迭代都要用全量数据计算距离内存占用和耗时会明显上升。MiniBatchKMeans 是 KMeans 的近似实现每次随机抽一个小批量样本来更新质心训练速度提升明显代价是聚类结果有一点随机波动。from sklearn.cluster import MiniBatchKMeans mbk MiniBatchKMeans( n_clustersbest_k, batch_size1024, # 每批样本数越大越接近标准 KMeans n_init3, # 初始化次数可以比 KMeans 少因为有批量随机性 max_iter100, random_state42 ) labels_mbk mbk.fit_predict(tfidf_matrix)batch_size是 MiniBatchKMeans 最重要的旋钮。设置过小比如 256结果波动大设置过大接近全量又失去加速意义。经验值是样本总量的 1% 到 5%同时不要小于 512。max_iter100是最大迭代轮数MiniBatch 收敛速度快通常 50 轮内就稳定了。这个算法有个特点如果数据本身就存在一个明显的主簇MiniBatch 容易把其他小簇合并掉。建议对比标准 KMeans 和 MiniBatchKMeans 在少量样本上的轮廓系数差异如果差别超过 0.03 就值得考虑用标准版。4.3 LDA 主题模型是聚类的“另一条路”它给的不是分组是主题分布LDA 不是聚类算法但它能解决舆情分析里的一个核心诉求——热点话题分组后每个簇到底在聊什么。KMeans 给出的每个簇的质心向量可以按特征权重大小输出关键词但主题词经常重叠LDA 则是为每篇文章计算一个主题分布然后你可以按主题归属来“软聚类”。使用 LDA 需要考虑两个预处理差异一是 LDA 对高频词敏感停用词要加得更狠二是特征一般只用CountVectorizer不用 TF-IDF因为 LDA 本身通过统计共现来发现主题TF 的权重分布会干扰它的计算。from sklearn.decomposition import LatentDirichletAllocation from sklearn.feature_extraction.text import CountVectorizer count_vect CountVectorizer(min_df5, max_df0.7) count_matrix count_vect.fit_transform(df[clean_text]) lda LatentDirichletAllocation( n_components8, # 主题个数对应舆情中的“几个方向” learning_methodbatch, # 小数据用 batch大数据用 online random_state42, max_iter20 ) doc_topic lda.fit_transform(count_matrix) # 打印每个主题的最显著词汇 feature_names count_vect.get_feature_names_out() for topic_idx, topic in enumerate(lda.components_): top_words [feature_names[i] for i in topic.argsort()[:-10-1:-1]] print(fTopic {topic_idx}: { .join(top_words)})learning_method的选择标准很简单样本量小于 10 万用batch更稳定大于 10 万用online提速明显。n_components和 KMeans 的K一样需要调经验做法是设 5 到 15 之间跑几轮看每个主题的高频词是否可分。LDA 在短文本上的效果要比长文本差因为共现信息少所以如果微博正文普遍很短LDA 的结果可能不如 KMeans 直观。项目里也可以用 LDA 做 KMeans 之后的簇内再分析这样两个方向各司其职。5. 舆情聚类分析常见问题避坑现象、原因和解决办法5.1 聚类结果“一坨”所有话题全挤在一个簇里现象不管 K 值设多少轮廓系数都在 0.05 以下每个簇的大小差距悬殊最大簇占了 80% 以上的样本。原因多数情况是向量化之前的文本预处理出了问题。最常见的是停用词表太弱“今天”“真的”“一个”“什么”这类词没有过滤干净它们在所有文本中频繁出现成了聚类的主导信号其次是把转发前缀“转发微博”保留了下来这类结构化的冗余内容会在 TF-IDF 里获得较高权重。解决先返回向量化步骤把max_df从 0.6 降到 0.3 再跑一次轮廓系数。如果结果没有改善打印 TF-IDF 特征权重 TOP 20逐个看这些词是不是真的和“某个方向”相关。再不行就回退检查清洗规则——clean_text字段里是否残留了乱码或英文。舆情聚类的文本质量门槛远高于你的直觉宁可少留词也不能留泛词。5.2 Jieba 把关键实体切碎了现象聚类主题词里出现“电影”“首发”“预告”而不是“某电影首发预告”一个作品名被拆成三、四段导致本来应该聚在一起的话题被分散到不同簇。原因默认词典没有收录这个实体名称。我之前碰到过“数据要素”被切成“数据/要素”聚类结果里数据政策的讨论散落到了三个簇。解决最直接的办法是维护一个用户词典。从热点榜拉出全部话题词再结合当日微博热门搜索词筛出长度大于等于 2 的专有名词作为词典输入。jieba.add_word的freq设 10000 到 20000 之间太大容易误合并相邻词。需要提醒的是不要把所有热门词都塞进词典像“今天”这类高频泛词加进去了反而会影响 jieba 的标准分词效果。5.3 编码问题导致运行到一半崩溃现象读入 CSV 时直接报UnicodeDecodeError或者聚类跑了很久之后发现大量文本变成了\uXXXX转义字符。原因微博数据的编码有两种常见情况——CSV 可能是gbk或utf-8混合JSON 字段里嵌了\u转义但没有被正确解码。有些爬虫把数据写文件时用了默认编码换到 Linux 环境读取就出问题。解决读文件时统一用utf-8尝试报错就回退到gbk用errorsignore不推荐因为会静默丢数据。另一个更稳妥的方案是所有中间结果存成parquet或jsonl格式这两个格式的编码问题比 CSV 少得多。import pandas as pd def load_csv_auto(path: str): 按编码优先级自动加载并输出实际使用编码 for enc in [utf-8-sig, gbk, latin1]: try: df pd.read_csv(path, encodingenc) print(fFile loaded with encoding: {enc}) return df except UnicodeDecodeError: continue raise ValueError(fAll encodings failed for {path})5.4 聚类结果不稳定同一份代码两次运行簇标签完全不同现象固定了random_state42之后KMeans 的标签仍然每次不同或者不同机器上跑完全不同的簇划分。原因如果你在TfidfVectorizer之前对 DataFrame 做了一次sample(frac1)打乱操作且没有固定随机种子那么每次跑的顺序不同fit_transform生成的稀疏矩阵行序不同KMeans 的结果虽然理论上不受行序影响但 MiniBatchKMeans 的批量采样会受到牵连。另一个原因是 numpy 或 sklearn 版本差异导致 SVD 结果有微小浮点差异聚类边界样本归属变化。解决全流程固定所有随机种子包括 pandas 的抽样、jieba 的分词不涉及随机但要在导入顺序上稳定还有 sklearn 各估计器的random_state。另外在聚类完成之后把df的原始索引作为唯一标识输出到结果文件下一次跑的时候对比一下哪些样本的簇归属变了用这种方法定位是预处理波动还是算法波动import numpy as np np.random.seed(42) # 全局固定随机种子5.5 可视化时词云全是废话现象词云的 TOP 词是“一个”“没有”“什么”之类的泛词热点事件的核心词反而出现率很低。原因词云脚本直接用了原始文本做词频统计没走清洗和停用词流程。这和聚类共用一套清洗逻辑但词云对停用词的敏感度更高——聚类时出现一两次的噪声词对向量影响有限在词云里却会占据可观的视觉面积。解决建一套“词云专用停用词表”在通用停用词基础上追加人称代词、时间副词、程度副词。词云直接用df[tokens]里过滤后的词做Counter统计。更省事的方法是词云前先跑一遍聚类只取目标簇的样本做词云这样词云内容天然聚焦在特定话题方向视觉表达更有说服力。6. 让结果能交代轮廓系数验证、可视化输出与业务解读优化聚类跑完只是第一步结果要能给别人讲清楚才算是真的交付。面对非技术背景的业务方或评审老师一张散点图加几个簇主题词的组合比任何评估指标都有说服力。TSNE 降维是业界最常用的可视化手段把高维 TF-IDF 向量压到二维平面再用不同颜色标记簇归属一眼就能看到聚类是否形成了团聚结构。from sklearn.manifold import TSNE import matplotlib.pyplot as plt # 限制样本量TSNE 计算复杂度高全量数据跑很慢 sample_df df.sample(n2000, random_state42) sample_labels labels_mbk[sample_df.index] # 假设用 MiniBatchKMeans 的标签 tsne TSNE(n_components2, random_state42, perplexity30, max_iter300) coords tsne.fit_transform(tfidf_matrix[sample_df.index]) plt.figure(figsize(12, 8)) scatter plt.scatter(coords[:, 0], coords[:, 1], csample_labels, cmaptab20, s10, alpha0.7) plt.colorbar(scatter) plt.title(TSNE Visualization of Weibo Topic Clusters) plt.savefig(cluster_result.png, dpi150, bbox_inchestight)TSNE 的参数里perplexity影响可视化的局部结构取值范围一般设在 5 到 50 之间2000 个样本时 30 是一个合理的起点。max_iter300不够时会出现警告提高到 500 到 1000 可获得更稳定的嵌入结果。注意 TSNE 每次运行结果会有细微差异这是正常现象不要因为这个去调随机种子之外的东西。可视化之后还需要一套“簇主题词样本示例”的输出模块让非技术人员能快速理解每个簇在讲什么def describe_cluster(topic_matrix, labels, df, top_n10): 为每个簇输出关键词和代表性微博 results {} for cluster_id in sorted(set(labels)): # 找到该簇的样本索引 indices [i for i, lab in enumerate(labels) if lab cluster_id] if not indices: continue # 用 cluster 内词频最高的词作为主题词 cluster_texts [df[clean_text].iloc[i].split() for i in indices] from collections import Counter word_counter Counter() for tokens in cluster_texts: word_counter.update(tokens) top_words [w for w, _ in word_counter.most_common(top_n)] results[cluster_id] { size: len(indices), top_words: top_words, examples: df[text].iloc[indices[:3]].tolist() } return results cluster_summary describe_cluster(tfidf_matrix, labels_mbk, df) for cid, info in cluster_summary.items(): print(f簇 {cid}: 共 {info[size]} 条样本主题词: {、.join(info[top_words])})这段代码输出的“大小主题词样例微博”就是舆情报告的核心素材。运营人员在解读时会发现具体哪条微博属于哪个簇并不重要重要的是“哪些话题在抱团”“哪些话题只是孤零零发酵”。这部分逻辑如果项目源码里有可以直接套用如果没有按上面的结构补上就行。最后说一个我自己的习惯舆情聚类的项目交付时建议做一份“时间切片对比”。把数据按天或按小时切片分别跑聚类观察簇的数量和主题词随时间变化的轨迹。某话题从“单个热点”变成“簇”再裂成“子话题”的过程才是舆情分析里最有价值的信息。我接手过的类似项目里那些让业务方真正觉得有用的结论都不是看完静态聚类结果产生的而是看了时间维度上簇的变化。这是这套源码和数据之外你可以额外投入一点精力获得的增量。希望帮到你。本文还有配套的精品资源点击获取