最近接了个小需求,想分析某个领域里一批博主的传播情况。最直接的思路就是看微博的转评赞,也就是转发数、评论数、点赞数这组核心互动指标。手动一条条去点开详情页看,几十条还能忍,几百上千条下来绝对让人崩溃。用 Python 爬虫批量抓取微博转评赞 data,是这类轻量级数据采集里非常典型的需求,新手上手快,老手拿来当工具脚本也很顺手。
我这次的目标很简单:给定一组微博链接或博主主页,把每一条微博的正文、发布时间、转发数、评论数、点赞数抓下来,存成结构化表格,方便后续做数据分析或趋势判断。整个项目实施下来,最关键的点其实不在代码本身,而在请求接口的选型、登录态的处理和反爬规避策略上。下面把这套方案完整拆开讲,从思路到落地,再到遇到的问题和排查方法,都给捋清楚。
1. 方案选型:为什么放弃页面解析,直接走移动端接口
很多人第一次写微博爬虫,脑子里冒出来的第一个方案就是请求网页版微博的页面,然后用 BeautifulSoup 或者 XPath 去解析 HTML。这个方案不是完全跑不通,但真的不推荐作为首选。
网页版微博的页面结构非常复杂,正文、时间、转评赞分布在不同的 DOM 节点里,而且不同时间段的页面结构还不一样。加上微博前端有大量的异步渲染,你直接请求到的 HTML 里,部分数据根本不在里面,而是通过后续的 AJAX 请求加载出来的。这就意味着你要么得分析一堆 JS 调用,要么就得借助无头浏览器(比如 Selenium 或 Playwright)去模拟真实浏览,成本一下就上去了。
移动端接口的方案完全绕开了这些坑。微博有一个面向移动端的接口,返回的是结构化 JSON 数据,字段按规则排列,解析起来非常轻松。这个接口的路径和参数相对固定,不需要模拟复杂的浏览器环境,直接用 requests 就能拿到干净的数据。
用移动端接口有几个非常明显的好处:
- 数据量小,响应速度快。接口返回的是纯数据,不像 HTML 页面那样带着一堆标签和无关脚本。
- 结构稳定。JSON 字段名含义清晰,转评赞数据、正文、发布时间都可以准确定位。
- 反爬压力小。相比网页端频繁的验证码和登录墙,移动端接口只要带上有效的 Cookie 并控制请求频率,正常使用问题不大。
当然,这个方案的前提是:我们抓的是公开可见的微博数据。涉及隐私、非公开的内容,抓取是既不合法也不合理的行为。整个项目我只处理公开账号的公开微博信息,并且严格控制请求频率,不给目标服务器造成额外压力,这个边界大家做的时候一定要有数。
2. 动手前准备:环境搭建、登录态获取与合规边界
2.1 基础环境与依赖库
这次项目用 Python 3 实现,依赖库保持精简,尽量少引入重量级模块。核心就三个:
requests:负责发 HTTP 请求,拿接口数据。json:Python 标准库,解析接口返回的 JSON。pandas:用来做数据清洗和表格输出,方便后续分析。
如果你只是想要最低限度的实现,甚至可以不装 pandas,直接用标准库里的csv模块就能写文件。不过用 pandas 操作数据更顺手,后续做去重、排序、筛选都方便,我建议还是装一个。
2.2 登录态 Cookie 的处理思路
微博的接口,特别是需要查看完整信息的接口,通常要求登录态。但这不代表你必须得用账号密码去模拟登录,那个过程涉及加密算法分析和验证码处理,对新手来说门槛太高,也容易惹上不必要的麻烦。
最简单的方式是:从你自己的浏览器里复制 Cookie 出来用。操作路径:用浏览器登录微博网页版(或者移动版),打开开发者工具(F12),随便选择一个网络请求,在请求头(Request Headers)里找到Cookie字段,整段复制出来,贴到脚本的配置变量里。
这里有几个细节要注意:
- Cookie 有效期有限。通常是几天到几周不等,过期了请求就会返回登录失效或者数据异常。
- Cookie 不要到处贴,也不要提交到公开的代码仓库里。这相当于是你账号的通行凭证,泄露了别人可以冒用你的身份做各种操作。我一般把 Cookie 放在独立的配置文件里,用
config.py或者.env文件单独存放,并加入.gitignore列表。 - 如果某个接口必须登录才能看数据,而你又不想用自己的账号,可以专门注册一个小号来取 Cookie,用完即弃,风险隔离。
2.3 请求头和请求参数的伪装
接口请求时,User-Agent是基础中的基础。默认的 Pythonrequests库的 UA 是一串很明显的标识,比如python-requests/2.31.0,服务器看到这种 UA 很容易识别为爬虫并直接拒绝服务。我习惯伪装成移动端的正常浏览器 UA,整体请求头模仿真实用户的行为模式。
我实际用的是:
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", "Accept": "application/json, text/plain, */*", "Accept-Language": "zh-CN,zh;q=0.9", "Referer": "https://m.weibo.cn/", "X-Requested-With": "XMLHttpRequest", "Cookie": cookie_value }Referer设置为移动端首页,X-Requested-With设置为XMLHttpRequest,这两个值配合 UA 一起用,请求看起来更像一个正常的移动端用户在浏览页面。
这里还要强调一句:头伪装的目的只是让请求看起来像正常用户,绝对不是为了绕过什么安全机制去搞破坏。爬取公开数据的初衷是为了分析和学习,不该用于任何干扰平台正常运营的行为。
2.4 合规边界先想清楚
做爬虫前,先把合规边界划清楚,省得后面被动。这次项目我给自己定了三条规矩:
- 只抓取公开账号的公开微博数据,不碰私密内容、不碰用户隐私信息。
- 严格控制请求频率,每次请求之间加延时,宁可慢一点,绝不给服务器添乱。
- 数据仅用于个人学习和分析,不用于商业用途,不对外公开原始数据集。
这三条是底线。实际操作中,我还会额外避免在短时间内大量请求同一个接口,尽量把请求打散,模拟人工浏览节奏。这样既保护了平台的服务稳定性,也减少了自己 IP 被限制的风险。
3. 核心代码实现:从单条微博到批量抓取
3.1 数据结构与字段解析
移动端微博接口返回的数据是标准 JSON。要抓的核心字段主要通过mblog这个节点下的内容来获取。我以一条微博的详情接口为例,它长这样:
import requests import json cookie_value = "你的cookie" headers = { ... 上面提到的请求头 ... } weibo_id = "5042085800575602" url = f"https://m.weibo.cn/statuses/show?id={weibo_id}" resp = requests.get(url, headers=headers, timeout=10) data = resp.json() mblog = data["data"]["mblog"] print(mblog["raw_text"]) # 微博正文 print(mblog["created_at"]) # 发布时间 print(mblog["reposts_count"]) # 转发数 print(mblog["comments_count"]) # 评论数 print(mblog["attitudes_count"]) # 点赞数这几个字段是核心中的核心:
raw_text:微博的正文内容。注意,如果微博里有长图、链接、话题、@用户,正文里会带一些特殊标记,后面清洗的时候需要处理。created_at:发布时间,格式类似于Tue Dec 12 10:30:00 +0800 2024,不是标准的 ISO 时间格式,后面处理的时候需要转换一下。reposts_count:转发数,整数类型。comments_count:评论数,整数类型。attitudes_count:点赞数。这个字段在不同的接口版本里名字可能有差异,有的地方叫like_count,有的叫attitudes_count,我这次用的是移动端接口,就是attitudes_count。
除了这些核心字段,mblog里还有id(字符串形式的微博ID,后面抓评论和转发详情时要用)、user(博主信息,包含screen_name和用户ID)、pic_num(图片数量)等,具体需求具体取。
3.2 从链接里提取微博 ID
很多场景下,用户拿到的是微博的完整分享链接,比如https://weibo.com/6286500601/NvDPL6QmT这种。这种链接里并没有直接给出微博 ID,需要解析。
实际处理的思路分两种:
- 如果链接是
weibo.com/数字/字母串的形式,字母串部分是微博的短 ID,需要调接口转换成真实的数字 ID。 - 如果链接是
m.weibo.cn/status/数字的形式,那最后一段数字直接就是微博 ID,直接拿来用即可。
为了节省复杂度,我建议在项目初期统一要求输入m.weibo.cn格式的链接,或者在代码里做一次自动转换。转换逻辑不复杂,我写了一个简单的函数:
def extract_weibo_id(url): if "m.weibo.cn/status/" in url: return url.strip().split("status/")[-1].split("?")[0] # 其他格式的分享链接,先用简单方式提取尾部的字母串 part = url.strip().split("/")[-1].split("?")[0] return part短链和字母串的完整转换逻辑这里先不展开,后面在“常见问题与排查”部分再细讲。
3.3 批量抓取与进度控制
单条微博的数据拿到手之后,批量抓取就是循环加延时的事。但这里有个很关键的细节:绝大多数情况下,我们不是手动收集了一堆微博链接再去抓,而是给定一个博主 ID,把他最近几十上百条微博全部拉下来,然后再针对每一条微博去取转评赞。
这个场景需要用到另一个接口:用户发布的微博时间线接口。请求路径是:
url = f"https://m.weibo.cn/api/container/getIndex?type=uid&value={uid}&containerid=107603{uid}"这个接口返回的是该用户最近发布的微博列表(默认大概十几条一页),每条微博在data.cards数组里,每个card的mblog节点就是微博信息。翻页是通过since_id参数控制的,具体值是上一次请求返回的data.cardlist_info.since_id。
我的批量抓取逻辑是这样的:
def fetch_user_weibos(uid, max_pages=5): all_weibos = [] since_id = "" for page in range(max_pages): params = { "type": "uid", "value": uid, "containerid": f"107603{uid}", "since_id": since_id } resp = requests.get("https://m.weibo.cn/api/container/getIndex", headers=headers, params=params, timeout=10) data = resp.json() cards = data.get("data", {}).get("cards", []) if not cards: break for card in cards: if card.get("card_type") == 9 and "mblog" in card: all_weibos.append(card["mblog"]) # 解析下一页游标 since_id = data.get("data", {}).get("cardlist_info", {}).get("since_id", "") if not since_id: break time.sleep(2 + random.random() * 3) # 随机延时,2~5秒之间 return all_weibostime.sleep这一步千万别省。很多爬虫脚本被限制,不是因为没有伪装请求头,而是因为请求频率太高,一看就是机器行为。我在循环里加入随机延时,让两次请求之间相隔 2 到 5 秒,整体速度虽然慢了点,但安全系数高很多。
拿到全部微博列表之后,转评赞数据就已经包含在每条mblog里了,不需要再额外请求详情接口。因为时间线接口返回的mblog字段里本身就带着reposts_count、comments_count、attitudes_count这些数据。这给我们省了一大笔请求量,也减少了对服务器的压力。
3.4 数据清洗与表格输出
数据抓下来之后,直接拿来用肯定不行,必须做一轮清洗。以我这次项目为例,清洗步骤主要有四个:
- 去除正文里的 HTML 标签和特殊字符。部分微博的正文会带有
<a>、<span>这类标签残片,用正则表达式清理掉。 - 统一时间格式。
created_at字段是英文时间格式,我把转成YYYY-MM-DD HH:MM:SS,方便排序。 - 处理空值。有些老微博没有点赞数据,字段可能缺失,填 0 就行。
- 去重。如果多次抓取,同一微博 ID 可能出现多次,按微博 ID 去重,保留最新记录。
清洗这部分我用的是 pandas,核心代码很简单:
import pandas as pd df = pd.DataFrame(all_weibos) df = df[["id", "raw_text", "created_at", "reposts_count", "comments_count", "attitudes_count"]] df.columns = ["weibo_id", "content", "publish_time", "reposts", "comments", "likes"] df["publish_time"] = pd.to_datetime(df["publish_time"], format="%a %b %d %H:%M:%S %z %Y") df["reposts"] = pd.to_numeric(df["reposts"], errors="coerce").fillna(0).astype(int) df["comments"] = pd.to_numeric(df["comments"], errors="coerce").fillna(0).astype(int) df["likes"] = pd.to_numeric(df["likes"], errors="coerce").fillna(0).astype(int) df = df.drop_duplicates(subset=["weibo_id"]) df.to_csv("weibo_data.csv", index=False, encoding="utf-8-sig")输出用utf-8-sig而不是utf-8,主要是为了解决 Excel 打开 CSV 时中文乱码的问题。这个小细节很多人容易忽略,等打开文件看到一堆乱码才反应过来,又要重新跑一遍脚本,浪费时间。
created_at的转换试了几次,不同接口返回的格式略有差异,建议先打印一条数据确认格式再写解析逻辑,别直接套死模板。
4. 数据存储与二次扩展:除了表格还有什么玩法
4.1 增量抓取与断点续抓
如果只是想一次性拉一批数据,上面提到的方案已经够用了。但实际使用中经常遇到的情况是:第一次跑了 100 条,过了一周想再跑一次,拉取新增的微博,如果每次都是全量抓,不仅慢,而且会重复拉取大量旧数据。
我的做法是用本地文件做增量标记。每次抓取时,先读取上次已经保存的微博 ID 集合,新抓到的数据如果 ID 已经存在,就跳过;只把新增的追加到数据集里。
实现逻辑是这样的:
import os import json seen_ids = set() if os.path.exists("seen_ids.json"): with open("seen_ids.json", "r", encoding="utf-8") as f: seen_ids = set(json.load(f)) new_weibos = [] for mblog in all_weibos: if mblog["id"] in seen_ids: continue seen_ids.add(mblog["id"]) new_weibos.append(mblog) with open("seen_ids.json", "w", encoding="utf-8") as f: json.dump(list(seen_ids), f, ensure_ascii=False)这样每次跑完,只处理新增部分,几分钟就能完成增量更新。
4.2 从转评赞到更深的互动数据
转评赞只是互动数据的冰山一角。如果要做更深入的分析,还可以针对每一条微博继续抓取评论列表和转发列表。比如评论列表的接口路径是:
url = f"https://m.weibo.cn/api/comments/show?id={weibo_id}&page={page}"拿到评论内容后,可以做词频分析、情感分析,甚至提取评论里的表情符号分布。这个扩展性强,但请求量会大幅增加,抓取时间翻好几倍,建议按需使用。
以我这次项目为例,我额外抓了评论列表里的热评内容,用来判断每条微博的讨论焦点,配合转评赞数据一起看,整体信息量丰富了很多。
4.3 成果数据的可视化呈现
数据拿到手里,不做可视化等于白干。pandas 处理完的数据可以直接喂给 matplotlib 或 pyecharts 做图表。我最常用的几个展示角度:
- 按时间维度统计转评赞的趋势变化。
- 按微博类型(纯文字、带图、带视频)对比互动量差异。
- 转赞比、评赞比的计算,识别出那些转发特别高但点赞一般的内容。
这些可视化工作虽然不复杂,但能让分析结果直观很多,做汇报的时候也更好讲。
5. 常见问题与排查技巧实录
5.1 登录态失效:返回数据里没有 mblog 节点
最常见的问题之一。运行一段时间后突然发现返回的data是空的,或者data.cards里没有mblog,这时候大概率是 Cookie 过期了。
排查思路很简单:第一步,先在浏览器里手动访问一次同样的接口,看看能不能正常返回数据。如果能,说明问题出在脚本的 Cookie 上,更新一下 Cookie 就行。如果浏览器也看不到数据,那考虑是不是账号本身权限问题(比如被限制登录)。
我的习惯是每次写代码之前先手动在浏览器里确认一次接口能通,再把 Cookie 贴进脚本,省得代码问题还是权限问题分不清。
5.2 微博 ID 解析失败:短链和字母串 ID
前面提到过,weibo.com/6286500601/NvDPL6QmT这种链接里的尾部不是数字 ID,而是字母串短 ID。直接用这个值去请求详情接口,返回的结果为空。
解决办法是找到短 ID 和真实 ID 之间的转换接口。比较靠谱的方式是请求这个链接,观察重定向过程中携带的参数,或者访问一个专门解析短链的接口。实际操作中,我发现了https://weibo.com/ajax/statuses/show?id=字母串这个接口可以直接返回真实数字 ID 和微博详情,对于这个场景是够用的。
如果转换接口不好使,还有个土办法:用移动端链接格式的 ID。即尝试https://m.weibo.cn/status/字母串这个链接,手动打开看能不能正常访问。能访问的话,有些场景下可以直接用字母串替代数字 ID,因为部分接口兼容这种格式。
5.3 部分字段缺失:老微博没有点赞数
抓一批老微博时发现,早期的部分微博返回的数据里压根没有attitudes_count字段,或者字段值为None。直接用int(None)必然会报错。
解决方案上面代码里已经体现了一部分:用pd.to_numeric(errors="coerce")把非数字值强制转成 NaN,再fillna(0)补成 0。用纯 Python 写的话,就是用mblog.get("attitudes_count", 0) or 0,把None和0统一兜住。
这个思路也可以推广到其他字段。凡是可能缺失的字段,一律用get加默认值的方式处理,不要直接下标访问。
5.4 被限制访问:请求频率过高
表现是请求一段时间后,接口开始大量返回 418 或者 403 状态码,或者干脆返回一串看不懂的错误提示。这种情况通常不是程序逻辑问题,而是访问频率过高被限制。
处理办法比较朴素:降低抓取频率,延长延时时间。我个人的经验是,微博移动端接口单次会话下,每秒超过 1 个请求的风险较高,最稳妥的做法是每次请求后至少间隔 2 到 3 秒。
如果延时调低后还是频繁受限,那就考虑分段抓取:比如分多个时间段跑,每次只跑一部分数据。把任务拆散了,整体受控概率小很多。
5.5 数据异常大或内存溢出
这批数据正常情况下不会很大,但如果抓取的用户微博有几万条,全部堆在内存里再转 DataFrame,可能会出现内存占用过高的情况。
我的做法是分批写入 CSV。每抓完一定数量的微博(比如 500 条),就写一次文件,然后释放内存。这样即使中断了,前面保存的数据也不会丢。
batch_size = 500 for i in range(0, len(all_weibos), batch_size): batch = all_weibos[i:i + batch_size] df_batch = pd.DataFrame(batch) # 处理字段并追加写入文件 df_batch.to_csv("weibo_data.csv", mode="a", header=not saved, index=False) saved = True5.6 常见问题速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
接口返回data为空 | Cookie 过期 | 在浏览器中重新登录并更新 Cookie |
| 返回状态码 403/418 | 请求频率过高 | 加大延时,降低频率 |
转评赞字段出现None | 老微博数据缺失 | 用get+ 默认值兜底 |
| 解析出来的时间是英文格式 | 接口返回非标准时间格式 | 自定义时间解析,统一转标准格式 |
| CSV 用 Excel 打开乱码 | 编码问题 | 保存时用utf-8-sig |
| 微博短链接解析不了 ID | 链接是字母串短ID | 用转换接口或直接请求移动端链接验证 |
写在最后的实操体会
这个项目整体不大,但从需求确认到数据落地,踩过的坑一个不少。Cookie 过期、字段名不一致、时间格式混乱、请求频率把控,每一类问题单独拎出来都不复杂,但串在一起的时候,排查起来还是挺花时间的。
我个人最大的体会是:写爬虫脚本之前,一定要先在浏览器里手动把整个流程走一遍。拿真实的数据格式来逆推解析逻辑,比凭空猜字段名靠谱得多。这个习惯看着笨,但能帮你省掉大把调试时间。
另外一个务实建议:抓取频率宁慢勿快。很多限制不是因为爬虫识别得多准,而是因为请求行为实在太不像是人。把延时加进去,功能一样能完成,但稳定性高出好几个级别。
最后再提醒一句,数据是抓下来了,但怎么用、拿来做什么,心里得有杆秤。公开数据的采集和学习是技术圈的正常需求,但别拿它去做任何影响他人权益的事。守住这个边界,这样的工具脚本才能用得安心。