Python爬虫实战:微博数据采集与情感分析词云可视化

发布时间:2026/9/9 6:47:48
Python爬虫实战:微博数据采集与情感分析词云可视化 1. 项目背景与整体方案设计爬虫这个老话题这几年被AI大模型一冲又要被重新拿出来聊了。很多时候你缺的不是模型而是喂给模型的数据——尤其是带有真实情绪的文本数据。张雪峰的微博就挺有代表性的他的动态里有教育观点、职业选择建议、生活日常甚至偶尔带点调侃和吐槽文本风格分明情感色彩非常清晰拿来做情感分析和词云可视化再合适不过。这个项目整体走一条非常标准的链路requests模拟请求爬取微博内容pandas做数据清洗jieba分词后分别接SnowNLP情感分析和wordcloud词云绘图。整个过程不依赖Selenium这类重型浏览器工具单纯靠接口请求加JSON解析就能完成门槛不高但能把Python爬虫的核心底层逻辑完整练一遍。整条链路完成后你会得到三样东西一份干净的微博文本数据集、一个能判断每条微博情感倾向的评分结果、一张反应高频关键词的可视化词云图。这套玩法换个目标账号照样能跑本质上是一个可以复用的采集-清洗-分析-展示模板。1.1 为什么选择微博接口而非页面解析微博网页版的反爬强度属于中等偏上级别如果你直接请求weibo.com的HTML页面会发现一大半内容靠JS动态渲染首页源码里只有空壳框架。反而移动端的m.weibo.cn服务的是手机浏览器场景为了省流量数据直接以JSON格式下发给前端这就省掉了渲染这一步截获数据格外方便。所以在实际项目中爬取微博的首选方案永远是找移动端接口而不是去啃网页源码。移动端接口返回的JSON结构稳定字段命名清晰解析成本低。更重要的是接口需要的数据量远小于整页HTML对服务器资源占用也更小请求成功率反而更高。当然有得必有失移动端接口需要携带有效的Cookie才能访问否则服务端会直接返回401。Cookie的获取方式我放在2.2节专门讲这里你先记住一个核心结论只要拿到了Cookie整个微博的数据接口基本就等于放开了一半。1.2 技术栈选型与各模块职责划分这个项目用到五个核心库每个库解决一个具体问题分工非常明确模块库负责工作网络请求requests发起HTTP请求获取接口数据数据处理pandas存储、清洗、筛选、统计微博数据中文分词jieba将中文长文本切成有意义的词语情感分析snownlp对每条文本输出情感倾向评分数据可视化wordcloud matplotlib生成词云图并控制图片样式我特意没有引入Scrapy这样的大型框架原因很简单这个项目单账号、单任务、数据量在几百到几千条之间用Scrapy属于杀鸡用牛刀。requests配合一个for循环足够了而且逻辑直白初学者一眼能看懂。等以后数据量到了一定规模你再往Scrapy或分布式爬虫方向迁移核心思路是一样的。1.3 环境准备与依赖安装整个项目基于Python 3.8以上版本推荐用3.10或3.11太老的版本会有一些依赖兼容性问题。装依赖的速度取决于你的网络状况建议直接用国内镜像源加速pip install requests pandas jieba snownlp wordcloud matplotlib -i https://pypi.tuna.tsinghua.edu.cn/simple装的时候有个细节值得注意snownlp这个库的依赖非常轻基本不会报错。真正容易出问题的是wordcloud它依赖Pillow处理图像如果你用的是精简版Python或虚拟环境Pillow可能没被一起装进去。遇到ModuleNotFoundError时报什么缺什么就是千万别慌。全部库装完以后在Python交互环境里跑一下确保版本正常import requests, pandas, jieba, snownlp, wordcloud print(所有依赖就绪)到这里环境准备就收工了。接下来进入正题我们从微博接口开始逐步把数据抓下来。2. 微博数据采集从接口分析到JSON解析爬虫最基础也最关键的能力不是写代码而是能看懂目标网站的数据交互逻辑。微博移动端的核心接口是https://m.weibo.cn/api/container/getIndex这个接口用你当前浏览的容器ID来定位数据流返回一段包含全部博文的JSON数据结构。2.1 微博移动端接口结构与参数分析这个接口有两个核心参数需要理解containerid容器ID相当于微博数据流的唯一标识。个人主页的containerid通常是107603加上用户UID你可以把UID在个人主页URL里找到然后拼接出来。since_id分页游标。微博不像传统分页那样用page2、page3而是用一串加密的游标字符串。第一页请求不传这个参数返回的JSON里会带出下一页的since_id只要把它拿出来拼进第二页请求就能实现连续翻页。接口返回的JSON里数据主体在data.cards数组中每一条card是一个微博对象。真正需要的文本内容藏在card.mblog.text字段里这里面是HTML格式的正文。另外mblog.created_at是发布时间mblog.attitudes_count是点赞数mblog.reposts_count是转发数这些都是后续分析能用到的信息。2.2 请求头伪装与Cookie模拟登录要稳定拿到数据请求头必须伪装成真实手机浏览器。微博服务端会校验User-Agent、Referer等基础信息如果不符合移动端特征请求会被直接拒绝。我是这样构造请求头的headers { User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1, Referer: https://m.weibo.cn/, X-Requested-With: XMLHttpRequest }Cookie的获取方式很简单手机浏览器或者电脑浏览器的开发者工具里打开微博移动版网页随意浏览片刻在Network面板里找到任意一条接口请求把请求头里的Cookie字段完整复制出来。粘贴到代码里即可但要注意Cookie有有效期短则几个小时长则几天过期后要重新获取。我用Cookie参数的方式管理cookies { Cookie: 粘贴你的Cookie字符串 }这里提醒一句项目代码里不要硬编码Cookie更不要发到公开仓库里。Cookie本质等同于你的身份凭证泄露了别人就能用它操作你的账号。实际项目中建议放在环境变量里读取时用os.getenv(WEIBO_COOKIE)。2.3 完整爬虫代码分页抓取与字段清洗先确定目标用户的UID。这个项目的目标账号是张雪峰你在微博里搜索到他本人账号后看主页URL里的数字串就是UID。以下代码以占位符YOUR_UID代替你把它替换成实际数字即可。import requests import pandas as pd import time import re UID YOUR_UID CONTAINER_ID f107603{UID} BASE_URL https://m.weibo.cn/api/container/getIndex headers { User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1, Referer: https://m.weibo.cn/, X-Requested-With: XMLHttpRequest } cookies { Cookie: 粘贴你的Cookie字符串 } def clean_html(text): 去掉微博HTML正文里的标签和多余字符 text re.sub(r[^], , text) text re.sub(rhttps?://\S, , text) text re.sub(r#, , text) return text.strip() all_data [] since_id for page in range(1, 101): params { containerid: CONTAINER_ID, since_id: since_id } resp requests.get(BASE_URL, headersheaders, cookiescookies, paramsparams, timeout10) if resp.status_code ! 200: print(f第{page}页请求失败状态码{resp.status_code}) break data_json resp.json() cards data_json.get(data, {}).get(cards, []) for card in cards: if card.get(card_type) ! 9: continue mblog card.get(mblog, {}) text mblog.get(text, ) row { 发布时间: mblog.get(created_at, ), 正文: clean_html(text), 点赞数: mblog.get(attitudes_count, 0), 评论数: mblog.get(comments_count, 0), 转发数: mblog.get(reposts_count, 0) } if row[正文]: all_data.append(row) since_id data_json.get(data, {}).get(since_id, ) if not since_id: print(f第{page}页后无更多数据) break print(f第{page}页抓取完成累计{len(all_data)}条) time.sleep(1 len(all_data) % 3)这段代码有几个核心设计点值得逐个拆开讲。第一card_type必须筛一遍。接口返回的cards数组里混合着用户推荐、热搜卡、话题卡等不同类型的卡片只有card_type 9才是真正的微博卡片不筛的话后面清洗会非常痛苦。第二clean_html函数把HTML标签、链接、话题符全部去掉。微博正文里带着大量嵌入标签和URL直接用于情感分析分词这些杂讯会干扰统计结果。链接要去掉是因为短链接URL对语义分析毫无价值还容易把词云搞出一堆乱码。第三翻页终止条件做了双保险。一个是since_id为空时自然终止另一个是加上页数上限100页做兜底防止某些异常情况下进入死循环。第四time.sleep(1 len(all_data) % 3)是延迟策略。请求间隔在1到3秒之间波动避免固定间隔被服务端识别为脚本特征。再加一个随机偏移效果更好更具体的方式可以看5.1节。抓完后做事后参数检查print(f总共抓取 {len(all_data)} 条微博) df pd.DataFrame(all_data) df.to_csv(zhangxuefeng_weibo.csv, indexFalse, encodingutf-8-sig) print(df.head())编码选择utf-8-sig有个讲究不加-sig后缀Excel直接打开CSV会乱码。加上以后Excel能正确识别UTF-8的BOM头中文不乱。这是从踩坑里总结出来的细节新手尤其容易忽视。2.4 数据清洗预处理从原始文本到分析语料抓下来的原始数据虽然已经去了HTML标签但直接拿去做分词和情感分析还是不够干净。还要再走一轮预处理。df pd.read_csv(zhangxuefeng_weibo.csv) print(f原始数据量{len(df)}) df df.drop_duplicates(subset[正文]) print(f去重后数据量{len(df)}) df df[df[正文].str.len() 10] print(f过滤过短正文后数据量{len(df)}) df df.reset_index(dropTrue) df.to_csv(zhangxuefeng_clean.csv, indexFalse, encodingutf-8-sig)去重是必须的微博转发、置顶、编辑重发都会造成正文重复。过滤掉少于10个字符的短文本也很实用——微博经常有转发微博这类无意义转载太短的文本信息量太低还会干扰后续统计。这轮清洗完之后数据质量已经可以支撑分析了。你可以在这一步多看几眼数据感受一下张雪峰微博的整体文字风格方便后面分析结果时做交叉验证。3. 情感分析用SnowNLP判断每一条微博的情绪情感分析的目标是回答一个看似主观的问题这条微博传递的情绪是正面的、负面的还是中性的机器靠什么判断情绪靠的是大量已标注语料训练出来的概率模型。SnowNLP就是这样一个开箱即用的情感分析工具它对每段文本输出一个0到1之间的倾向分值。3.1 情感分析与分词的衔接逻辑中文文本和英文文本的最大区别是中文句子没有天然空格分隔单词。要判断整句话的情感倾向得先把句子切成有意义的词语再交给模型分析每个词的贡献。这个小步骤就是jieba分词的核心作用。SnowNLP本身自带分词器但它内嵌的词典偏通用对微博这种含大量网络用语、缩写和专有名词的短文本支持一般。都交给模型本身处理准确率会掉下来。所以这里我选择先用jieba对文本做预处理把清洗后的语料统一成标准格式再喂给SnowNLP。在微博场景里jieba还常会用到一个技巧就是加载自定义词典。比如你发现张雪峰被切成张/雪峰了这个词组本来是一个人名拆开后语义就飘了。自定义词典能告诉jieba哪些词是整体单位import jieba jieba.add_word(张雪峰) jieba.add_word(考研) jieba.add_word(志愿填报)有需要的话你还可以把专业术语、目标人物的名字、领域名词都加进去。这对后续分词的准确性影响非常大是文本分析里最容易忽略又最划算的优化手段。3.2 基于SnowNLP的逐条情感评分与统计核心处理逻辑非常简洁from snownlp import SnowNLP def sentiment_score(text): 返回0到1的情感分值越接近1越正面 try: s SnowNLP(text) return s.sentiments except Exception: return 0.5 df pd.read_csv(zhangxuefeng_clean.csv) df[情感分] df[正文].apply(sentiment_score) df[情感类别] pd.cut(df[情感分], bins[0, 0.4, 0.6, 1.0], labels[负面, 中性, 正面])pd.cut是边界划分函数把0到1的连续分值切成三段。这里我选择了0.4和0.6作为分割线比四舍五入的0.5分割更有容错空间避免把中间地带模棱两可的文本强行归到正或负。处理完成后对结果做整体统计counts df[情感类别].value_counts() print(counts) print(f平均情感分{df[情感分].mean():.4f}) positive_texts df[df[情感类别] 正面][正文].head(5) negative_texts df[df[情感类别] 负面][正文].head(5) print(正面文本示例) for t in positive_texts: print(-, t[:50]) print(负面文本示例) for t in negative_texts: print(-, t[:50])从输出结果里你不但能看到整体情感分布还能抽查几条典型文本看模型分类对不对。这一步是必要的人工质检判断模型输出是否和你的阅读感受一致。如果偏差过大说明这个目标账号的文本风格超出了SnowNLP默认模型的训练覆盖范围这时可以用我后面提到的方式来做针对性优化。3.3 SnowNLP的局限性与应对策略SnowNLP默认模型是用电商评价语料训练的优点是我们完全不用训练就能直接用缺点是它对一些特定场景的文本语义理解会有偏差。比如对反讽、反话、调侃、网络热梗模型经常判断不准。微博恰恰是反讽和调侃的重灾区张雪峰本人也经常用自嘲和夸张语气。所以SnowNLP跑出来的结果仅供参考维度不能当做绝对真理。我的经验是不要试图100%精确情感分析的产出重点在量级趋势和分布特征上单条精确度反而不重要。如果你的目标账号确实出现了大面积判断异常有两个优化思路。第一个思路是准备几百条人工标注好情感倾向的微博用SnowNLP自带的train方法做模型微调。第二个思路是直接改用大模型接口做情感分类拿AI来分析语义成本可控且准确率高把接口返回结果接进现有pipeline即可。实际上把大模型API接入爬虫项目现在的应用正在变得越来越普遍。4. 词云可视化把高频词变成一张好看的图词云是一个极其直观的文本分析工具它能通过字号大小展示哪些词出现频率高。制作的底层逻辑很简单先统计高频词再用绘图库把词按照频次画出来。4.1 基于jieba的高频词统计与停用词过滤词云的原料不是整句文本而是分词后的词频统计。虽然jieba自带分词器已经能切出词来但不做过滤的话词云里会挤满我们这个一个这类没有信息量的停用词真正有价值的关键词反而排不上名次。标准做法是准备一个停用词表。不用你自己写网上有公开的中文停用词表常见的分词项目里都带。把它加载进来进行过滤import jieba from collections import Counter with open(stopwords.txt, r, encodingutf-8) as f: stopwords set([line.strip() for line in f]) def extract_keywords(text): words jieba.lcut(text) results [] for w in words: w w.strip() if not w: continue if w in stopwords: continue if len(w) 2: continue results.append(w) return results df pd.read_csv(zhangxuefeng_clean.csv) all_words [] for text in df[正文]: all_words.extend(extract_keywords(text)) word_counts Counter(all_words) top_words word_counts.most_common(200) print(top_words[:30])过滤规则有三道停用词列表直接滤掉无意义虚词长度小于2的截掉单字strip()清掉空白。most_common(200)保留频次前200位因为词云上画太多词反而显得拥挤杂乱。要注意的一点是jieba.lcut默认模式会做全切分也就是输出所有可能的词。在统计高频词这个场景里最好用jieba.lcut而不是jieba.lcut_for_search前者更贴近人对词语的划分直觉统计出来的词频更符合阅读习惯。还有一个小技巧是如果某个词频繁出现在词云里但你并不想突出它直接在停用词表里加上这个词就行。4.2 WordCloud参数详解与中文字体配置词云库本身是一个通用工具核心在于参数配置。我通常用下面这组参数from wordcloud import WordCloud import matplotlib.pyplot as plt font_path C:/Windows/Fonts/simhei.ttf wc WordCloud( width1600, height900, background_colorwhite, font_pathfont_path, max_words200, max_font_size100, random_state42, collocationsFalse, ) wc.generate_from_frequencies(dict(top_words)) plt.figure(figsize(16, 9)) plt.imshow(wc, interpolationbilinear) plt.axis(off) plt.show()每个参数的选择都有自己的理由width1600, height900设置画布尺寸横版16:9是社交媒体分享最常用的比例分辨率够高打印也不糊。background_colorwhite默认黑色背景太突兀白底适合绝大多数内容场景如果你要生成深色背景改成black即可。font_path中文字体必配。wordcloud默认字体是DroidSansMono根本不包含中文字形不指定中文字体的话所有中文都会显示成方框。Windows系统可以直接用simhei.ttf黑体macOS用/System/Library/Fonts/PingFang.ttc。collocationsFalse这个参数很隐蔽但很关键。它控制是否统计二元词组默认True会把考研和调剂这种搭配组成bigram导致词云里出现考研 调剂这样的双词条。设为False后只显示单个词语视觉效果更清爽。random_state42固定随机种子让每次生成的词云布局一致。如果去掉同一个词频表每次运行都会产生不同排列不利于反复调参对比。generate_from_frequencies是直接接收词频字典的方法。要注意的是这时候不要再调用generate(text)否则会重复做一次分词统计完全打乱你刚才辛苦处理好的词频表。4.3 词云结果解读与项目产出展示我实跑后的词云效果整体视觉上高频词会非常突显。教育类账号的词云一般是什么感觉考研专业学校孩子家长工作这一类关键词会占据中心区域字号越大代表词频越高。把词云和之前的统计结果合在一起可以输出一份结论性强的内容情感分布比例体现了这个账号整体表达风格的积极程度词云则直观呈现了他反复强调的主题。两者一对照你就能回答这个人的微博整体在传递什么信号这个原本很主观的问题。生成的词云图保存下来推荐直接存成PNG格式通用且画质好wc.to_file(weibo_wordcloud.png)注意一点matplotlib的plt.show()在无图形界面的环境Linux服务器、Docker容器里会卡住需要设置matplotlib.use(Agg)或者是直接调用wc.to_file()保存图片即可不一定非得弹窗预览。5. 常见问题与避坑指南爬虫类项目的坑大多在工程层面不在算法层面。这几类问题我实际跑的时候都遇到过用排查表的形式整理出来方便你对照处理。5.1 反爬虫拦截的应对策略报错现象可能原因解决方案返回403 Forbidden请求头不像移动端被拒绝检查User-Agent是否完整补Referer头返回401 UnauthorizedCookie缺失或已过期重新获取Cookie设置环境变量管理返回频繁请求接口上限请求频率过高增加延时每次随机2-5秒数据突然全为空UID填错/容器ID不对检查containerid拼接规则实际中遇到最多的就是403和401。403通常是请求头问题换一个真实的手机UA就能解决。401基本可以断定是Cookie过期微博的Cookie有时效性短则几小时长则一周过期后去浏览器里重新复制一份就行。还有一个容易被忽略的点请求频率不能看起来像机器。固定每2秒一次反而容易被封因为人的操作不可能这么恒定。我实际项目里用随机延时import random time.sleep(random.uniform(1.5, 4.0))每次请求间隔在1.5到4秒之间随机浮动这比固定延时触发风控的概率低很多。如果你要爬的数据量大还可以每天分批次抓取第一天抓前几十页第二天换上新的Cookie接续抓后面的页数。这类分批任务里Cookie过期管理是需要优先考虑的。5.2 中文编码、字体与词云显示问题写入CSV用encodingutf-8-sig否则Excel打开乱码词云中文全变方框检查font_path是否指向真实存在的中文字体文件分词结果全是单字确认是否把jieba.lcut的返回值误当字符串使用词云出现我们这个等无意义词停用词表没加载加上即可这组问题没有技术深度但卡住新手的时间往往最长。尤其是中文方框问题很多人以为是wordcloud没有正确安装折腾半天重装好几遍其实就缺一个font_path参数值。5.3 微博接口升级后的适配思路微博接口属于经常调整的典型。容器ID格式可能变、返回字段可能改名、甚至整个接口路径都可能换掉。代码写完后不能一劳永逸随时要有调试排查的思想准备。接口一旦失效正确的排查顺序是先在浏览器里手动打开移动端微博找到目标用户主页打开开发者工具的Network面板刷新页面观察实际请求了哪个接口、返回了什么结构、字段名叫什么。拿到这些信息后再回来改你的代码里的URL、参数名、解析字段。任何爬虫代码都不可能永远有效掌握调试节奏比记住一套固定写法更重要。5.4 运行效率与数据规模评估单账号、几百条数据这套脚本跑完大概只要几十秒到几分钟。主要的耗时瓶颈在sleep延迟上为了降低被屏蔽的概率这个时间不能省。如果你数据量上万条建议改成多线程并发加随机延时同时做好Checkpoint断点记录每隔固定条数就存一次中间结果防止中途断网导致一切重来。另外一个值得说的效率点是内存管理。单账号解析JSON时pandas DataFrame能轻松承载几万条数据完全不用担心。但你如果同时爬多个账号建议爬完一个立即存盘然后释放内存下面用del df加上gc.collect()清掉无用对象避免内存无限增长。5.5 拓展思路大模型如何融入现有流程把已经抓下来的文本数据交给大模型做更深度的分析是眼下热度非常高的玩法。比如你可以对清洗后的每条微博生成一句话摘要或者对账号整体的内容风格做画像描述再或者直接从文本里抽取关键观点这些任务传统NLP做起来很费劲但大模型接口几乎是零成本实现。我这个项目的下一个扩展方向就是把情感分析这一环从SnowNLP换成大模型的Few-shot提示词方案让模型直接输出情感标签和理由准确率会比默认模型高出不少。再加上AI Agent的理念让爬虫具备自动调度策略那这个项目就可以从一个简单的教学Demo进化成真正意义上的AI爬虫工具。我个人在实际操作中的体会是这类项目最有价值的地方不在于技术复杂度而在于它把采集、清洗、分析、可视化整条链路打通了。每一环单独拿出来都有无数教程但能从头到尾跑一遍并产出可用结果才是真正把知识转成技能的关键一步。最后再分享一个小技巧词云图的背景色和配色一定要根据内容场景来定教育类内容用浅色底深色字效果远比花哨的颜色方案好得多这个细节决定了最终产出是作品还是作业截图。