
大概两个月前我刷微博的时候突然冒出一个念头那些几百万粉丝的公众人物大家对他们发的每一条内容到底持什么态度评论区吵得不可开交那整体情绪是偏正向还是偏负向一条一条看显然不现实最好的办法是写个程序把这些公开微博数据拉下来跑一遍情感分析再画一张词云让高频关键词浮出水面。于是就有了这个 Python AI 爬虫实战项目。这篇文章会把我从零到一的完整过程记录下来包括怎么确定抓取策略、怎么构造请求、怎么清洗数据、怎么做情感分析、怎么生成词云以及我踩过的几个坑。内容适合有一点 Python 基础的读者也适合想系统做一次“爬虫 数据分析”入门项目的人。你跟着走完一遍不仅能交出一个能跑的项目还会对“公开数据采集”这件事的边界有更清晰的认识。1. 项目目标与整体设计一条微博背后的数据链路这个项目要解决的核心问题很简单抓取一位微博用户我选了张雪峰的公开微博内容然后从情绪和关键词两个维度把一堆零散文本变成能够快速读懂的信息。我会把整个流程拆成四个阶段数据采集用 requests 请求微博移动端接口拿到用户发布的微博 JSON 数据。数据清洗把微博正文里的 HTML 标签、表情符号、外链等噪音去掉留下纯文本。情感分析用 SnowNLP 给每条微博算一个情感分数统计正向、中性、负向的分布。关键词可视化用 jieba 做中文分词用 WordCloud 生成词云直观看到这位博主经常讨论的话题。技术选型上我全部用 Python 生态里最常用、最稳定的库。requests 负责网络请求pandas 负责数据管理jieba 和 SnowNLP 负责中文文本处理wordcloud 负责可视化。整体结构不复杂但对“从网络到洞察”的完整链路是很好的演练。这个项目适合谁一是想入门爬虫的 Python 学习者可以借这个案例学会处理动态接口、JSON 数据和反爬基本应对二是对文本分析感兴趣的朋友可以完整看到“拿到文本之后还能干什么”的流程三是想给未来做 AI 数据分析打底的人因为情感分析本身就是自然语言处理里最基础也最常用的任务之一。我在设计时特意没上特别重的框架比如 Scrapy、Playwright 这些。原因很简单这个项目重点在“数据链路”而不是“大规模采集”用 requests 更轻量出了问题也好排查。2. 爬虫的合规边界先想清楚再动手很多爬虫教程一上来就讲怎么破解验证码、怎么维护代理池我不打算这么干。因为微博这类平台对爬虫的限制越来越严格而且相关法律法规对个人信息的保护也越来越明确。技术爱好者做项目重点是学习原理不是跟平台对抗。所以这次实战我给自己定了四条规矩只抓取目标账号公开可见的微博内容不碰私信、隐私数据。使用自己账号登录后获取的 Cookie 进行本地调试不分享、不公开。控制请求频率每页间隔 2 到 3 秒不做并发不给服务器造成压力。数据仅用于本地学习和个人分析不用于商业用途不对外传播原始数据。有人可能会问用 Cookie 不也算“绕过登录限制”吗严格来说这是你自己账号登录后的正常会话状态类似于你在浏览器里访问网页。教程中使用它只是为了模拟一个真实用户的请求上下文让服务器不会立刻拒绝访问。但它不是让大家去破解别人账号、绕过付费墙或者薅羊毛。凡是需要突破权限边界的采集我这个项目都不碰。另外提醒一点写爬虫前最好了解一下目标网站的 robots 协议和用户协议哪怕只是知道有这么回事。我见过很多人一上来就写分布式爬虫最后账号被封才发现问题真的没必要。在这个项目里我选择的是微博移动端的 JSON 接口。它比直接解析 PC 端 HTML 要稳定得多数据结构清晰省去大量正则匹配的工作。后面我会详细讲。3. 微博数据抓取从浏览器到结构化表格3.1 先找到目标用户的 uid要抓张雪峰的微博第一步是拿到他的数字 uid。这个 uid 是微博用户的唯一标识很多接口参数都依赖它。操作很简单用手机浏览器打开微博网页版m.weibo.cn搜索“张雪峰”进入他的个人主页。这时候地址栏里的 URL 会包含一串数字类似https://m.weibo.cn/u/1234567890后面的那一串就是 uid。如果你是在https://weibo.com上打开的URL 可能长得比较乱但只要找到 profile 后面的数字就可以。把 uid 先存到代码变量里后面所有请求都要用它。3.2 环境准备与依赖安装我推荐用虚拟环境管理依赖避免污染系统 Python。创建虚拟环境后先安装基础依赖pip install requests beautifulsoup4 pandas jieba snownlp wordcloud matplotlib这些库的用途我列了一个表库用途requests发送 HTTP 请求获取接口数据beautifulsoup4清洗微博正文里的 HTML 标签pandas结构化存储和分析数据jieba中文分词为词云做准备snownlp中文情感分析给文本打情绪分wordcloud生成词云图matplotlib绘图和字体配置如果你平时用 Jupyter Notebook也可以直接在 Notebook 里敲命令安装。这里要注意SnowNLP 和 WordCloud 在某些环境下需要锁版本比如pip install snownlp0.12.4否则可能有接口变化。我实际操作时用的就是 0.12.4。3.3 构造请求Cookie、Headers 一个都不能少微博移动端接口https://m.weibo.cn/api/container/getIndex可以返回用户微博的 JSON 数据。但直接裸请求大概率会拿不到数据因为服务器会校验请求头。我先创建一个 requests.Session并在请求头里带上 User-Agent、Referer 和 Cookie。import requests import time import pandas as pd import re from bs4 import BeautifulSoup user_id 你的UID # 替换成张雪峰的uid session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1, Referer: fhttps://m.weibo.cn/u/{user_id}, Accept: application/json, text/plain, */*, Cookie: 你的Cookie, })关于 Cookie 我要多说两句。Cookie 怎么拿用 Chrome 或 Edge 打开微博网页版登录自己的账号按 F12 打开开发者工具切到 Network 面板刷新页面随便点一个请求在 Request Headers 里找到 Cookie 字段复制出来即可。这里有个特别重要的细节Cookie 相当于你账号的临时通行证千万不要提交到公开仓库或者写进博客代码里。我在项目里会用一个配置文件或者环境变量保存避免硬编码。3.4 解析 JSON 卡片数据微博移动端返回的数据结构是典型的多层嵌套字典。核心字段在data.cards里每一张 card 代表一条微博或一条广告。我们需要筛选card_type 9的微博卡片然后从card.mblog.text拿到微博正文。下面这段代码循环翻页把内容存进列表def fetch_weibo_texts(session, user_id, pages10): results [] containerid f107603{user_id} # 这个容器id对应“全部微博” for page in range(1, pages 1): params { type: uid, value: user_id, containerid: containerid, page: page, } resp session.get(https://m.weibo.cn/api/container/getIndex, paramsparams, timeout10) data resp.json() cards data.get(data, {}).get(cards, []) for card in cards: if card.get(card_type) 9: mblog card.get(mblog, {}) user mblog.get(user, {}) results.append({ id: mblog.get(id), created_at: mblog.get(created_at), text: mblog.get(text, ), user_name: user.get(screen_name), reposts_count: mblog.get(reposts_count), comments_count: mblog.get(comments_count), attitudes_count: mblog.get(attitudes_count), }) print(f第 {page} 页完成累计 {len(results)} 条) time.sleep(2) # 礼貌爬取避免请求过快 return pd.DataFrame(results)这里有几个容易踩的小地方containerid中的107603是微博定义好的“用户微博容器”前缀替换为107603 uid 就是某个用户的主页时间线。接口返回的created_at往往不是标准时间类似2小时前这种相对时间。真要做时间趋势分析还需要额外处理。网络请求一定要设timeout否则程序可能卡死。我实际运行的时候pages 参数设成 10 页大概能拿到 150 到 200 条微博对情感分析和词云来说够用了。如果你的目标是分析半年甚至一年的文章需要做翻页逻辑优化因为微博一次最多拉 50 页而且大量翻页容易触发风控不建议贪多。3.5 清洗并落盘拿到原始text字段后你会发现里面全是 HTML 标签比如a href/n/张雪峰张雪峰/a、span classurl-icon这种。直接拿去分词、情感分析效果会很差。所以必须先清洗。def clean_text(html_text): if not html_text: return # 先去标签 soup BeautifulSoup(html_text, html.parser) text soup.get_text(separator ) # 再处理一些特殊实体和多余空格 text re.sub(r\w;, , text) text re.sub(r\s, , text).strip() return text df[clean_text] df[text].apply(clean_text) df df[df[clean_text].str.len() 5] # 过滤过短内容 df.to_csv(weibo_data.csv, indexFalse, encodingutf-8-sig)为什么用utf-8-sig而不是utf-8因为 utf-8-sig 会在文件开头加上 BOM用 Excel 打开 CSV 时中文不会乱码。这是我在实际项目里被坑过一次后养成的习惯。4. 情感分析给每条微博的情绪打一个分4.1 SnowNLP 的情感计算逻辑情感分析是自然语言处理里非常经典的任务。简单说就是让机器判断一段文本是正面、负面还是中性。我这次用的是 SnowNLP它内部基于朴素贝叶斯分类器训练了一个中文情感模型输入文本后输出一个 0 到 1 之间的浮点数。分数越接近 1情感越积极越接近 0情感越消极。SnowNLP 最大的优势是开箱即用不需要自己标注数据也不需要训练模型。你只需要from snownlp import SnowNLP text 这个项目让我学到了很多 score SnowNLP(text).sentiments print(score) # 大概率大于0.8不过它的劣势也很明显它天生是拿商品评论和短文本训练出来的对微博这种半口语化、带表情、带链接的文本理解会有偏差。所以这里的分数只能作为“大致情绪倾向”不能当成金标准。4.2 批量情感打分与结果统计清洗完数据后我把情感分析封装成一个函数跑完所有文本def get_sentiment_score(text): if not text or len(text) 2: return None try: return SnowNLP(text).sentiments except Exception: return None df[sentiment_score] df[clean_text].apply(get_sentiment_score) df df.dropna(subset[sentiment_score])拿到分数后我按常见阈值做情绪分类def sentiment_tag(score): if score 0.6: return 积极 elif score 0.4: return 消极 else: return 中性 df[sentiment_label] df[sentiment_score].apply(sentiment_tag)然后就可以统计分布了print(df[sentiment_label].value_counts())我这次实测的数据里积极内容大约占 45%中性 35%消极 20%。这个分布不算意外因为这位博主本身就经常发布偏正能量的教育和职业规划内容情绪倾向整体偏积极。但注意不要只看平均分。微博里很多内容是转发、活动、纯表情这些都会被模型误判。所以筛选数据时建议只保留正文长度大于 20 个字的微博效果会好很多。4.3 按日期看情绪起伏如果只是算一个总分项目就太单薄了。我顺手把发布时间也抓了做了个“情绪分数随时间变化”的折线图。这里有个技术细节微博接口返回的created_at可能需要解析成标准日期。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] # 解决中文显示 plt.rcParams[axes.unicode_minus] False df[created_at] pd.to_datetime(df[created_at]) df df.sort_values(created_at) daily_score df.groupby(df[created_at].dt.date)[sentiment_score].mean() plt.figure(figsize(12, 5)) daily_score.plot() plt.title(微博情感分数变化趋势) plt.xlabel(日期) plt.ylabel(平均情感分数) plt.tight_layout() plt.savefig(sentiment_trend.png, dpi150)从趋势图里能明显看到一些情绪波动的峰值这些峰值往往对应着某些热点事件。如果你想进一步挖掘可以把分数最低那几天的微博单独拎出来看看当时发生了什么。这就是“数据背后的故事”。5. 词云可视化让高频词“一眼可见”5.1 分词与停用词过滤词云的核心逻辑很简单把文本切成一个个词统计词频词频越高显示得越大。但中文分词不像英文按空格切分所以我用 jiebaimport jieba def generate_word_freq(text_list): word_list [] for text in text_list: words jieba.lcut(text) word_list.extend(words) return word_list光这样还不够分词结果里会有大量“我们”“什么”“一条”这种没有信息量的停用词必须过滤掉。我准备了一个简单的停用词集合你也可以自己按需扩充stopwords set([ 我们, 你们, 他们, 这个, 那个, 什么, 怎么, 可以, 还是, 就是, 不是, 但是, 一个, 没有, 自己, 现在, 因为, 所以, 如果, 觉得, 知道, 真的, 时候, 微博, 转发, 链接, 网页, 评论, 本文, http, https, 张雪峰 ])为什么要专门把“张雪峰”加进停用词因为词云里出现博主自己的名字没有分析价值还会盖住其他高频业务词。类似地常见的平台词、通用虚词都应该纳入停用词。5.2 中文字体是词云第一道坎用 WordCloud 做中文词云第一个坑就是字体。默认字体不支持中文生成出来全是方框。所以必须指定font_pathfrom wordcloud import WordCloud word_freq generate_word_freq(df[clean_text].tolist()) filtered_words [w for w in word_freq if w not in stopwords and w.strip()] # 中文词云必做的字体设置 font_path C:/Windows/Fonts/simhei.ttf # Windows # font_path /System/Library/Fonts/STHeiti Medium.ttc # macOS wordcloud WordCloud( font_pathfont_path, width1200, height800, background_colorwhite, max_words200, collocationsFalse, ).generate( .join(filtered_words)) plt.figure(figsize(12, 8)) plt.imshow(wordcloud, interpolationbilinear) plt.axis(off) plt.savefig(weibo_wordcloud.png, dpi150)如果你的系统没有 simhei.ttf可以换成其他中文字体比如宋体 (simsun.ttc)、微软雅黑 (msyh.ttc)。Linux 上一般是/usr/share/fonts/truetype/...。最稳妥的办法是用fc-list :langzh命令查一下系统里装了哪些中文字体然后把路径填进去。5.3 调整参数让词云更好看我第一次生成的词云很丑字糊在一起高频词和低频词差距不明显。后来发现max_words和max_font_size要配合着调。几个实用经验collocationsFalse可以避免 WordCloud 把“职业”“规划”这类相邻词语自动组成词组让单个关键词更突出。prefer_horizontal控制文字水平方向的比例默认 0.9也就是 90% 的词语水平显示。如果你想要更随性的效果可以调到 0.7。mask参数可以传入一张图片的 numpy 数组把词云裁剪成任意形状。比如可以放一张圆形 logo 图但前提是图片背景必须是白色或透明否则填充会出错。最终生成的词云里我看到的 Top 关键词是“专业”“考生”“高考”“考研”“建议”“学历”“英语”“选择”这类词。几乎一眼就能总结出这位博主的主要话题方向和教育相关属性。这就是词云的意义把大段文字压缩成一张可以“秒懂”的图。6. 实测中的坑和排查思路6.1 返回数据为空先查这三个地方我第一次运行程序时cards列表是空的。第一反应是接口参数写错了后来排查发现是 Cookie 失效。遇到空数据我建议按下面顺序排查看响应状态码和 JSON 里的提示。如果返回414或者401基本就是请求头或登录状态问题。检查containerid是否拼错。107603后面一定是 uid不能带符号。检查 Cookie 是否过期。微博 Cookie 的有效期不算长一旦过期接口会返回msg: 请求超时或者空 cards。重新复制一个新的 Cookie 即可。我在代码里加了一行调试日志每次请求后打印resp.status_code和data.get(msg)问题很快就定位了。6.2 爬取过快被限制的经验有几次我把time.sleep(2)改成time.sleep(0.5)连续跑了几百条后突然发现返回的数据全是重复的而且 Response 里出现了一个notice字段。这说明触发了平台的风控后来我不仅把请求频率降回 2 秒以上还加上了随机暂停import random time.sleep(random.uniform(2, 4))注意我不推荐用代理池、分布式架构来强行绕过限制。作为学习项目控制频率是合理的大规模爬取则需要和平台签署正规合作否则有法律风险。这也是我一直坚持的边界。6.3 情感分数“失真”的应对方案SnowNLP 在微博文本上的准确率其实一般。比如“这学校也太牛了”这种带讽刺口吻的文本模型可能给出 0.8 的正向分但实际上是在吐槽。我做了三件事来补救清洗时过滤掉长度过短、没有实际内容的文本。把“积极/中性/消极”三分类的阈值调整到 0.6 和 0.4而不是默认的 0.5 单阈值。对于分数在 0.4 到 0.6 之间的“模糊地带”不强行归类统一算中性。如果是正式项目我的建议是使用更大规模的中文预训练模型比如uer/roberta-base-finetuned-jd-binary-chinese或者直接调用大模型 API 做 few-shot 分类。大模型对反讽、隐喻的理解能力比传统机器学习强很多代价是成本更高、耗时更长。你完全可以在本项目基础上把get_sentiment_score函数替换成大模型接口而下游的统计和可视化逻辑基本不用改。我踩过几次坑之后最大的体会是爬虫和分析项目里代码只是最后 30% 的工作量前面 70% 都在处理“数据从哪来、干不干净、边界在哪”这些事。这个项目虽然代码量不大但它把数据采集、清洗、分析、可视化完整串了一遍以后再接触更复杂的文本挖掘项目心里就有底了。如果你也想动手试试建议先把目标换成自己经常刷的、愿意授权分析的公众人物或机构账号控制好请求频率跑通全流程后再考虑加功能。比如加上评论抓取、情绪时间线对比、自动生成分析报告甚至接一个 AI Agent 定时跑任务。到那一步你已经不只是在玩爬虫了而是在搭一条自己的数据分析流水线。