☰
Python微博数据爬取:移动端接口与增量采集工程实践
2026/10/15 7:10:48 网站建设 项目流程

简介:针对微博平台的数据采集需求,这份基于Python的完整实现覆盖了从网络请求、HTML/JSON解析、模拟登录到反爬处理、数据存储的全流程。资源以Python爬虫为核心,兼顾requests、urllib、BeautifulSoup、json等库的实战用法,并为学习者梳理了爬虫中的异常处理、异步提效、微博官方API以及合规边界,适合有一定Python基础的开发者快速上手微博数据抓取。压缩包共包含18个文件,以13个Python脚本为主,辅以3个Markdown说明文档、1个说明压缩包和1个配置文件,整体体积仅90KB。脚本按功能划分模块,覆盖数据抓取、任务创建、用户搜索等场景,说明文档可用于快速理解项目结构和运行逻辑。这份资源已吸引335人学习下载,体量虽小但结构清晰,提供了项目说明、工具脚本与运行配置等可直接参考的内容,可帮助读者从代码层面掌握微博数据爬取的实现思路,并在实际项目中快速复用或改造。

1. Python 微博数据爬取:这套资源包里到底有什么,值不值得花时间拆

做舆情监测、热点事件追踪或电商口碑分析的人,大概率绕不开微博数据。很多人一上手就抓网页版 HTML,结果 DOM 结构一改,解析器全线崩盘,每周修选择器都能修出心理阴影。我拆过这份 Python微博数据爬取.zip,核心思路不是硬碰网页版,而是绕到移动端接口,用 cookie 维持登录态,以 JSON 形式拉取搜索和用户时间线数据,再统一落库。这套方案对 python 版本依赖轻、脚本结构清晰、可替换性高,适合需要短周期采集公开数据的开发者和数据分析人员。下面把可直接复现的部分、参数含义和踩过的坑一次说清,新手能跟步骤走,熟手可以直接翻到避坑章节核对边界和补偿策略。

2. 前置准备:cookie 持久化、请求头与容器接口选型

2.1 为什么绕开网页版 HTML 解析

网页版的数据流是服务端渲染加前端异步混搭,正文、转发数、时间戳分散在多个 DOM 节点里,class 名还在不停加盐。用 BeautifulSoup 或 XPath 定位节点,今天写好的选择器,明天可能就全军覆没。移动端接口的返回是结构化 JSON,字段命名相对稳定,而且移动端流量在整个反爬体系里的限流阈值比桌面端宽松。这不是玄学,是维护成本的现实考量——同样的采集任务,走 HTML 解析每周要修一次脚本,走移动端 JSON 基本可以一个月不动。

资源包选定移动端接口还有一层原因:搜索和用户时间线两个场景共用同一套分页协议,容器和数据流的取数逻辑一致。这样可以只维护一个 fetch 函数、一个解析函数,按场景传不同的 containerid 参数。对中小规模采集任务来说,少一半代码就意味着少一半出错面。网页版 HTML 解析并非不能跑,但它更适合低频单页抓取,不适合规模化落库。

2.2 登录态与 cookie 的获取和持久化

微博的搜索接口和用户主页时间线都需要登录态。资源包的默认做法不依赖 Selenium,而是手动登录一次移动端页面,从浏览器开发者工具里复制 SUB 和 SUBP 两个 cookie 值,写入独立配置文件。cookie 不写死在采集脚本里,便于过期后快速替换,也让代码可以直接分享而不会泄露凭据。

# config.py COOKIE = "SUB=你的SUB值; SUBP=你的SUBP值" BASE_HEADERS = { "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15", "Accept": "application/json, text/plain, */*", "Referer": "https://m.weibo.cn/", "X-Requested-With": "XMLHttpRequest", }

COOKIE 字段用分号拼接多个键值对,requests 的 Session 会自动解析成独立 cookie。Referer 必须指向移动端域名,否则接口直接返回 403。User-Agent 选 iPhone 机型是移动端接口最常见的行为特征,比桌面端 UA 的通过率高很多。这里有一个容易被忽略的细节:SUBP 过期时间通常比 SUB 短,只换 SUB 不换 SUBP,接口照样会飘验证码。我一般会两个一起换,省一次排查。

2.3 请求头配置与容器接口选型

实际操作里接口选型就是两个 URL 模板加一个 containerid 探测逻辑。搜索接口模板:

https://m.weibo.cn/api/container/getIndex?containerid=100103type=1&q=关键词&page_type=searchall&page=1

用户时间线接口模板:

https://m.weibo.cn/api/container/getIndex?containerid=230283{uid}_-_profile_-_weibo&page=1

containerid 是移动端数据流的核心标识。搜索场景里,先用一个不带关键词的请求拿到搜索页预设的 containerid,再拼上关键词翻页。因为 containerid 偶尔会在版本更新后变动,资源包的做法是启动时先热探测一次搜索页,把返回的 containerid 缓存到本地文件,而不是硬编码在脚本里。这个细节能少踩一半的坑,后面避坑章节会再提。

缓存文件建议用 JSON 存储,包含 containerid 和探测时间,每次启动前检查是否超过 24 小时,超过则重新探测。探测量只有一次请求,成本极低,但对稳定性提升明显。搜索接口有时会返回多条 containerid 分组,比如综合、实时、热门分别对应不同 id,资源包默认取第一个;如果需要分类型采集,可以在配置里多加一个字段手动指定。

提示:cookie 里的 SUB 属于账号凭据,不要提交到公开仓库。部署机建议用环境变量注入,config.py 只留占位符。

3. 核心采集:搜索接口翻页与用户时间线游标抓取

3.1 搜索接口的请求构造与翻页逻辑

搜索接口返回的 JSON 结构是外层 code + data,data 里是 cards 数组。每个 card 代表一条微博或一个内容块。真正的博文 card 的 card_type 是 9,card_type 为 11 的是推荐位、38 是热搜词条,都要过滤掉。资源包的核心采集函数是这样的:

import requests def fetch_search(session, keyword, page=1): url = "https://m.weibo.cn/api/container/getIndex" params = { "containerid": f"100103type=1&q={keyword}", "page_type": "searchall", "page": page, } resp = session.get(url, params=params, timeout=10) data = resp.json() if data.get("code") != 0: raise RuntimeError(f"搜索接口返回异常: {data.get('msg', '')}") cards = data.get("data", {}).get("cards", []) results = [] for card in cards: if card.get("card_type") != 9: continue mblog = card.get("mblog", {}) results.append(mblog) return results

这个函数只做一件事:按关键词和页码拉数据,过滤非博文卡片,返回 mblog 列表。session 由外部传入,让多个请求复用同一套 cookie 和连接池,避免每次请求重新握手。params 里 page_type 固定为 searchall,表示综合搜索;想看原创结果,改成 search_orig;想看热门,改成 search_hot。timeout 设 10 秒,平台响应慢时快速失败,而不是让线程无限挂起。

翻页逻辑的关键是判断是否真的到底。常见做法是循环请求直到返回空 cards,但这里有个陷阱:搜索接口有单页条数上限,超过之后第 N 页和第 N+1 页返回相同内容,cards 不为空但全是重复。正确判断方式是记录本页第一条微博的 id,和上一页最后一条对比,一旦出现重复就停止翻页。资源包在循环里维护一个 last_id 变量,每次迭代比对,日后再加去重逻辑也能直接复用这个判断。

3.2 用户时间线的游标翻页

用户时间线同样走 containerid 分页,但不用 page 数字,而是用 since_id 游标。这个 uid 也不是用户主页 URL 里的数字串,而是接口返回的 user.id 字段。很多新手在这里翻车,把网页版 URL 里的数字当 uid,请求直接 404。正确拼法是 containerid 为230283{uid}_-_profile_-_weibo。

def fetch_user_timeline(session, uid, since_id=None): url = "https://m.weibo.cn/api/container/getIndex" params = {"containerid": f"230283{uid}_-_profile_-_weibo"} if since_id: params["since_id"] = since_id resp = session.get(url, params=params, timeout=10) data = resp.json() if data.get("code") != 0: raise RuntimeError(f"时间线接口异常: {data.get('msg', '')}") info = data.get("data", {}) cards = info.get("cards", []) next_since = info.get("cardlistInfo", {}).get("since_id") return cards, next_since

since_id 是服务端生成的游标,每次返回的 cardlistInfo.since_id 就是下一页入口。循环里把它传给下一次请求,直到 since_id 为空。这个设计比数字页码抗数据变化,用户在两次翻页之间新发微博,也不会打乱游标顺序。资源包在这里做了双保险:过滤 card_type 为 9 的博文之外,还把 card_type 为 11 的置顶推荐一并滤掉,避免把推广内容混进数据集。

3.3 字段映射:从平台命名到业务字段

mblog 里的字段是平台内部命名,直接落库不友好。资源包写了一个 mapper 函数统一转换,把平台字段映射成业务字段名:

import json def map_mblog(mblog): user = mblog.get("user") or {} return { "id": mblog.get("id"), "bid": mblog.get("bid"), "text": mblog.get("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), "uid": user.get("id") or "", "screen_name": user.get("screen_name", ""), "raw": json.dumps(mblog, ensure_ascii=False), }

id 是平台内部唯一标识,bid 是短链接里的字符串,两者都能用来去重;具体业务里 id 更可靠,bid 适合做人肉核验。text 字段可能含 HTML 标签和表情转义,清洗放下一步。raw 字段把原始 JSON 原样保存,以后想补新字段就不用重新爬。这种"整包存档 + 字段抽取"的双层结构是长线采集项目的标准做法:映射规则变了,重跑解析函数即可,网络请求一次都不用再发。

字段映射顺序也有讲究,先取 user 再取文本,避免 user 缺失时整条报错。实际使用里,text 字段往往还包含 video、url_struct 等附属信息,如果做链接分析,可以在 mapper 里额外加一项 urls,把 url_struct 里的完整链接单独抽出来,和 text 分开存储,方便后续展开短链。

4. 落地存储:文本清洗、主键去重与增量游标

4.1 文本清洗与字段规整

微博正文里全是 HTML 标签、微博表情转义、@提及和话题符。清洗规则取决于业务:情感分析要保留 @提及和话题符,词频统计则全删。资源包默认提供一套中性清洗,HTML 标签剥掉、实体转回字符、零宽空格移除:

import re import html def clean_text(raw_text): text = html.unescape(raw_text) text = re.sub(r"<[^>]+>", "", text) text = re.sub(r"&nbsp;", " ", text) text = text.replace("\u200b", "") return text.strip()

html.unescape 把 & 之类的实体先转回字符,剥标签时才不会误伤。\u200b 是零宽空格,肉眼看不见,但进到分词阶段会把 tokenizer 折腾得够呛。清洗放在入库前而不是入库后,是为了保住原始数据可追溯性。资源包的默认策略是 raw 字段存原始 JSON,text_clean 字段存清洗结果,两者并列,互不覆盖。后续换清洗规则,改一版清洗函数批量重跑清洗列就行,原始数据永远还在。

时间字段的坑更多人踩。接口返回 created_at 是类似"刚刚""5分钟前""昨天"的相对时间,直接存文本没法排序。资源包写了个时间转换函数,先匹配"刚刚"和"N分钟前",再匹配"N小时前",最后匹配带日期的绝对格式,转换成标准时间戳;正则处理不了的兜底置为 None。处理规则宁缺毋滥——猜错时间比留空更伤数据质量。

4.2 去重策略与入库

采集任务跑上几轮,重复是必然事件。去重键选 mblog.id,平台内部 ID 稳定唯一。资源包在 SQLite 里给 id 建主键,插入用 INSERT OR IGNORE,数据库层面完成去重,不依赖采集脚本的判断逻辑:

import sqlite3 import time DB_PATH = "weibo.db" def init_db(): conn = sqlite3.connect(DB_PATH) conn.execute(""" CREATE TABLE IF NOT EXISTS weibo_posts ( id TEXT PRIMARY KEY, bid TEXT, uid TEXT, screen_name TEXT, text_clean TEXT, created_at TEXT, reposts_count INTEGER, comments_count INTEGER, attitudes_count INTEGER, raw_json TEXT, fetched_at TEXT ) """) conn.commit() return conn def save_posts(conn, posts): rows = [( p["id"], p["bid"], str(p["uid"]), p["screen_name"], p["text_clean"], p["created_at"], p["reposts_count"], p["comments_count"], p["attitudes_count"], p["raw"], time.strftime("%Y-%m-%d %H:%M:%S") ) for p in posts] conn.executemany( "INSERT OR IGNORE INTO weibo_posts VALUES (?,?,?,?,?,?,?,?,?,?,?)", rows ) conn.commit()

SQLite 单机处理几百万条毫无压力。fetched_at 记录采集时间,和 created_at 放一起可以做"采集时间 vs 发布时间"的双维分析,能发现平台是否存在内容回溯或补删。如果预期数据量过千万,后期可以换 MySQL 或 ClickHouse,但表结构和去重逻辑不用改——先定标准 Schema 的价值就在这里。

4.3 增量采集的游标设计

增量采集的核心是记住"上次看到哪"。搜索场景用 keyword 做 key 存最后一条微博 id,用户时间线场景用 uid 存 last_since_id。资源包单独建了一张 meta 表:

def load_since_id(conn, uid): row = conn.execute( "SELECT since_id FROM user_meta WHERE uid = ?", (str(uid),) ).fetchone() return row[0] if row else None def save_since_id(conn, uid, since_id): conn.execute( "INSERT OR REPLACE INTO user_meta (uid, since_id, updated_at) VALUES (?, ?, ?)", (str(uid), since_id, time.strftime("%Y-%m-%d %H:%M:%S")) ) conn.commit()

meta 表和业务表分离,好处是游标更新失败不会污染业务数据。每次采集流程:读游标 → 拉新数据 → 入库 → 更新游标。这个顺序里入库和更新游标之间如果程序崩溃,下一轮会重拉部分数据,靠主键去重兜底。since_id 失效是常态,如果返回空 cards 但 since_id 非空,说明游标过期,常见做法是回退用页面数字重扫最近 50 页做补偿。补偿逻辑放避坑章节细讲。

5. 爬取避坑:cookie 失效、限流回跳与数据异常排查

5.1 cookie 失效与验证码跳转

现象:脚本跑着跑着,接口开始连续返回 code 为 417 的响应,或者返回的 cards 全是空列表,日志没有报错,但采集量断崖式下跌。

原因:SUB 值过期,或账号被判定为异常登录,接口悄悄切到验证码通道。移动端接口在 cookie 失效时不一定会返回 401,更多时候返回空数据或引导页结构,造成"接口正常但没数据"的假象。

解决:在 fetch 函数里加响应结构检测,发现 code 不为 0 或 data 里缺 cards 字段时,立即抛明确异常并停止采集,别静默返回空列表。我在资源包基础上加了个健康检查:每采集 100 条就主动请求一次用户主页接口,返回码异常就写日志并休眠 10 分钟。cookie 过期没有捷径,只能重新登录换新 cookie。把换 cookie 流程脚本化成半自动,能省不少人工盯屏时间。

5.2 限流与封禁

现象:请求频率稳定在每秒 1 次,跑了半小时后部分接口随机返回 504 或空响应,重试能恢复,但恢复间隔越来越长。

原因:平台的限流不是简单按 IP QPS 封顶,而是滑动窗口加行为特征的多重判定。固定频率请求反而容易被识别成脚本。另一个常见误用是多个任务共享一个 session 并发跑,连接数一拉高,风控阈值立刻被打满。

解决:把请求间隔从固定值改成随机区间,比如 1.5 到 3.5 秒随机,模拟人工浏览的抖动。并发严格控制在单 session 串行执行,多线程扩并发是这条路上最典型的翻车姿势。我一般按 uid 分片,每个分片独立进程跑,进程之间用 meta 表协调游标,而不是单进程内开线程抢带宽。被封禁后不要对抗,停半小时再起;连续被封三次就降每天总请求量,优先保数据完整性而不是速度。

5.3 数据内容异常与字段缺失

现象:采到的正文出现大量"抱歉,此微博已被作者删除"类占位文本,或者 user 字段缺失导致 uid 和 screen_name 为空。

原因:已删除或进入审核的微博在接口里保留 card 骨架,但 mblog 内容被替换成占位符。历史微博的 user 字段会整体缺失,这是接口兼容性问题,不是请求参数写错。

解决:在 map_mblog 里加字段存在性检查。占位文本不能只判断 text 为空,要匹配平台特定的删除提示句式。user 缺失时,uid 用请求里的 uid 兜底,screen_name 置 unknown,保证每行数据都有主键和归属。这个判断要写进采集脚本而不是清洗脚本,原始 JSON 一旦落库再发现缺字段,就得重爬一遍,成本太高。

5.4 重复数据与游标回跳

现象:增量采集时,同一个 id 的微博连续两轮跑到,且时间线游标指向了比上一轮更早的位置,老数据被反复重抓。

原因:since_id 是服务端生成的临时游标,有有效期。如果单用户时间线拉取速度慢,游标可能在下一次请求前失效,服务端回退返回更早的数据。

解决:采集前记录当前最大 created_at 水位线,本轮结束后检查最后一次写入的 created_at 是否早于水位线,如果是则丢弃该批次并重试。另一个更稳的做法:meta 表里同时存 last_since_id 和 max_since_id,每轮取较大的一个更新。游标对比更精确,建议作为默认方案;水位判断简单,但不适合按时间倒序回溯历史数据的场景。两种我都用过,游标对比的误判率明显更低。

6. 进阶:采集结果核验与自动补采的两个习惯

6.1 从被采集方视角核验字段

每次改完字段映射函数,抽 3 条采到的 bid,直接在移动端打开对应微博页,拿页面展示的转发数、评论数、点赞数和库里对比。字段映射错位是整条链路里最隐蔽的问题,接口字段名和业务字段名没有一一对应关系,不主动核验很难发现。我习惯把核验做成独立脚本,输入 bid 列表,输出库内值和接口实时值的差异表,方便一眼定位。

6.2 用双写日志做数据差异对比

采集脚本每写一批数据,同时把本批次的 id 集合追加到当日日志文件。隔天拉取时,把前一天日志里的 id 集合和本轮新写入数据做差集。差集非空,说明这段事件里微博被删除或隐藏——这在舆情分析里本身就是有价值的信号。我把差集单独存成 deleted_log 表,跑了几周后发现某个话题的删除率达到 3%,比正常值高一截,顺着查下去是水军账号在集中删帖。

6.3 增量推送与自动补采

资源包本身不带调度器,我习惯在外面包一层系统定时任务,每天固定时间触发增量采集。采集完统计本批次新增数,如果低于历史均值的 30%,判定为异常,触发一次全量补偿采集,最多补偿三轮。从那以后,我每次部署采集项目都会先写一个"健康检查 + 异常补偿"的壳,再进入正常增量循环。数据采集最怕的不是跑得慢,而是静默地少采了一个窗口还没人发现。回到最初拆这套 Python微博数据爬取 资源包的经验,真正值钱的不是那几段请求代码,而是这套核验、补偿、存档的闭环。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询