
简介基于Python构建的微博数据抓取与分析可视化项目源码定位为毕业设计、期末大作业和课程设计的完整参考方案解决从微博数据采集、文本清洗到主题建模与可视化呈现的全流程需求。包内共53个文件以Python脚本、HTML交互页面和PNG图像为主要类型辅以模型文件、词典、CSV数据及说明文档压缩包整体约66.36MB。项目模块划分清晰覆盖文本预处理、中文简繁转换、TF-IDF聚类、Word2Vec相似度计算、情感词典分析等环节并生成LDA主题树图、地图、饼图、3D柱状图等可视化页面便于直观展示分析结论。代码内附详细注释与使用说明简单部署即可运行也可按需替换数据源或调整模型参数适合需要实际项目代码作为参考的Python学习者。目前已有181人学习使用。1. 从一条微博到一张主题树图这套源码解决什么问题做社交媒体数据分析的同行应该都有同感微博数据是中文文本分析里最“拧巴”的语料之一短文本、口语化、表情符号和话题标签混杂直接拿通用分词和主题模型去跑得到的往往是一堆让人哭笑不得的主题词。而LDALatent Dirichlet Allocation这类主题模型恰恰又对输入文本的质量极其敏感预处理差一点后面整个主题分布就跑偏。这也正是“基于python抓取微博数据并对微博文本分析和可视化项目源码-LDA树图”这类项目存在的意义它把爬取、清洗、建模、可视化串成一条完整的流水线最终输出一张能直接读懂的树图——根节点是“某条微博的主题归属”子节点是“主题背后的关键词权重”让非技术背景的运营同事也能一眼看出这批微博在聊什么。本文我会从数据采集讲起把LDA建模里最容易踩的坑和树图可视化的实现细节逐一拆开适合已经会基础Python、想完整跑通一个微博文本分析项目的读者。2. 微博数据采集接口选择、登录态管理与存储落地2.1 抓微博到底走哪个接口m.weibo.cn的移动端API为什么是首选微博网页端has a strong反爬策略验证码和风控升级频繁而移动端接口m.weibo.cn相对稳定返回的是结构化JSON字段覆盖微博正文、发布时间、点赞数、转发数、评论数、用户信息等对文本分析项目来说足够用了。常见做法是用requests库模拟登录后的Cookie去请求时间线或搜索接口而不是去解析复杂的HTML页面。import requests import json # 登录微博网页版后从浏览器开发者工具中复制Cookie值 COOKIE 你的登录Cookie HEADERS { User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X), Cookie: COOKIE, X-Requested-With: XMLHttpRequest } def fetch_weibo_page(keyword, page): 按关键词搜索微博返回单页JSON数据 keyword: 搜索关键词 page: 页码每页约20条 url https://m.weibo.cn/api/container/getIndex params { containerid: f100103type1q{keyword}, page_type: searchall, page: page } resp requests.get(url, headersHEADERS, paramsparams, timeout10) if resp.status_code ! 200: return None return resp.json() data fetch_weibo_page(防溺水, 1) if data and data.get(data) and data[data].get(cards): for card in data[data][cards][:3]: try: text card[mblog][text] print(text[:50]) except (KeyError, TypeError): continue这段代码的关键在于Cookie必须保持新鲜。微博的Cookie通常几小时到一天内有效过期后接口会返回414状态码或跳转登录页。参数说明containerid里的100103type1q是搜索类型的固定前缀q值为URL编码后的关键词page_typesearchall表示全类型搜索如果你只想要热门微博可以换成hot。实际使用中不要连续高频率请求同一个关键词最稳妥的策略是每页请求间隔2到3秒并在收到414或非200响应时自动暂停、等待Cookie更新。2.2 采集任务的工程化多关键词轮询、去重与进度续跑做一次有说服力的微博文本分析单关键词、单页数据远远不够一般需要按多个关键词分别采集几百到上千条。这里有两个工程问题必须处理请求频率控制和断点续采。频率控制前面提过断点续采则需要把“已采集的微博ID”持久化避免脚本中断后重复抓取同一批数据。import sqlite3 import time import random conn sqlite3.connect(weibo.db) conn.execute(CREATE TABLE IF NOT EXISTS weibo_post (mid TEXT PRIMARY KEY, text TEXT, created_at TEXT, reposts_count INT, comments_count INT, attitudes_count INT)) conn.commit() def save_weibo(post_list): saved 0 for p in post_list: mid str(p.get(mid, )) text p.get(text_clean, ) if not mid or not text: continue try: conn.execute( INSERT INTO weibo_post (mid, text, created_at, reposts_count, comments_count, attitudes_count) VALUES (?,?,?,?,?,?), (mid, text, p[created_at], p.get(reposts_count, 0), p.get(comments_count, 0), p.get(attitudes_count, 0)) ) saved 1 except sqlite3.IntegrityError: pass # 主键冲突说明已存在跳过 conn.commit() return saved keywords [防溺水, 校园安全, 暑期安全] for kw in keywords: for page in range(1, 5): data fetch_weibo_page(kw, page) # 解析cards中的mblog字段 posts [] if data and data.get(data, {}).get(cards): for card in data[data][cards]: if card.get(card_type) ! 9: continue mblog card.get(mblog, {}) # 提取纯文本去掉HTML标签和微博表情代码 import re html_text mblog.get(text, ) plain_text re.sub(r.*?, , html_text) plain_text plain_text.replace(\\u200b, ).strip() posts.append({ mid: mblog[mid], text_clean: plain_text, created_at: mblog.get(created_at, ), reposts_count: mblog.get(reposts_count, 0), comments_count: mblog.get(comments_count, 0), attitudes_count: mblog.get(attitudes_count, 0) }) print(f关键词 {kw} 第{page}页新增 {save_weibo(posts)} 条) time.sleep(random.uniform(2, 5)) # 随机间隔降低风控概率 conn.close()这里把数据存进了SQLite以mid作为主键天然去重。字段里保留了转发数、评论数和点赞数是为后续做“重要度加权”准备的——不是所有微博对主题的贡献都一样交互量高的微博往往更能代表公众关注焦点。如果微博规模大可以用MySQL或MongoDB替换SQLite但字段设计保持一致。注意直接把HTML文本存入数据库是大忌因为a href、图片链接和表情符号会污染后续分词结果入库前必须做HTML标签剥离上面的re.sub就是干这个的。2.3 语料质量筛选什么样的微博该留下什么样的该扔掉原始数据里充斥着大量“无效微博”纯转发无原创内容、只含一个链接的营销博、标点符号占比过高的水帖。这些噪声会直接拉低LDA模型的区分度。常见做法是设置三条筛选规则过滤后正文长度小于30个字符的不要去重后正文完全相同且超过5条的批量营销微博剔除含有明显广告关键词如“加V”“私信”“点击链接”的标记排除。# 筛选有效语料 MIN_LEN 30 AD_KEYWORDS [加V, 私信, 点击链接, 抽奖, 代购] def filter_valid_posts(text): if len(text) MIN_LEN: return False if any(kw in text for kw in AD_KEYWORDS): return False # 标点符号占比超过40%视为噪声 punct_count sum(1 for ch in text if not ch.isalnum() and not \u4e00 ch \u9fff) if punct_count / max(len(text), 1) 0.4: return False return True按这个逻辑筛完通常能过滤掉20%到30%的原始数据。在做LDA之前建议把筛选后的语料按“单条微博一行”的格式导出为纯文本文件后续用gensim读取时会更方便。别忘了保存一份清洗前后的统计量——比如清洗前总条数、清洗后有效条数这个数字写进分析报告里能体现你的数据质量把控能力。3. 文本预处理与LDA建模主题数、超参数和Gensim实操3.1 中文分词和停用词表影响主题可解释性的第一关微博文本的分词难点在于网络新词和口语化表达比如“yyds”“绝绝子”“破防了”通用词典往往切得稀碎。常见做法是把jieba的默认词典和自定义词典结合把业务领域词提前加进去。针对“防溺水”这个场景我会把“溺水”“游泳圈”“野泳”“救援”“急救”等词加进自定义词典。分词后的停用词处理同样重要微博自带的话题词#防溺水#和常用虚词“了”“的”“是”删不干净主题词就会被这些高频噪声绑架。import jieba import jieba.analyse # 自定义词典每行一个词可指定词频和词性 custom_words [溺水, 野泳, 游泳圈, 救援, 急救, 安全教育, 暑期] for w in custom_words: jieba.add_word(w) STOP_WORDS set() with open(stopwords.txt, r, encodingutf-8) as f: STOP_WORDS.update([line.strip() for line in f]) # 额外添加微博特殊符号和通用噪声 STOP_WORDS.update([http, 微博, 转发, 分享, 图片, 链接]) def tokenize(text): words jieba.cut(text) return [w for w in words if w.strip() and w not in STOP_WORDS and len(w) 1]选停用词表时不要直接照搬网上的通用中文停用词表需要针对微博文本做增量。我的习惯是先跑一次全量分词把出现频率前100的词打印出来人工过一遍把与主题无关的高频词补充进停用词表这样迭代两轮后分词质量会明显改善。jieba.add_word的第三个参数tag可以指定词性比如动词、名词默认情况下不传也能正常工作。3.2 构建词典与向量语料Gensim的数据管线干净的分词结果要转成Gensim能消费的格式标准管线是分词列表 →dictionary词频统计 →bow向量。这里有两个参数直接影响建模效果no_below和no_above分别过滤出现在文档数过少和过多的词。前者删掉只在个别微博里出现的低频噪声后者删掉在几乎所有文档里都出现的通用词。from gensim.corpora import Dictionary from gensim.models import LdaModel from gensim.models.coherencemodel import CoherenceModel import pickle # 读取每行一条微博的清洗文本 with open(weibo_corpus.txt, r, encodingutf-8) as f: docs [line.strip() for line in f if line.strip()] tokenized_docs [tokenize(doc) for doc in docs] dictionary Dictionary(tokenized_docs) # 保留在至少2篇文档中出现、且在50%以下文档中出现过的词 dictionary.filter_extremes(no_below2, no_above0.5) dictionary.compactify() corpus [dictionary.doc2bow(tokens) for tokens in tokenized_docs] print(f词典规模: {len(dictionary)}文档数: {len(corpus)}) # 保存中间产物方便复现 dictionary.save(weibo_dict.dict) with open(corpus.pkl, wb) as f: pickle.dump(corpus, f)filter_extremes的两个参数需要按语料规模调整。如果总共采集了500条微博no_below2是合理下限如果采集了5000条可以考虑no_below5。no_above0.5表示去掉在超过50%文档中都出现的词微博里常见的“大家”“我们”这类词即使停用词表漏了这个过滤器也能兜底。compactify()是必须调的否则删除词项后序号不连续后续LDA训练会出错。3.3 LDA主题数怎么定困惑度不够看Coherence才靠谱LDA最让人头疼的就是选择主题数K。初学者常用的困惑度Perplexity曲线在实际中文短文本上基本是个“永远下降”的单调曲线看拐点是玄学。我一般以一致性分数Coherence Score为主困惑度为辅K值从5到20逐个跑选择一致性分数高的K值同时人工看一眼各主题词能否解释得通。num_topics_range range(5, 21) coherence_scores [] for k in num_topics_range: lda LdaModel( corpuscorpus, id2worddictionary, num_topicsk, passes20, alphaauto, # 让模型自己学文档-主题分布 etaauto, # 让模型自己学主题-词分布 random_state42 # 固定随机种子保证复现 ) cm CoherenceModel(modellda, textstokenized_docs, dictionarydictionary, coherencec_v) coherence_scores.append((k, cm.get_coherence())) print(fK{k}, Coherence{cm.get_coherence():.4f}) # 选出coherence最高的K best_k max(coherence_scores, keylambda x: x[1])[0] print(f最佳主题数: {best_k})这里用到了alphaauto和etaauto让模型自动学习两个Dirichlet分布的超参数比手动设置更贴合微博这种分布不均匀的语料。passes20是训练轮数语料小时设20轮足够语料大时可以调低到10轮以节省时间。random_state必须固定否则每次运行结果都会变没法向别人复现你的分析结论。最后训练时注意打印每个主题的前15个词逐个人眼检查可解释性——如果出现“同一个词出现在多个主题里且权重类似”说明这批主题区分度不足要么减少K值要么回源头加强停用词过滤。4. 树图可视化把LDA主题结构变成可读的层级图4.1 树图数据结构设计主题-关键词-权重如何映射成节点LDA模型训练完成后我们得到的是每个主题下每个词的权重分布。树图的经典用法是根节点标注为“全部微博语料”往下第一层是各个主题用主题编号或主题代表性短语命名第二层是每个主题下权重最高的Top N个词节点大小或颜色映射权重值。把这个层级结构组织成一个嵌套字典再交给可视化库渲染。def build_tree_data(lda_model, dictionary, top_n10): tree_children [] for topic_id in range(lda_model.num_topics): # lda.show_topic返回[(词, 权重), ...]列表按权重降序 topic_words lda_model.show_topic(topic_id, topntop_n) word_nodes [] for word, weight in topic_words: word_nodes.append({ name: f{word} ({weight:.3f}), value: round(weight, 4), size: max(10, weight * 200) # 映射为可视化节点大小 }) # 用主题权重最高词串作为主题名 topic_label .join([w for w, _ in topic_words[:3]]) tree_children.append({ name: f主题{topic_id 1} ({topic_label}), children: word_nodes }) return { name: 微博语料主题结构, children: tree_children }树图的可视化要做“语义映射”权重不能直接作为显示值要转成可感知的视觉变量。size字段就是让高权重词在视觉上更突出映射公式max(10, weight * 200)是为了避免权重过低的词节点小到看不见。主题名称用分数最高的三个词拼接比如“防溺水游泳圈救援”比干巴巴的“主题1”要直观得多。4.2 用PyEcharts渲染树图配置项与图表导出Python生态里做树图最简单的是PyEcharts底层的Tree图表类型天然支持嵌套字典数据。中文标签需要额外设置字体族否则在部分浏览器里渲染会变成方块。导出到HTML文件后可以直接放进报告或分享给同事。from pyecharts import options as opts from pyecharts.charts import Tree tree_data build_tree_data(lda_model, dictionary, top_n10) chart ( Tree(init_optsopts.InitOpts(width1200px, height800px)) .add( series_nameLDA主题树图, data[tree_data], layoutorthogonal, # 正交布局适合层级清晰的树 symbolcircle, symbol_size8, orientLR, # 从左到右展开默认TD是自上而下 edge_shapepolyline, label_optsopts.LabelOpts( positiontop, vertical_alignmiddle, font_size12, font_familyMicrosoft YaHei ), leaves_label_optsopts.LabelOpts( positionbottom, rotate0, font_size11, font_familyMicrosoft YaHei ) ) .set_global_opts(title_optsopts.TitleOpts(title微博文本LDA主题层级树图)) ) chart.render(lda_topic_tree.html) print(树图已保存至 lda_topic_tree.html)这里layoutorthogonal和orientLR组合让树横向展开主题名和关键词在一行内完整显示避免纵向布局导致的长词截断。symbol_size8是节点直径与前面字典里的size是两套体系——一个控制视觉上的圆形标记大小一个控制标签内容展示映射关系需要手动保持协调。生成HTML后建议用浏览器打开检查中文是否正常显示右键导出图片时用高DPI模式。4.3 树图变体按交互权重扩展的圆圈图与矩形树图树图不是唯一选择。如果运营同事想看“哪个话题的互动量最高”可以用矩形树图Treemap按主题或微博的点赞数、转发数映射矩形面积如果想看“某条具体微博的完整主题路径”可以用旭日图Sunburst。核心变换就是调整传给图表的数据结构把“主题-词”层级替换成“微博-发布时间-主题-词”的导航层级。这里面常见的做法是先写一个build_data_for_dept的工厂函数根据目标图表类型返回不同嵌套结构复用LDA模型输出不重复训练模型。def build_treemap_data(lda_model, df_with_weights, group_coltopic_id, value_colinteraction_score): grouped df_with_weights.groupby(group_col).agg( total_score(value_col, sum), post_count(value_col, count) ).reset_index() return [ { name: f主题{row[topic_id]}, value: round(row[total_score], 2), children: [] # 可继续挂关键词子节点 } for _, row in grouped.iterrows() ]这个扩展函数的关键在于interaction_score的计算。我通常用1 2*reposts_count comments_count 0.5*attitudes_count这样的加权公式——转发权重要高因为转发代表传播力强点赞权重低因为点一下的成本太低。算好之后把分数按主题聚合就能画出“主题互动量分布矩形树图”和LDA树图形成互补视角。5. 微博文本分析常见坑5个让结果翻车的真实案例5.1 Cookie过期导致采集中断静默失败最难排查现象脚本运行一段时间后接口返回的JSON为空或data字段为None但程序不报错只是所有页面的增量都是0。原因Cookie失效后微博接口不再返回异常状态码而是返回一个空结构的正常响应。解决每次请求后检查返回里是否包含cards字段若为空则主动抛出异常并停机等待不要继续盲目跑。另一个实用技巧是给请求加一个时间戳缓存每隔30分钟自动从配置文件重读Cookie方便手动更新后热加载。5.2 停用词表不匹配导致LDA主题全是“的”“了”“在”现象训练出的主题词列表里前五个词全是“我们”“什么”“可以”“现在”这类无意义词汇主题之间毫无区分度。原因通用中文停用词表侧重于新闻语料对微博的口语化表达和网络用语覆盖不足。解决先跑一次词频统计按词频降序打印前200个词人工圈出与业务无关的词汇批量追加到停用词表再用dictionary.filter_extremes(no_above0.5)兜底过滤高频噪声。这个坑我踩过好几回血泪经验就是分词后必须做词频巡检别偷懒跳过。5.3 主题数K选错模型要么重复要么散架现象K5时三个主题的核心词高度重叠主题1和主题2都包含“防溺水”“游泳”K15时出现三四个完全无法解释的拼凑主题。原因K值小于真实主题数会导致主题合并K值大于真实主题数会导致把噪声切分进独立主题。解决以Coherence Score为主K值从5到20优先选c_v分数最高的同时备选分数相近的K值人工对比主题词列表选解释性强的。困惑度曲线只作参考不要用它做唯一依据。5.4 可视化中文乱码图表演示现场翻车现象树图HTML在浏览器打开所有中文全部显示为小方块或乱码。原因PyEcharts默认字体在部分操作系统或浏览器环境中对中文支持不完整。解决在LabelOpts中显式设置font_familyMicrosoft YaHeiWindows或sans-serif通用兜底同时检查HTML文件的编码声明确保是UTF-8。如果部署到Linux服务器需要安装中文字体包或用系统自带的Noto Sans CJK SC。5.5 微博文本里的“转发//用户名”污染分词结果现象分词后出现一长串“用户名一起”“用户名转发”等无意义的词对LDA主题里混入了大量陌生人名。原因微博转发文本自带“//昵称: 原文内容”的结构昵称没有从正文中剥离。解决在清洗阶段用正则把//[^:][:]整体删除只保留原文内容。注意用[^:]匹配昵称时如果昵称里包含冒号会中断匹配稳妥做法是先按//切分再处理每段。6. 进阶用主题强度加权让树图直接回答业务问题树图能看出“有什么主题”但业务方往往想知道“哪个主题更值得关注”。这时要在LDA输出上叠加一层权重计算把每条微博的主题分布、文本长度、交互数据组合成主题热度分数。做法是对每条微博用lda_model.get_document_topics拿到它的主题分布比如主题1占0.6、主题2占0.3再乘上该微博的交互权重前面算出来的interaction_score按主题维度累加就得到“主题热度”。把这个数值加进树图的节点名里例如“主题2救援方案 热度 258.4”同事一看便知优先级。def compute_topic_heat(lda_model, corpus, interaction_scores, num_topics): 计算每个主题的加权热度 corpus: 每个文档的bow向量 interaction_scores: 每条微博的交互权重评分 num_topics: 主题数 topic_heat [0.0] * num_topics for bow_vec, score in zip(corpus, interaction_scores): doc_topics lda_model.get_document_topics(bow_vec) for topic_id, prob in doc_topics: topic_heat[topic_id] prob * score return topic_heat这段代码的关键是get_document_topics返回的prob是该文档在主题上的后验概率乘上文档自身的交互权重反映“这条微博为这个主题贡献了多少关注度”。注意这条逻辑假设交互热度与文本主题是统一的实际上部分高转发微博可能是负面舆情因此更严谨的做法是把情感分析分数也乘进去。我一般用SnowNLP或预训练情感模型对清洗后文本打分把负向情感的交互热度单独拆成“负面主题关注度”这样树图上既能看热度又能看情绪方向。验证这套方案是否可靠有一个低成本做法抽10条微博人工判断主题归属再对照LDA给出的最大概率主题计算准确率。如果准确率在70%以下回到第2章调整数据筛选标准往往比调模型参数更有效。我在多个项目上验证过微博文本分析项目最花时间的永远是数据清洗阶段LDA本身的反而是最顺滑的一段。用上面的代码跑通一遍再按业务需要加情感维度就够支撑多数实际分析了。希望帮到你。本文还有配套的精品资源点击获取