☰
用LDA主题模型分析微博文本:从爬虫到树图可视化全流程
2026/10/3 9:22:34 网站建设 项目流程

简介:这套基于Python的微博数据抓取、文本分析与可视化项目源码,面向毕业设计、期末大作业及课程设计场景,适合对Python数据分析与自然语言处理有基础了解的开发者参考。项目以LDA主题模型为核心并采用树图展示结果,完整覆盖微博数据采集、文本预处理、情感分析、TF-IDF聚类、Word2Vec聚类及关键词提取等流程,代码注释详细,新手也能读懂关键逻辑。资源共53个文件,压缩包大小66.36MB,核心为20个Python脚本,负责数据抓取、建模与图表生成;6个HTML文件呈现树图、饼图、3D柱状图等可视化结果;另含词向量模型、情感词典、JSON数据、CSV标注结果及README说明文档,部署时可按目录快速定位。目前已有179人学习下载。项目经严格调试可直接运行,从数据清洗到多维可视化层层递进,结构清晰,便于二次扩展,适合直接作为毕设或期末大作业的高分参考。

1. 微博文本分析为什么非要用 LDA 树图:从 3000 条微博里看出热点结构

把微博数据抓下来,分词、去停用词、跑 LDA,最后画成一张树图——这条链路听起来像课程设计,但真正做舆情和竞品分析的人会告诉你,它解决的是「评论太多读不完」和「关键词共现看不出结构」这两个实际问题。我接过一个品牌口碑分析的需求,客户要求把一周内提到该品牌的几万条微博按话题归类,领导要看一页能讲明白的图。这时候 LDA 主题模型加上树图展示,比词云和柱状图都更能说明「大家到底在吵什么」。本文把这套方案的完整套路拆开讲,新手按章节能跑通,熟手可以直接抄参数和排错清单。

2. 抓微博数据的技术选型:登录态、接口字段与存储结构

2.1 微博数据获取的三条路径:为什么我选 requests 而不是 selenium

微博数据的抓取路径,常见的有三条。第一条是网页版搜索页直接解析 HTML,用 requests 拿回来再用 BeautifulSoup 抽结构化字段,问题在于微博页面是服务端渲染加异步加载混着来,翻页和展开全文要处理不少动态逻辑,代码脆,页面一改版就崩。第二条是 Selenium 模拟浏览器,能绕过一部分渲染问题,但慢、吃内存、容易被识别,而且微博对无头浏览器的检测并不弱,跑一两个小时就掉登录态,运维成本高。第三条是直接打微博的搜索接口(俗称 ajax 接口),带 cookie 请求 JSON 数据,字段干净、翻页稳定,这也是我实际项目中用的方案。

爬虫方案选型的一个判断标准是:数据量级和更新频率。如果只是跑一次分析任务,追求的是「今天必须出结果」,那稳定性比隐蔽性重要,带 cookie 的 requests 就够。如果是长期监控,建议再加一层异常重试和登录态失效检测,而不是一上来就上分布式。微博的反爬重点是签名参数和频率控制,这两点用 requests 都能应付,后面避坑章节会细讲。

2.2 用 requests 带 cookie 抓微博搜索页:最小可运行脚本

这里给一套能直接跑的搜索接口抓取脚本。注意,cookie 需要你自己从浏览器登录微博后复制,脚本不负责模拟登录,这是刻意为之——模拟登录微博的账号密码流程早就被风控盯上,用自己账号的 cookie 做低频分析,是风险和成本最均衡的做法。

import requests import json import time from urllib.parse import quote # 核心参数:q是搜索关键词,page是页码,Referer和Cookie必须带上 keyword = "某手机品牌" cookie = "你的浏览器登录后的Cookie字符串" # 从浏览器开发者工具里复制 headers = { "Cookie": cookie, "Referer": f"https://s.weibo.com/weibo?q={quote(keyword)}", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "X-Requested-With": "XMLHttpRequest" } def fetch_page(page): # containerid 是搜索结果的固定标识,1035 表示综合搜索 url = ("https://m.weibo.cn/api/container/getIndex?" f"containerid=100103type%3D1%26q%3D{quote(keyword)}" f"&page_type=searchall&page={page}") r = requests.get(url, headers=headers, timeout=10) r.raise_for_status() cards = r.json().get("data", {}).get("cards", []) results = [] for card in cards: if card.get("card_type") == 9: # 9是微博正文卡片类型 mblog = card.get("mblog", {}) results.append({ "id": mblog.get("idstr"), "text": mblog.get("text"), "user": mblog.get("user", {}).get("screen_name"), "reposts_count": mblog.get("reposts_count"), "comments_count": mblog.get("comments_count"), "attitudes_count": mblog.get("attitudes_count"), "created_at": mblog.get("created_at") }) return results all_data = [] for page in range(1, 6): # 只抓前5页,控制频率 all_data.extend(fetch_page(page)) time.sleep(2) # 必须等,否则触发频率限制 with open("weibo_raw.json", "w", encoding="utf-8") as f: json.dump(all_data, f, ensure_ascii=False, indent=2) print(f"共抓取 {len(all_data)} 条微博")

这段代码的逻辑很直接:fetch_page负责单页请求,返回列表;主循环控制页码和间隔。card_type == 9是搜索接口里微博正文的身份标识,除此之外还有 11(热门话题)、12(推荐用户)等类型,不做过滤会把一堆非微博内容混进文本分析,LDA 的主题就会变成「用户昵称大会」。mblog字段里text是 HTML 格式,后面预处理时需要剥标签,这里先原样存下来,方便追溯原始数据。

2.3 数据落地的字段设计与 JSON 存储的取舍

存储方案上,我建议原始数据一律 JSON 落盘,不要直接进数据库。原因是文本分析项目的数据清洗、去重、人工抽样检查都发生在建模前,JSON 文件用编辑器就能打开看,出了问题能快速定位。如果你抓的数据量超过十万条,再考虑 SQLite 或 MySQL,但我做过的舆情项目里,单次分析很少超过五万条,JSON 加 DataFrames 完全扛得住。

字段设计上,text、user、created_at是必留的,转发、评论、点赞数看分析目标选留。特别提醒,text字段里可能带「展开全文」等占位标签,以及图片链接、话题标签,这些会在 LDA 分词时混入噪声。我在预处理章节会给出完整的清理规则。还有一点经验:把created_at存成原始字符串的同时,最好再解析一版标准时间格式,比如2025-01-18 14:30:00,因为后面做时间维度分析时,微博返回的「刚刚」「12分钟前」这类相对时间没法排序。

3. 微博文本预处理:把几千条微博变成 LDA 能吃的词袋

3.1 清洗规则:HTML 标签、URL、表情符号、话题标签的优先级

微博文本和普通新闻稿不一样,噪声类型极其丰富。HTML 标签是第一个要处理的,因为接口返回的text自带<a>、<span>包裹,直接分词会让「href」「class」这种垃圾词进词典。URL、图片链接同理。话题标签#某某#需要单独抽出来,它本身就是用户主动打的语义标签,对 LDA 反而有价值,但直接分词会把井号也当成字符,所以我一般把话题标签提出来附到文本后面,去掉井号保留内容,让分词器把话题拆成词,而不是把「#某某#」当成一个整体。

表情符号的处理要分两层:中英文括号里的文字表情,类似「[允悲]」,属于微博特有词汇,可以直接删除;纯 emoji 字符则要依赖正则范围过滤。另外,展开全文、查看图片这类功能性文案也是高频噪声,这些是在列表页截断长微博时自动拼上的,不删的话几乎每条微博都带着,LDA 会把它当成头号主题词。

import re import html as html_lib def clean_weibo_text(raw): # 先解 HTML 实体,再剥标签,顺序不能反 text = html_lib.unescape(raw) text = re.sub(r"<[^>]+>", "", text) text = re.sub(r"https?://\S+", "", text) # 去 URL text = re.sub(r"\[(允悲|哈哈|微笑|泪|狗头|doge)]", "", text) # 去文字表情,按需扩充 text = re.sub(r"\u200b", "", text) # 去零宽空格 # 话题标签:单独提取,内容保留 topics = re.findall(r"#([^#]+)#", text) text = re.sub(r"#([^#]+)#", "", text) return text.strip(), topics

这段代码里有几个细节值得说明。先unescape再剥标签,是为了避免&nbsp;等实体残留成乱码;零宽空格\u200b是微博文本里隐藏的字符分隔符,肉眼看不见,不去掉会让 jieba 分词产出莫名其妙的半截词;话题标签提取回来后,我建议并入topics列表单独存,不进正文,后面如果需要做话题聚合可以单独分析。

3.2 jieba 分词参数与自定义词典:词典质量直接决定 LDA 主题是否可读

分词我用 jieba,原因是微博文本领域词多、网络词多,jieba 的自定义词典机制最容易调。默认的jieba.cut在微博场景下有三个问题:一是英文和数字会被切成单个字母,二是手机型号如「iPhone 15 Pro Max」会被拆碎,三是品牌词和产品词如果不进词典,会被拆得面目全非。常见做法是维护一个自定义词典文件,把品牌名、产品名、竞品名、行业内惯用语全部放进去,一行一个词。

import jieba # 自定义词典,格式:词 词频 词性,词频可不填 user_dict = "weibo_dict.txt" jieba.load_userdict(user_dict) # 示例内容: # 折叠屏 20 nz # 骁龙 15 nz # 影像旗舰 10 nz # 性价比 10 n def tokenize(text): # cut_all=False 表示精确模式,适合 LDA words = jieba.lcut(text, cut_all=False) # 过滤:长度小于2的词、纯标点、纯数字、stopword result = [w for w in words if len(w) >= 2 and not re.fullmatch(r"[\W\d_]+", w) and w not in STOP_WORDS] return result

这里有两个参数直接影响后期建模。cut_all=False选精确模式,因为全模式会把「折叠屏」同时切出「折叠」和「屏」,LDA 词典里出现大量重叠词,主题权重会被打散。过滤条件里len(w) >= 2是强行去掉单字词——微博里单字词的语义太弱,「的」「了」「在」靠停用词表,但一些奇怪的姓氏、语气词也经常混进来,用长度过滤比穷举停用词更稳。[\\W\\d_]+这个正则把纯符号、纯数字、下划线全部排除,避免「8」「999」这类数字堆进主题。

3.3 从清洗文本到 gensim 词典与语料库

LDA 建模前最后一步,是把所有分词结果组装成 gensim 能识别的语料格式。这一步最常见的翻车点是把整条微博当一篇文档,但微博正文平均只有几十个字,单独成文档会极其稀疏,主题分布几乎等于零。我处理的方式是按天或者按用户聚合——分析热点舆情时按天聚合,分析账号画像时按用户聚合。聚合后每个文档长度在几百到上千词,LDA 才能稳定学习主题分布。

import pandas as pd from gensim.corpora import Dictionary from gensim.models import LdaModel from collections import defaultdict # 假设 df 有 columns: date, clean_text(已清洗+分词后的词列表) df = pd.read_json("weibo_cleaned.json", encoding="utf-8") # 按天聚合:把同一天的微博文本拼成一个长列表 df["token_list"] = df["clean_text"].apply(lambda x: x.split(" ")) daily = df.groupby("date")["token_list"].sum().reset_index() documents = daily["token_list"].tolist() # 构建词典并过滤极端稀疏词 dictionary = Dictionary(documents) # 词频低于5的词删掉,出现超过50%文档的词删掉 dictionary.filter_extremes(no_below=5, no_above=0.5) # 词袋向量化 corpus = [dictionary.doc2bow(doc) for doc in documents]

filter_extremes的两个参数值得较真。no_below=5表示词必须在至少 5 篇文档里出现过,否则认为它是拼写错误或噪声;no_above=0.5表示词不能出现在超过 50% 的文档里——像「微博」「转发」这类每篇都出现的词,对区分主题没有信息量,删掉以后 LDA 的主题区分度会明显变好。这两个值没有标准答案,数据量小就调低no_below到 2 或 3,数据量大可以调到 10,先跑一版看主题词再反推调参方向。

4. LDA 主题建模与树图可视化:主题数怎么定,树图怎么读

4.1 用一致性分数和困惑度确定主题数 k

LDA 建模最核心的超参数是主题数num_topics,也就是k。调得太小,主题之间会互相杂糅,比如「屏幕」和「电池」的话题混在一起;调得太大,主题会碎成几个强关联词的重复组合。我的做法是让模型自己「投票」:对一系列k值分别计算主题一致性(topic coherence),一致性高的候选值优先。

from gensim.models import CoherenceModel import numpy as np # 对 k=8 到 k=30 逐个训练并计算一致性 coherence_scores = [] k_range = range(8, 31, 2) for k in k_range: lda = LdaModel(corpus=corpus, num_topics=k, id2word=dictionary, passes=20, random_state=42, alpha="auto") cm = CoherenceModel(model=lda, texts=documents, dictionary=dictionary, coherence="c_v") coherence_scores.append(cm.get_coherence()) print(f"k={k}, coherence={coherence_scores[-1]:.4f}") best_k = k_range[int(np.argmax(coherence_scores))] print(f"最优主题数: {best_k}")

coherence="c_v"是 gensim 实现里效果比较稳的一致性指标,基于滑动窗口词共现计算,数值在 0 到 1 之间,一般微博文本能跑到 0.4 以上就算主题可读。alpha="auto"让模型在训练中自动学习文档-主题分布的稀疏度,比固定默认值更适配微博这种长尾分布明显的语料。passes=20是模型在整个语料上的迭代轮数,过小(比如 5)会让主题不稳定,每次跑出来结果都不一样,我一般取 20 到 50,视语料大小而定。

一致性分数不是越高越好,这一点容易被新手误解。c_v分数极高的极端情况往往表示主题词全部是高频同义反复,对业务没有区分价值。所以优选k的逻辑是:先看一致性曲线的拐点,把拐点附近的几个k都跑一遍,人工读主题词,而不是纯靠分数最高值选。

4.2 用 pyecharts 画树图:把主题-词-权重映射成嵌套节点

LDA 结果的树图有两种画法,一种是pyecharts的Tree图,展示「主题 -> 关键词」的层级结构;另一种是sunburst旭日图,展示权重占比。这里重点讲 Tree 图,因为标题明确指向「树图」,而且 Tree 图对层级关系的表达能力更强。画图前的数据准备非常关键:LDA 训练出来的原始格式是「主题编号 -> (词, 权重)」的列表,要转成树节点的字典结构再喂给 pyecharts。

from pyecharts import options as opts from pyecharts.charts import Tree # 从训练好的 lda 模型提取每个主题的高权重词 def lda_to_tree_data(lda, dictionary, topn=8): nodes = [] # 根节点,名称可以写业务语义 root = {"name": "微博主题", "children": []} for topic_id in range(lda.num_topics): # show_topics 返回格式: [(主题ID, [(词, 权重), ...]), ...] topic_words = lda.show_topic(topic_id, topn=topn) topic_name = f"主题{topic_id}" # 主题词拼接作为子节点的层级 topic_children = [] for word, weight in topic_words: topic_children.append({ "name": f"{word} ({weight:.3f})", "value": round(float(weight), 3) }) root["children"].append({ "name": topic_name, "children": topic_children }) return [root] tree_data = lda_to_tree_data(lda, dictionary, topn=8) tree = ( Tree(init_opts=opts.InitOpts(width="1200px", height="800px")) .add( series_name="LDA主题树图", data=tree_data, layout="orthogonal", # 正交布局,适合横向层级展示 orient="LR", # 从左到右展开 symbol="circle", symbol_size=8, levels=[ opts.TreeLevelsOpts(depth=0, itemstyle_opts=opts.ItemStyleOpts(color="#F6BD16")), opts.TreeLevelsOpts(depth=1, itemstyle_opts=opts.ItemStyleOpts(color="#3169E6")), opts.TreeLevelsOpts(depth=2, itemstyle_opts=opts.ItemStyleOpts(color="#27B1B6")) ] ) .set_global_opts(title_opts=opts.TitleOpts(title="微博话题 LDA 主题树图")) ) tree.render("lda_tree.html")

lda_to_tree_data把主题模型的输出整理成嵌套字典,show_topic拿到每个主题下权重最高的 8 个词,把词和权重一起写进节点名称,权重存进value字段,这样鼠标悬停在节点上能看到数值。layout="orthogonal"是树图默认的正交布局,orient="LR"决定根在左、子节点向右展开,适合主题层级不超过 3 层的场景。levels按深度配置颜色,0 层是根,1 层是主题,2 层是关键词,三层颜色拉开差异后,汇报投屏时听众一眼就能分清层级。

4.3 树图参数调优:节点大小、布局方向、展示关键词条数

树图调参有几个实际经验。第一,topn别超过 10,否则主题节点下挂太多关键词,图会横向拉得很长,反而看不清结构。第二,节点大小symbol_size和分支数量有关,如果主题数大于 15,建议把symbol_size调小到 6,不然主题节点相互遮挡。第三,orient参数在Tree图里只有LR和RL两种横向布局有实际意义,竖向TB在主题词过多时会让高度爆炸,不建议。第四,中文节点名的显示宽度取决于浏览器的字体渲染,name里带权重的写法如屏幕 (0.032)在小字号下容易截断,颜色层级调亮一些能缓解阅读压力。

如果读者拿到的是源码包里现成的文档和配置,你会发现真正要改的通常就是这几处:TOPIC_K(主题数)、TOP_N_WORDS(每主题展示词数)、DEPTH_COLORS(层级配色)。这三个参数确认下来,树图的整体观感基本定型。

5. 微博爬虫与 LDA 建模的避坑记录:现象、原因、解决

5.1 搜索接口返回 414 或空 cards:签名参数缺失与请求频率失控

现象:代码跑通第一页,第二页开始返回 HTTP 414,或者返回 JSON 里cards缺失、为空列表。原因:m.weibo.cn的搜索接口在翻页时会校验page参数与containerid的匹配关系,page_type=searchall在某些分页位置会失效,同时单 IP 高频请求会被网关直接拦截。解决:一是把请求间隔从time.sleep(2)提高到time.sleep(3),并加入随机抖动sleep(2.5 + random.random());二是给fetch_page加一层重试机制,遇到状态码 414 就等 30 秒换page参数重新请求;三是高频需求下建议抓https://s.weibo.com/weibo?q=关键词的桌面版搜索页,它的接口用的是x-www-form-urlencoded的 POST 请求,抗限流能力稍强,但解析成本会高一些。

5.2 LDA 结果里全是「好的」「真的」「微博」:停用词表没针对微博定制

现象:训练出来的主题词前五位是「好的」「真的」「微博」「转发」「内容」,业务关键词一个都没进主题。原因:通用的哈工大停用词表没有覆盖微博口语高频词,「真的」「好的」「啊啊啊」这类词在每条微博都出现,LDA 把它们当成了背景主题。解决:把前几次模型输出里高频且无语义的词手动追加进停用词表,常见做法是先跑一版模型,把lda.show_topic输出里明显是噪声的词加入STOP_WORDS,再重跑一遍。这一迭代通常要做两三轮,第一轮删通用停用词,第二轮删微博特征噪声,第三轮删品牌周边代称的无效变体。

5.3 树图渲染出来是空白页面:数据没进 nodes 或者 JS 资源没加载

现象:lda_tree.html用浏览器打开,只看到标题,树图区域是空白。原因有几种,最常见的是lda_to_tree_data返回的数据结构里children键名写错,pyecharts 的Tree图要求根节点必须包含children字段,且data必须是列表结构,写成单个字典会静默失败。另一个常见原因是 HTML 文件中的 ECharts JS 资源请求失败,离线环境或内网隔离环境加载不了 CDN 脚本。解决:检查返回的tree_data是否json.dumps后能看到完整嵌套结构;离线场景把echarts.min.js下载到本地,通过Tree.add的is_remote_asset=True参数指定本地路径,或者在 HTML 生成后手动替换<script src>指向本地文件。

5.4 jieba 分词把「荣耀Magic6 Pro」切成「荣耀」「Magic」「6」「Pro」:

现象:品牌型号词被切碎,LDA 主题里出现大量孤立数字和「Pro」「Max」。原因:自定义词典里没有收录该型号,jieba 对数字开头的英文组合默认切分。解决:在自定义词典文件里补上完整的品牌型号词,例如荣耀Magic6 Pro 20 nz,注意词里的空格要与实际写法完全一致,然后重新执行jieba.load_userdict。另外建议在分词前做一步「品牌词保护」:对文本里的已知型号先做替换为占位符,再分词,最后把占位符还原成原词,这样能避免cut_all=False模式下长型号仍然被拆掉。

5.5 源码包里的文档说明与实际代码版本不一致:配置项对不上

现象:下载的源码包里README.md写着「修改 config.py 的 COOKIE 字段」,但解压后发现配置文件叫settings.py,或者字段名是COOKIE_LIST。原因:作者迭代过程中改了文件名或变量名,文档没同步。解决:先全局搜索Cookie关键词,找到实际读取 cookie 的代码位置,再反推配置文件需要改哪个字段;同时看requirements.txt的依赖版本,pyecharts 从 1.x 升到 2.x 后Tree的接口有破坏性变化,如果你本地装的 pyecharts 版本和源码包要求的版本不同,优先把本地虚拟环境按requirements.txt重建,而不是手动改代码。

6. 让树图真正能汇报:把静态 HTML 变成可交互的数据分析看板

只交一张静态树图,业务方看完会问「能点开看明细吗」。我的常用做法是把树图嵌进一个简单的 Flask 服务,节点点击后跳转到对应主题下的原始微博列表,这样汇报时可以从主题下钻到单条内容,说服力强很多。实现不复杂:Flask 暴露两个路由,一个是首页渲染lda_tree.html,另一个是/topic/<topic_id>返回该主题下权重最高的原始微博文本。注意返回微博文本时把用户昵称和发布时间附上,业务方真正关心的是「谁在说」和「什么时候说的」,不只是「说了什么」。

from flask import Flask, render_template, jsonify app = Flask(__name__) # 预处理阶段把每条微博的 topic_id 也存下来 # 这里用演示数据示意:按主题过滤原始微博 @app.route("/topic/<int:topic_id>") def topic_detail(topic_id): df = pd.read_csv("weibo_with_topic.csv", encoding="utf-8") subset = df[df["topic_id"] == topic_id] results = subset.nlargest(10, "reposts_count")[ ["user", "created_at", "clean_text"] ].to_dict(orient="records") return jsonify(results) if __name__ == "__main__": app.run(port=8000, debug=False)

树图节点点击跳转的联动,需要在生成的 HTML 里注册click事件,pyecharts 支持Tree.add后通过tree.on绑定,也可以在 HTML 生成后用原生 JS 监听事件并跳转。这个联动看似简单,但对演示效果提升极明显:主题词不再只是「词」,而是通向原始证据的入口。数据量大了以后,再加一步——每类主题下的微博数量做成柱状图放在旁边,树图管结构,柱状图管数量,一屏就能讲完整。

我个人的习惯是把以下文件固定保留:原始 JSON、清洗后的 CSV(含分词结果)、LDA 模型文件、树图 HTML、文档说明中提到的配置清单。做分析类项目最怕的就是重跑一次忘参数,下次调起来等于从零开始。希望这篇笔记帮你少踩几个坑,也把这套方案真正跑成自己手里能复用的工具。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询