简介:这是一份针对微博平台定制的网络爬虫项目,面向希望学习社交数据采集的爬虫开发者与数据分析人员。程序聚焦抓取微博正文与评论,覆盖API请求、HTML解析、动态内容加载、反爬规避及数据持久化等核心模块,适用于舆情监控、话题分析、用户行为研究等场景。压缩包约1.26MB,文件总数标注为0,具体文件类型明细暂未提供,资源本体应包含核心爬虫脚本与配置说明。已有1899人学习浏览,适合具备Python基础、希望以实战项目理解爬虫开发流程的读者。通过研读代码,可掌握requests会话构造、BeautifulSoup页面解析、Selenium模拟登录与滚动加载等实现方式,同时能体会代理IP轮换、请求间隔设置与登录态维持等反爬策略的落地写法。评论抓取部分还涉及分页拼接、评论者ID与时间字段提取等典型处理思路,有助于举一反三迁移到其他社交媒体平台,是一份值得参考的实操性案例。
1. 爬微博评论前,先看 weibo_spider 会遇到什么
我们试着把 weibo_spider 这类微博爬虫从“能打开”推到“能稳定跑完一个话题下的所有评论”时,最常见的卡点不是登录,而是评论流的翻页参数和风控边界。同一篇文章的评论,热门区能拿到几十条,全部区却经常在第三页后返回空 data;换个未登录的 cookie,可能直接 403。这不是脚本写错,而是微博把评论分成了热门评论与全部评论两套流,并且对未登录态做了频控。这篇文章面向打算自己实现或改造 weibo_spider 的开发者,讲清评论接口的数据结构、最小抓取脚本、翻页条件,以及怎么让爬取微博评论的过程可暂停、可续跑、可复核。默认你用 Python 3.9+,只依赖 requests 和标准库,这样后续接入任何调度框架都容易。
2. 评论接口的翻页模型:weibo_spider 抓取的数据源头
2.1 评论流不是一条:热门评论与全部评论的差异
写爬取微博评论的代码前,先确认你面对的是哪条流。web 端和移动端接口很不一样,weibo_spider 类项目通常优先选 m.weibo.cn 的评论接口,因为返回体小、分页参数明确。常见接口路径类似/comments/hotflow,也有一部分项目用/comments/flow,两者返回的 data 字段结构几乎一致,但 max_id 的语义不同。热门评论接口通常只返回被顶起来的评论;全部评论接口才返回完整楼层。如果只想要“所有评论”做情绪分析,别把它当成一条流去遍历,否则抓到的永远是热门区。
| 参数 | 热门评论 hotflow | 全部评论 flow / buildComments |
|---|---|---|
| 用途 | 抓高赞、博主精选 | 抓完整楼层 |
| 主要入参 | id, mid | id, mid,部分场景还要 since_id |
| 翻页字段 | max_id | max_id 或 since_id |
| 返回空 list 的场景 | 评论数少时正常 | 翻页到尾部时常见,也会遇到风控 |
| 处理方法 | data 里没有 max_id 就结束 | 空 list 要配合响应码判断是否重试 |
刚开始跑 weibo_spider 时,我一般先分别请求一次热门和全部接口,比较返回条数与max_id的位置。这个动作看起来很笨,却能避免后续把“热门评论”误当成“全部评论”,直接省掉一轮返工。
2.2 一条评论的核心字段
观察一次返回的data['data'],里面每一项大致如下。字段名在不同接口版本略有差异,但 id、text、user、created_at 基本稳定。写 weibo_spider 时建议把原始 dict 整份先落盘,再提取字段,因为评论正文里有嵌套的 url_struct、pic_video 等扩展信息,单独挑字段时容易漏。
{ "id": 4707123456789012, "text": "<a href=\"/n/某用户\">@某用户</a>:这条评论内容", "like_count": 8, "created_at": "2024-05-12 10:24:31", "user": { "id": 123456, "screen_name": "昵称" }, "root_id": 0, "reply_comment_id": 0, "max_id": 15876543210987 }逻辑说明:text 里带 HTML 标签,后面清洗时要去标签;max_id 是下一条评论的游标。这一步最好保留 root_id 以区分楼中楼。注意这里的max_id有时挂在评论对象上,有时挂在data顶层,两种取值方式都要兼容。实际抓取评论时,建议把 id 当作唯一主键,因为同一个用户的多条评论可以完全相同,不能靠 text 去重。
2.3 翻页终止条件要区分“结束”和“风控”
weibo_spider 在爬评论时最忌一遇到空 data 就 break。全部评论流翻到 150 条以后,返回 data 可能为空,但 status 仍是 200;而一旦遭遇频控,接口会返回-100或 403。因此代码里需要三重判断:ok不等于 1 时记录错误;max_id为空或 0 时按正常结束;list为空但max_id不为空时重试。把这三条写进循环,后续排错才不用翻日志。另一个值得注意的点是,data存在但list为空的场景里,响应头里可能带Content-Encoding: gzip,所以 requests 要允许自动解压,否则你看到的是一串字节而不是空列表。
2.4 请求头里哪些字段影响评论接口返回
直接用requests.get打该接口会收到 403,常见做法是带上完整的浏览器请求头。最关键的三个键是 User-Agent、Referer 和 Cookie。Referer 应该指向具体微博页;Cookie 里至少包含 SUB 或 SUBP,否则评论接口可能只返回登录引导。下面给出 HEADERS 模板,用它去请求热门评论等页面都通用。
HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0", "Accept": "application/json, text/plain, */*", "Referer": "https://weibo.com/", "X-Requested-With": "XMLHttpRequest", "Cookie": "换成你自己的 SUB 和 SUBP" }参数说明:这里 Referer 只填了根域,实际使用时可替换成目标正文页地址;X-Requested-With 是给反爬识别用的,不加有时也能通,但加了表现为更像浏览器。Cookie 不要写死在代码仓库里,建议放环境变量,否则换一台机器就要改源码。
3. 用 Python 请求库跑通爬取微博评论的最小脚本
3.1 先确认微博正文 id 和 mid
爬取微博评论需要两个参数:微博正文的 id 和 mid。从 PC 端地址里最容易拿到:/detail/数字那一段通常是 mid;打开浏览器开发者工具,在评论接口请求的 query string 里能看到 id 与 mid。weibo_spider 类项目通常直接解析正文接口返回,但临时脚本里可以写死这两个值来验证。我们要先跑通,再抽象,所以不要让第一步就写配置文件。
3.2 最小循环:从首页评论爬到最后一条
下面这段代码只依赖 requests,完成一次完整评论流的抓取,并把结果按 JSONL 追加保存。注意不要直接用resp.json()["data"]去覆盖列表,因为 data 里可能套着data['list']。
import time import json import requests # HEADERS 沿用上一节的字典,Cookie 必须带上 HEADERS = { "User-Agent": "Mozilla/5.0 ...", "Referer": "https://weibo.com/", "X-Requested-With": "XMLHttpRequest", "Cookie": "SUB=xxx; SUBP=xxx" } def fetch_comments(weibo_id: str, mid: str, session: requests.Session, max_page: int = 50): base_url = "https://m.weibo.cn/comments/hotflow" params = {"id": weibo_id, "mid": mid} seen = set() for page in range(max_page): resp = session.get(base_url, params=params, headers=HEADERS, timeout=10) data = resp.json() if data.get("ok") != 1: # 常见错误码 -100 是频控,不要继续翻页 print("page", page, "failed with", data.get("ok"), data.get("msg")) time.sleep(10) continue card = data.get("data", {}) items = card.get("list", []) if not items: # 有 data 但 list 为空,表示评论流已经到底 break for c in items: cid = c.get("id") if cid and cid not in seen: seen.add(cid) line = json.dumps({ "id": cid, "text": c.get("text"), "like_count": c.get("like_count"), "created_at": c.get("created_at"), "user": (c.get("user") or {}).get("screen_name") }, ensure_ascii=False) with open("comments.jsonl", "a", encoding="utf-8") as f: f.write(line + "\n") max_id = card.get("max_id") if not max_id: break params["max_id"] = max_id time.sleep(2.5) return len(seen)代码逻辑说明:我们把 params 对象直接交给 requests,requests 会处理好 URL 编码;每次翻页后把新的 max_id 放回 params,循环继续。seen 集合用于去重,防止同一条评论重复写文件。这里没有做异常捕获,实际长跑时要包try/except requests.RequestException,否则 SSL 抖动会让整个任务中断。
3.3 参数调整:哪些值需要改成变量
你可能发现自己的评论流第一页能返回、第二页就开始失败,多半是 max_id 没传对。较新的接口里,第一页的 data 对象不一定带 max_id,需要从评论项里的 max_id 字段读;有些实现读的是data['max_id'],两者都要兼容。字段名不统一是微博接口最常见的坑。建议第一次运行时打印data的顶层 key,再决定取值路径。
| 翻页字段可能的位置 | 特征 | 兼容写法 |
|---|---|---|
| data.max_id | 顶层 | 先取 card.get("max_id") |
| data.list[i].max_id | 挂在评论对象上 | 取 items[-1].get("max_id") |
| data.since_id | 旧版接口 | 请求参数用 since_id 而不是 max_id |
如果三种情况都存在,最简单的方式是写一个get_next_cursor(card, items)函数,按顺序尝试上面三个位置,返回值非空即用。这样即使微博切换接口版本,改动也集中在一个函数里。
3.4 楼中楼是否要抓
多数爬取微博评论的场景只需要一层评论,但楼中楼会过滤掉回复。如果要抓,每个评论项的reply_comment_id不为 0 时,可以拿 root_id 去请求回复接口;这里容易死循环,建议不深度递归,只做两层。weibo_spider 项目里通常有专门的 reply 参数,普通分析场景一层就够。抓二层回复时,注意把根评论的 id 一并记录到每一条子评论里,否则后面做关系图时无法映射。
4. 把爬取微博评论做稳:限速、去重和异常恢复
4.1 并发不该超过 3 到 5
微博评论接口的频控会先作用于单条微博,再作用于账号。实测单账号并发超过 6 个时,hotflow 接口会随机返回-100或1002,这是接口层限频,并非封号。更稳的做法是单账号并发控制在 3,每次请求的 sleep 随机在 2~4 秒;如果要跑大量微博,优先加账号池而不是加并发。用requests.Session并对每个 Session 单独配 cookie 即可,Session 会复用 TCP 连接,比每次新建 requests.get 少一截 TLS 握手开销。
4.2 断点续爬:记录 max_id 而不是记录页码
我们常犯的错误是把页码 page 当成断点,但评论流以 max_id 为准。如果第 7 页失败,重启后不需要从第 8 页开始,而是把第 6 页带回来的 max_id 作为起始参数。为了支持断点,脚本应该在每次翻页后,把 weibo_id、mid、max_id 写入一个 checkpoint 文件,覆盖式写入,只保留最新位置。下次启动时读文件,若存在就直接续爬。
def save_checkpoint(weibo_id, mid, max_id): with open("checkpoint.txt", "w", encoding="utf-8") as f: f.write(json.dumps({"weibo_id": weibo_id, "mid": mid, "max_id": max_id})) def load_checkpoint(): try: with open("checkpoint.txt", "r", encoding="utf-8") as f: return json.loads(f.read()) except FileNotFoundError: return None上面两个函数的作用是保存和恢复游标。位置信息不依赖页码,因此进程死掉后重新拉起,最多重复最后一条评论,不会整体重来。要注意每次成功写完一条评论数据后再更新 checkpoint 文件,顺序反了会漏数。
4.3 用 SQLite 去重替代 Set
评论 id 是全局唯一的,用内存 Set 去重在进程重启后会失效。更可靠的做法是建一张 SQLite 表,以 id 为主键,INSERT OR IGNORE。单次任务几十万评论时,SQLite 比 JSONL 更适合做去重主库;JSONL 只做最终归档。下面是一段建表与写入的示例。
import sqlite3 import json conn = sqlite3.connect("weibo_comments.db") conn.execute("CREATE TABLE IF NOT EXISTS comments (id INTEGER PRIMARY KEY, text TEXT, created_at TEXT, screen_name TEXT, raw TEXT)") def save_item(item): raw = json.dumps(item, ensure_ascii=False) conn.execute("INSERT OR IGNORE INTO comments VALUES (?,?,?,?,?)", (item["id"], item.get("text"), item.get("created_at"), (item.get("user") or {}).get("screen_name"), raw)) conn.commit()参数说明:INSERT OR IGNORE意味着评论一旦插入就不会被重复记录,天然适合多轮抓取。raw 字段存整个原始对象,后面字段映射变化时不必回源接口。写入频繁时不要每条都 commit,可以先 100 条一次性 commit,能明显降低磁盘 IO。
4.4 失败需要区分:接口失败 vs 网络失败
requests 抛出的 ConnectionError 和接口返回的 ok != 1 要分开处理。前者通常不影响风控,可以退避 30 秒重试;后者如果是-100,说明短时间内请求太快,要等更久。这里建议对-100做连续 3 次退避,退避时间依次为 10、60、300 秒,超过就写死该微博并切下一条。不要遇到任何异常都直接 break,那样会把大量半截数据当作完成。排错时观察响应头里的X-Rate-Limit-*字段,有时能看到明确的剩余配额,这比盲猜 sleep 时间更有效。
5. 爬取微博评论后,按评论快照校验漏抓字段
5.1 用两次抓取对比产出快照
跑完一轮爬取微博评论不代表数据可信。最简单的验证是隔 30 分钟后重新抓取同一条微博,比较两次都存在的评论 id 数量以及新增数量。中间规则:如果第二次抓到的老评论数量少于第一次的 80%,说明翻页提前终止;如果老评论有缺失,先补 max_id 的取值逻辑。这一步也顺带检查缓存导致的假空数据,因为微博接口的 CDN 边缘节点可能缓存了上一次的响应。
5.2 清理 text 中的 HTML 标签
微博正文里的<a>与 emoji 字符,会让后续分词变得很难看。稳健的清洗方式是先替换换行和 @ 链接,再去标签,最后把 HTML 实体 unescape。注意不要过度清洗,以免直接丢 URL。这样评论里带的网页链接在情绪分析时可单独抽出来,不会被当成普通词。
import re import html def clean_text(text): text = re.sub(r"<br\s*/?>", "\n", text) text = re.sub(r"<[^>]+>", "", text) text = html.unescape(text) return text.strip()这段用正则把<br>替换成换行,再去掉剩余标签,最后把&这类实体还原。参数上不需要额外调,只提醒一点:如果评论里有视频卡片的 URL,标签去掉后链接会保留;不想让它参与情绪分析,可以在词表中过滤。
5.3 校验时间窗口与数据边界
如果你的评论快照只有 30 天迁移数据,而接口里最早评论还没到底,检查是否把 max_id 忽略了。最后一步,把评论 id 的量级和日期范围用 SQL 跑出来,范围在合理区间内,比盯着日志看到“成功”有价值。
SELECT COUNT(*), MIN(created_at), MAX(created_at) FROM comments WHERE weibo_id = ?;如果 max(created_at) 离当前时间超过发布时间的合理窗口,说明第一次抓取时可能拿到的是缓存结果;如果 min(created_at) 比微博发布时间还早,就要检查是不是用了旧的 max_id 去续爬。这条 SQL 本身很简单,但它能把“看起来成功”和“真抓全”分开,挡住大多数翻页中断产生的数据空洞。
本文还有配套的精品资源,点击获取