大概两个月前,我刷微博的时候突然冒出一个念头:那些几百万粉丝的公众人物,大家对他们发的每一条内容到底持什么态度?评论区吵得不可开交,那整体情绪是偏正向还是偏负向?一条一条看显然不现实,最好的办法是写个程序,把这些公开微博数据拉下来,跑一遍情感分析,再画一张词云,让高频关键词浮出水面。于是就有了这个 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 snownlp==0.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": f"https://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, pages=10): results = [] containerid = f"107603{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", params=params, timeout=10) 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 class="url-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", index=False, encoding="utf-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", dpi=150)从趋势图里能明显看到一些情绪波动的峰值,这些峰值往往对应着某些热点事件。如果你想进一步挖掘,可以把分数最低那几天的微博单独拎出来,看看当时发生了什么。这就是“数据背后的故事”。
5. 词云可视化:让高频词“一眼可见”
5.1 分词与停用词过滤
词云的核心逻辑很简单:把文本切成一个个词,统计词频,词频越高显示得越大。但中文分词不像英文按空格切分,所以我用 jieba:
import 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_path:
from 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_path=font_path, width=1200, height=800, background_color="white", max_words=200, collocations=False, ).generate(" ".join(filtered_words)) plt.figure(figsize=(12, 8)) plt.imshow(wordcloud, interpolation="bilinear") plt.axis("off") plt.savefig("weibo_wordcloud.png", dpi=150)如果你的系统没有 simhei.ttf,可以换成其他中文字体,比如宋体 (simsun.ttc)、微软雅黑 (msyh.ttc)。Linux 上一般是/usr/share/fonts/truetype/...。最稳妥的办法是用fc-list :lang=zh命令查一下系统里装了哪些中文字体,然后把路径填进去。
5.3 调整参数让词云更好看
我第一次生成的词云很丑,字糊在一起,高频词和低频词差距不明显。后来发现max_words和max_font_size要配合着调。
几个实用经验:
collocations=False可以避免 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 定时跑任务。到那一步,你已经不只是在玩爬虫了,而是在搭一条自己的数据分析流水线。