snscrape 脚本突然不能用了?这是迁移方案
snscrape 脚本突然不工作了?这是迁移方案
跑了好几个月,你一行没动,现在它返回空列表,或者在库内部某处抛异常。你的代码什么都没改。
这是 snscrape 的标准故障模式。挑替代方案之前值得先搞懂它——因为如果你选错了类型,同样的事还会再发生一遍。
下面讲:它为什么会坏、现实的选项有哪些,以及几乎每个 snscrape 脚本都由之组成的三种写法要怎么逐行改。
只想要能跑的版本就直接跳到迁移部分。有个免费 demo 端点,不注册就能先对一下返回结构。
它为什么会坏,以及这不是维护者的错
snscrape 读的是 X 的内部网页端点——网站自己调的那套。那些端点从来就不是公开契约:GraphQL query ID 会轮换、必需的请求头会变、响应结构会在没有通知的情况下重组。
X 一次前端更新,可能同时打断三件事:
| 变的是什么 | 你看到什么 |
|---|---|
| GraphQL query ID 轮换 | 昨天还好好的调用开始 404 |
| 必需请求头变了 | 401,或者 HTTP 200 但结果是空的 |
| 响应结构重组 | 解析器什么都没返回,也不报错 |
第三行是最危险的。 静默的空结果,看起来和"这个账号没有推文"一模一样。如果你的管道是定时跑的,表停止增长好几天都可能没人发现。
所以第一个决定不是"用哪个库",而是——X 改东西的时候,谁来兜底。 自己维护的爬虫,答案是你,在它半夜坏掉的那个点。
现实的选项
四个,老实说:
1. 继续打补丁(或者用某个 fork)。 免费、完全可控,而且一直能用——直到不能用。如果 X 数据只是业余项目的一路辅助输入、停几天无所谓,这完全合理。依赖某个 fork 前先看它的提交活跃度:废弃的 fork 和健康的长得一模一样,直到你需要修复的那一刻。
2. 官方 X API。 如果你需要写(发推、回复、关注、删除),这是唯一正当的路。读的话按条计费,免费额度小到大多数人测试阶段就用完了。采纳或否决之前,建议认真按自己的量算一遍。
3. 自己照着内部端点造。 你会把 snscrape 已经做过的事重做一遍:query ID、请求头构造、请求签名、翻页游标、解析。代码本身不难,维护才是全部成本,而且是持续性的。
4. 托管的只读 API。 别人替你吸收破坏,你调一个稳定接口。代价是付钱,以及失去自己动手修的能力。这正是我们做的事,所以下面这段请当作"有利益相关方写的"来读——不过迁移代码本身跟用谁无关。
如果你需要写 X,上面的只读选项一个都替代不了官方 API。 没有任何第三方只读服务能以你的身份发推,声称能的一定是在用某人的登录态。
迁移
几乎所有 snscrape 脚本都是三种写法之一。下面逐个给出改法。
写法 1:按关键词搜索
经典的 snscrape 循环:
import snscrape.modules.twitter as sntwitter
tweets = []
for i, t in enumerate(sntwitter.TwitterSearchScraper("bitcoin").get_items()):
if i >= 100:
break
tweets.append({"date": t.date, "user": t.user.username, "content": t.rawContent})
改成用 requests 调数据 API:
import requests
API = "https://api.socialapi.tech/v1"
HEADERS = {"X-API-Key": "your_key"}
def search(query, limit=100):
r = requests.get(f"{API}/search/advanced",
params={"query": query, "limit": limit},
headers=HEADERS, timeout=30)
r.raise_for_status()
return r.json().get("data", [])
tweets = [
{"date": t["created_at"], "user": t["author"]["username"], "content": t["text"]}
for t in search("bitcoin", 100)
]
字段对照表:
| snscrape | 替代写法 | 说明 |
|---|---|---|
t.date |
t["created_at"] |
是字符串不是 datetime,用 "%a %b %d %H:%M:%S %z %Y" 解析 |
t.rawContent |
t["text"] |
|
t.user.username |
t["author"]["username"] |
作者内联嵌套,不用第二次请求 |
t.id |
t["id"] |
是字符串不是 int——19 位会撑爆 JS 的数字 |
t.likeCount |
t["like_count"] |
|
t.retweetCount |
t["retweet_count"] |
|
t.replyCount |
t["reply_count"] |
|
t.viewCount |
t["view_count"] |
|
t.lang |
t["lang"] |
|
t.sourceLabel |
t["source"] |
完整的响应结构——每个字段、哪些会是 null、什么时候是——在推文 JSON 逐字段拆解里。
写法 2:某个用户的时间线
# snscrape
scraper = sntwitter.TwitterUserScraper("elonmusk")
items = list(itertools.islice(scraper.get_items(), 200))
# 替代
def user_tweets(username, limit=200):
r = requests.get(f"{API}/user/last_tweets",
params={"username": username, "limit": limit},
headers=HEADERS, timeout=30)
r.raise_for_status()
return r.json().get("data", [])
items = user_tweets("elonmusk", 200)
写法 3:翻过第一页
大多数迁移是在这里出错的,因为 snscrape 的生成器把翻页完全藏起来了——你只管一直迭代就行。
# snscrape: 翻页是隐形的
for t in sntwitter.TwitterSearchScraper(q).get_items():
... # 一直走到你自己 break
换成 HTTP API,翻页要显式用游标:
def search_all(query, want=1000, page=100):
out, cursor = [], None
while len(out) < want:
params = {"query": query, "limit": min(page, want - len(out))}
if cursor:
params["cursor"] = cursor
r = requests.get(f"{API}/search/advanced", params=params,
headers=HEADERS, timeout=30)
r.raise_for_status()
body = r.json()
batch = body.get("data", [])
if not batch: # 没有更多结果
break
out.extend(batch)
cursor = (body.get("meta") or {}).get("next_cursor")
if not cursor: # 没有下一页
break
return out
这里有两处必须写对: 批次为空和游标为空,两个都要 break——只判其中一个,结果要么是死循环要么是结果被截断。另外别拿"这页满了"当作"还有下一页",要看游标。
如果你是横向跨很多账号而不是纵向深挖一个,把它们合并成一次请求是另一种量级的节省——账在批量化 X API 请求里算过。
snscrape 没逼你写的错误处理
snscrape 抛的是库异常。HTTP API 给的是状态码,反而更好对症处理:
import time
def call(path, params, attempt=0):
r = requests.get(f"{API}{path}", params=params, headers=HEADERS, timeout=30)
if r.status_code == 429: # 被限流
if attempt < 4:
time.sleep(2 ** attempt) # 指数退避
return call(path, params, attempt + 1)
raise RuntimeError("重试后仍被限流")
if r.status_code == 401:
raise RuntimeError("API key 不对")
if r.status_code == 402:
raise RuntimeError("额度用完了")
r.raise_for_status()
return r.json()
429 退避重试,401 和 402 直接失败。重试一个错的 key 纯属浪费时间,重试一个空余额永远不会成功。
你实际放弃了什么
这点值得说直白,因为取舍是真实的。
你放弃了自己动手修的能力。 用 snscrape,坏了很烦但归你管——你可以读源码、打补丁、继续走。用托管 API,你提工单然后等。如果这点对你的项目不可接受,选项 1 或 3 才是更好的选择。
你放弃了"免费"。 snscrape 只花你的时间。任何托管服务都要钱。问题只在于你的时间值不值那个账单。
你换来的是不会被它半夜叫醒。 整个卖点就这一句,说完了。
信任它之前先验证迁移
两边并行跑一天,然后对比——不是对比条数,是对比 ID:
old_ids = {t.id for t in snscrape_results}
new_ids = {int(t["id"]) for t in api_results}
print("只在 snscrape 里:", len(old_ids - new_ids))
print("只在新 API 里 :", len(new_ids - old_ids))
print("重叠 :", len(old_ids & new_ids))
不要期待完美重叠。 搜索结果本来每次拉都会有差异——排序会变、有删帖、实时结果在动。你要找的是系统性缺失:整个账号不见了,或者某个日期之前的一条都没有。
我们的 API 返回的就是本文通篇用的那套结构,所以迁移后的代码原样就能跑。用 Google 一键注册送 $0.20 额度、不用信用卡,足够你把上面那个对比跑完再决定。
常见问题
snscrape 死了吗? 代码还在,fork 也有人在用,但它读的是没有文档、随时会变的端点。请预期周期性损坏,并想清楚谁来修。"死了"这个说法不准确,更贴切的是"相对 X 的变化速度而言维护跟不上"。
snscrape 为什么突然就不能用了? 基本都是 X 那边变了:query ID 轮换(你看到 404)、请求头要求变了(401 或者静默返回空)、响应结构重组(解析器什么都不返回且不报错)。你的代码没变,它底下的契约变了。
snscrape 为什么返回空列表而不是报错? 因为 HTTP 请求本身成功了,只是解析没找到东西。端点可以返回 200,但结构是你的解析器不认识的。这是最危险的故障模式——它看起来和"确实没有结果"一模一样。
2026 年 snscrape 最好的替代是什么? 取决于维护责任该归谁。想要控制权、能承受停机就用自维护的 fork;需要写操作就用官方 API;想让破坏变成别人的问题就用托管只读 API。
没有 API key 还能抓推文吗? 公开数据不需要 X 的凭据就能读,这正是 snscrape 在做的事。第三方 API 需要的是它自己的 key,不是 X 的。不认证成用户就做不了的事是:写操作,以及读任何受保护的内容。
抓取推特合法吗? 读公开数据,和用别人的凭据做自动化、或访问私密内容,是不同性质的事。实践中的红线是:不用别人的凭据、不碰受保护内容、不要攻击级流量。数据拿到之后怎么用另有合规义务——这不构成法律意见。
TwitterSearchScraper 怎么替换?
换成一次搜索端点调用,查询词作为参数传。原来 get_items() 的循环要改成显式的游标翻页循环,因为生成器之前把翻页藏起来了。
TwitterUserScraper 怎么替换? 换成用户时间线端点,传用户名。要拉超过一页同样用游标翻页。
t.rawContent 对应什么?
text 字段。t.date 对应 created_at(是推特自有格式的字符串,不是 datetime),t.user.username 对应嵌套 author 对象上的 author.username。
推文 ID 为什么现在是字符串? 它是 19 位的 snowflake,超出 JavaScript 的安全整数范围。snscrape 给你的是 Python int,在 Python 里没问题,但一旦进 JSON 或 JS 就会损坏。字符串是可移植的选择。
没有 snscrape 的生成器,怎么翻页?
当还想要更多时循环,把上一次响应的 cursor 传进去,批次为空和游标缺失两种情况都要 break。只判一种会导致结果截断或死循环。
snscrape 抓 Telegram、Reddit 还能用吗? snscrape 覆盖多个平台,各自的稳定性互相独立——X 模块坏了不代表别的也坏。本文只讲 X 这一侧。
有没有 drop-in 的替代库? 没有库级别的无缝替换,因为迁移的全部意义就是不再依赖无文档端点。不过迁移过程是机械的:三种调用写法加一张字段改名表,也就是这篇文章的内容。
替换 snscrape 要花多少钱? 按调用计费的话,你用多少付多少,所以诚实的答案是"看量"。算出你每月的调用次数乘一下——如果比你打补丁花的工时便宜,这个决定就不用做了。
迁移期间能并行跑 snscrape 和新 API 吗? 能,而且应该这么做。两边跑一天,对比推文 ID 而不是条数。不要期待完美重叠,要找的是系统性的缺口。
snscrape 和 tweepy 有什么区别?
tweepy 是官方 API 的客户端,需要开发者凭据。snscrape 读公开网页端点,不需要凭据。访问模型完全不同,所以迁移路径也取决于你原来用的是哪个。
托管 API 会不会以同样的方式坏掉? 底层端点对所有人都会变,区别在于谁来修。托管服务那边修好了直接生效,你什么都不用做;自己的代码,半夜读 diff 的那个人是你。
迁移后怎么处理限流? 429 指数退避,401 和 402 不要重试——key 错了或余额空了,重试多少次都不会变好。更完整的讲法在限流到底意味着什么。
snscrape 停摆后还能拿到历史推文吗? 取决于替代方案。深度历史归档是所有来源里最难便宜拿到的东西——如果你的场景需要回补好几年而不是近期数据,请单独按这个需求去问价,别默认它包含在内。
迁移时数据库表结构要改吗? 通常只有 ID 那一列——如果你把推文 ID 存成了整数,改成字符串或 64 位类型。其余字段一一对应,名字变了含义没变。
不用官方 API 怎么抓推特数据? 要么自己跑抓取库、然后随着 X 变化一直维护它,要么调一个托管的只读 API 让别人维护。取舍是「维护负担」换「按请求付费」——不存在「没人承担坏掉的成本」这个选项。
能把一个人的所有推文都抓下来吗? 可以向后翻页,但不能无限翻——时间线有深度上限,换什么工具都一样。真要完整历史,得用按日期切片的搜索,而不是一次深挖时间线。
怎么按关键词抓而不是按账号抓? 换成搜索端点而不是时间线端点。翻页模式完全一样,只有查询参数不同。 值得先学会收窄查询的操作符——它削减数据量的效果比任何代码改动都大。
2026 年用 Python 抓推特还可行吗? 可行,但那些多年无人看管也能跑的库,现在会在 X 轮换内部实现时坏掉。变的不是可行性,是「谁来承担坏掉之后的修复」。
为什么我的爬虫昨天还好、今天就挂了? 几乎总是上游变了:轮换了 query ID、多了必需请求头、或者响应结构挪了位置。你的代码一行没改——这恰恰是这类故障持有成本高的原因。
迁移是几个小时的工作量,换来的是下次 X 再改东西时不需要你出现的代码。用 Google 注册送 $0.20 额度、不绑卡,够你把一个脚本移过来、拿输出和 snscrape 原来给你的对一遍。