导出粉丝列表:两种模式,成本差 18 倍
导出粉丝列表:两种模式,成本差 18 倍
大多数人写粉丝导出,是在跑到第 5 万个账号左右才发现问题:任务跑了一个小时、离结束还远,账单比预想的大。
原因几乎总是同一个——该用 ID 的地方拉了完整资料。两种模式每个粉丝的字节数差约 18 倍,而大多数导出场景只需要便宜的那种。
这篇讲清楚两种模式、写错就会悄悄丢行的游标翻页,以及开工之前怎么判断自己要哪种。
下面两个响应都是写这篇时现场调出来的。有个免费 demo 端点,不注册就能先对结构。
两种模式
完整资料——每个粉丝的全部信息
GET /v1/user/followers?username=naval&limit=3
每一行是一个完整的用户对象:
{
"id": "1234567890",
"username": "someone",
"display_name": "Someone",
"bio": "...",
"avatar": "https://pbs.twimg.com/...",
"followers_count": 412,
"following_count": 180,
"statuses_count": 2041,
"created_at": "Tue Jun 02 20:12:29 +0000 2009",
"location": "",
"url": "https://x.com/someone",
"blue_verified": false
}
每个粉丝 12 个字段。要做打分、分层、筛选时才需要——没拉 followers_count 就没法按粉丝量筛。
只要 ID——只有 handle 和数字 ID
GET /v1/user/followers_ids?username=naval&limit=5
{
"data": [
{"id": "2119051387224879104", "username": "gabrielmazarin_"},
{"id": "2119052145832210433", "username": "tamim7zrg"},
{"id": "2119051805015384064", "username": "Wendyzik5"}
],
"meta": {"count": 5, "next_cursor": null, "pages_fetched": 1, "username": "naval"}
}
两个字段。这正是它的意义所在。
体积差距就是决策依据。 一行完整资料算上 bio 和头像 URL 远超一百字节,一行 ID 只有几个字节。放到一百万粉丝上,这个差距就是"随手跑一下"和"排到夜里跑"的区别。
你到底需要哪一种
从"拿到文件之后要干什么"倒推:
| 你要做的事 | 模式 | 为什么 |
|---|---|---|
| 今天的名单和昨天对比 | ID | 你只需要集合成员关系 |
| 找出谁取关了 | ID | 同上,就是个集合差 |
| 喂给另一个自己会解析资料的系统 | ID | 别付两遍钱 |
| 按粉丝量/活跃度打分 | 完整 | 你需要那些计数 |
| 按 bio 关键词或地区分层 | 完整 | 你需要文本 |
| 小账号(<5k)一次性导出 | 都行 | 量太小,无所谓 |
最常见的错误是:为了做差集而拉完整资料。 如果你在追踪粉丝变化,比的是 ID 集合——bio 和头像在每一轮都是纯粹的累赘。变化检测那一侧在构建粉丝追踪器里讲过。
翻页:悄悄丢行的那部分
两个端点都是游标翻页,而且游标行为有个值得注意的差异。
看上面两次真实调用的 meta:
// followers (完整资料)
"meta": {"count": 3, "next_cursor": "1881204516267795489"}
// followers_ids
"meta": {"count": 5, "next_cursor": null, "pages_fetched": 1, "username": "naval"}
ID 端点会报 pages_fetched,因为它内部会自己翻页——一次调用可以覆盖很大范围。资料端点则是每页给你一个游标,由你来驱动。
一个两种都能处理的循环:
import requests
API = "https://api.socialapi.tech/v1"
HEADERS = {"X-API-Key": "your_key"}
def export_followers(username, ids_only=True, page=200, cap=None):
path = "/user/followers_ids" if ids_only else "/user/followers"
out, cursor, seen = [], None, set()
while True:
params = {"username": username, "limit": page}
if cursor:
params["cursor"] = cursor
r = requests.get(f"{API}{path}", params=params, headers=HEADERS, timeout=60)
r.raise_for_status()
body = r.json()
batch = body.get("data") or []
if not batch: # 没有更多行了
break
for row in batch: # 跨页去重
if row["id"] not in seen:
seen.add(row["id"])
out.append(row)
if cap and len(out) >= cap:
break
cursor = (body.get("meta") or {}).get("next_cursor")
if not cursor: # 没有下一页
break
return out
这里有三处必须写对,每漏一处都会丢行:
两个条件都要 break。 批次为空和游标缺失。只判一个,要么死循环,要么文件被截断——而被截断的文件看起来和"这个账号粉丝本来就没那么多"一模一样。
按 ID 去重。 你翻页的过程中粉丝列表在动——有人关注、有人取关,窗口在滑。同一个账号可能出现在相邻两页上。 没有那个 seen 集合,你会拿到重复行,而且总数对不上。
别指望总数能对上。 资料页上的 followers_count 是缓存值,你导出的是一次实时遍历。大账号上千分之几的差距是正常的,不是你循环写错了。
如果你是横向导出很多个账号而不是纵向深挖一个,批量化请求会从另一个方向改变这笔账。
写文件
ID 模式,一个扁平 CSV 就够:
import csv
rows = export_followers("naval", ids_only=True)
with open("followers.csv", "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["id", "username"])
for r in rows:
w.writerow([r["id"], r["username"]])
ID 必须当文本存。 这些是 19 位的 snowflake ID,超出 JavaScript 的安全整数范围,也超出表格软件能保留的精度。用 Excel 打开那个 CSV,它会很干脆地把最后几位抹平——ID 看起来没问题,但是错的。 如果文件要给人看,前面加 ' 或者直接写成 .tsv。
完整资料模式下,bio 里有换行和逗号,所以要正确加引号(csv 模块会处理),并且提前决定是保留原始文本还是压平。更大导出的格式取舍在导出 X 数据到 CSV 里讲过。
导不出来的东西
这几点值得说直白,因为每次都有人问:
- 受保护账号的粉丝——没获批的人谁都拿不到,换什么工具都一样。
- 邮箱地址——平台从不暴露。任何声称能把邮箱和 handle 对上的服务,都是在拿别的数据集做匹配,准确性和合法性自己掂量。
- 某个粉丝的完整历史——你拿到的是他的资料,不是时间线。那是每个账号一次单独请求,放到大粉丝列表上,这笔账基本算不过来。
- 粉丝关注的精确顺序——列表大致是最新在前,但没有可靠的逐条关注时间戳。
跑大账号时
超过几十万的,有三条实务提醒:
把游标存盘。 每页都写一次。任务在 80% 挂掉时你是续跑而不是重来——大账号上重来就是好几个小时。
预期数字会动。 两百万的名单永远不是一个干净快照,而是在一个流动集合里走了一遍。要严格的时间点视图,就取两次完整遍历的差,别试图把一次冻住。
限流是按账号算的,不是按 key 算的。 把请求摊到账号池上的服务商会比绑死单一凭据的快。导出慢得离谱通常就是这个原因——签约前值得直接问清楚。
我们的 API 返回的就是这篇文章里的结构,上面那个循环原样就能跑。用 Google 一键注册送 $0.20 额度、不用信用卡,足够你导一个中等账号、看清真实成本再决定。
常见问题
怎么把推特粉丝导出成 CSV? 用游标翻页遍历 followers 端点,收集结果,用 CSV writer 写出来。除非你需要资料字段,否则用只要 ID 的模式——同样的名单体积小得多。
followers 和 followers_ids 有什么区别? 前者每个粉丝返回完整用户对象(12 个字段,含 bio、各项计数、头像),后者只返回数字 ID 和用户名。按"拿到文件要干什么"来选。
只要 ID 能便宜多少? 每个粉丝的字节数差大约一个数量级,因为不用带 bio、头像 URL 和计数。小账号无所谓,过了几十万就决定这活能不能干。
没有 X 开发者账号能导出粉丝吗? 能,用第三方只读 API——你用的是服务商的 key,不是 X 的凭据。开发者账号是官方 API 才需要的,而官方 API 主要用于写操作。
怎么查谁取关了我? 定时导出 ID 名单,取相邻两次的集合差。这个场景不需要完整资料,这正是 ID 模式存在的意义。
导出的数量为什么和资料页的粉丝数对不上? 资料页的数字是缓存值,你的导出是在一个边读边变的列表里实时遍历。大账号上有小幅差异是预期行为,不是 bug。
为什么同一个粉丝出现了两次?
因为翻页过程中列表移动了。收集时按 ID 去重——一个 seen 集合不花什么成本,能彻底消除这个问题。
followers 端点怎么翻页?
把上一次响应的 next_cursor 作为 cursor 传回去,批次为空或游标缺失时停止。只判其中一个会导致文件被截断或者死循环。
响应里的 pages_fetched 是什么? ID 端点内部会自己翻页,这个字段报告它为你这次调用覆盖了多少底层页。是信息性的——你的循环仍然跟着游标走。
能导出别人账号的粉丝吗? 公开账号可以,粉丝列表是公开信息。受保护账号不行,用什么工具都不行。
能拿到粉丝的邮箱吗? 不能。平台从不暴露邮箱。声称能做到的,是在拿别的数据源做匹配。
导出一百万粉丝要多久? 完全取决于页大小和服务商把请求摊到多少个账号上。问并发能力,而不是问原始限流数字——前者才决定真实耗时。
粉丝 ID 该存成数字还是字符串? 字符串。19 位 snowflake ID 超出 JavaScript 安全整数范围,也会被表格软件静默抹平。存成数字会在不报错的情况下损坏数据。
Excel 为什么把我的粉丝 ID 弄乱了? 它把长数字串当数字解析,15 位之后丢精度。把那一列写成文本、前面加单引号,或者干脆给 TSV。
导出失败能续跑吗? 如果你每页把游标存了盘,可以——从存的游标接着跑。没存就得从零重来,大账号上很痛苦。
导出粉丝违反 X 的条款吗? 读公开数据,和用别人的凭据做自动化、或访问受保护内容,是不同性质的事。实践中的红线是:不用别人的凭据、不碰受保护内容、不要攻击级流量。数据拿到之后怎么用另有合规义务。
追踪变化的话多久导一次? 按账号变动速度定。大多数账号每天一次就够,涨得快的可以更密。每次都是一次完整遍历,所以成本是按次算的,不是按变化量算的。
导出时能按粉丝量或 bio 筛选吗? 只有完整资料模式可以,因为筛选需要那些字段。如果筛得很狠,先拉 ID、再对子集拉资料通常更便宜。
导到一半账号被封了会怎样? 后续页会报错,而不是返回残缺数据。显式处理这个错误并保留已收集的部分,别让一次失败把整轮成果丢掉。
粉丝列表有顺序吗? 实际上大致是最新在前,但没有可靠的逐条关注时间戳。不要做依赖精确顺序、或靠它推断关注时间的功能。
除了粉丝,能导出"关注中"吗? 可以,有对应的端点。同样的游标翻页、同样的两种模式取舍、同样需要去重。
我已获批关注的私密账号,能导出它的粉丝吗? 不能。获批让你在 App 里能看到这个账号,但不会让它的粉丝列表通过 API 可取。 受保护就是受保护,和你跟它的关系无关。
导出中途被人拉黑会怎样? 那一行在后续页里就不再出现。你不会收到任何报错——这正是「与上一次导出对账」比「相信单次导出」更重要的原因。
要找出没有互关的人,该导粉丝还是导关注? 两个都导,然后取集合差。关注减粉丝 = 没回关你的;反过来 = 你没回关的。
怎么一次导出好几个账号的粉丝? 循环账号,每个账号一个独立状态文件、各自一个游标。千万别让多个账号共用一个游标——它们索引的是不同列表,会把两边都截断。
这篇的那笔账就是全部决策:按「拿到文件要干什么」选模式,成本随之确定。 用 Google 一键注册送 $0.20 额度、不绑卡——够你拿一个中等账号把两种模式各导一次,在自己的账单上看清差别。