把推特数据导出成 CSV:能拿到什么,以及文件里的两个坑
把推特数据导出成 CSV:能拿到什么,以及文件里的两个坑
"导出推特数据"其实是两个不同的问题,而人们通常低估第二个。
把数据取出来。 走哪条路,以及能有多完整。
写出一个能正确打开的文件。 ★导出真正出问题的地方是这里★ —— 而且它是静默失败的:文件看起来没事,直到有人用 Excel 打开,发现列全错了。
两部分这篇都讲。后半部分几乎没人写。
按目标看,你到底能导出什么
你自己的推文 —— 完整
设置 → 你的账号 → 下载你的数据存档。
★这是唯一真正完整的导出。★ 从你注册那天起的每一条推文、回复、私信和点赞。等几小时到一两天,拿到一个 ZIP,里面有可浏览的 HTML 和原始 JSON。
★如果你要导的是自己的历史,别往下读了,直接去申请这个。★ 在完整性上没有任何东西比得过它,而且免费。
它的局限: 它是快照。明天发的推文需要重新申请一次。
对别人的数据,不存在等价物 —— 那正是接口填补的缺口,但要注意下面说的完整性限制。
别人的推文 —— 部分,而且有上限
对你不拥有的账号,不存在存档申请。 你只能翻他的时间线,导出你够得到的部分。
⚠️ ★你无法导出某人的完整历史。★ X 的索引没法无限往回,读它的任何东西也一样。预期是几个月,不是几年 —— 见现实中能够到多远。
粉丝与关注列表 —— 完整但昂贵
分页的,所以大号意味着很多次请求。一个 50 万粉丝的账号是 5,000 页。技术上没问题,商业上有影响 —— 翻页量正是成本的来源。
搜索结果 —— 受结果上限限制
⚠️ 搜索导出受每条查询的上限限制,而不是受"有多少条匹配"限制。 用一条查询导六个月,你只会拿到其中一小部分。要么把区间分块,要么你的导出就是静默残缺的。
怎么正确地写出文件
下面是真正会咬人的部分。 这两个不是理论问题 —— 它们是让一次导出看起来成功、实际是错的那两个 bug。
★坑一:公式注入★
CSV 里以 =、+、-、@ 开头的单元格,会被 Excel 和大多数表格软件当作公式执行。
推文正文是任意的用户输入。 有人可以把显示名或正文写成以 = 开头,于是你的导出文件变成一个打开就执行它的表格。⚠️ 一个名叫 =HYPERLINK("http://evil","click") 的代币出现在你的导出里,意味着打开文件的人会看到一个活链接,而它看起来像是你自己的数据。
修复只有一行: 给以这些字符开头的单元格加一个单引号前缀。
★坑二:编码★
把 CSV 写成纯 UTF-8,然后在 Windows 的 Excel 里打开 —— 每一个非英文字符都会变成乱码。中文、日文、阿拉伯文、带重音的欧洲文字,全都会。
修复: 写入 UTF-8 BOM。Excel 用它来判断编码。没有它,Excel 会去猜系统代码页,而且会猜错。
★这两个 bug 有一个共同属性:导出过程是成功的。★(取数那一半是一个分页调用;这一半完全是你自己的活。) 什么错都不报。你是在别人打开文件时才知道的,而那通常已经在你把文件发给他之后。
正确的做法
import csv, requests
BASE = "https://api.socialapi.tech"
KEY = "your_api_key"
HDRS = {"X-API-Key": KEY}
DANGEROUS = ("=", "+", "-", "@")
def safe(value):
"""中和公式注入。推文正文是不可信输入。"""
s = "" if value is None else str(value)
return "'" + s if s.startswith(DANGEROUS) else s
def export_posts(username, path, pages=10):
rows, cursor = [], None
for _ in range(pages):
params = {"username": username, "limit": 100}
if cursor:
params["cursor"] = cursor
r = requests.get(f"{BASE}/v1/user/last_tweets",
params=params, headers=HDRS, timeout=60)
r.raise_for_status()
body = r.json()
batch = body["data"]
if not batch:
break
rows.extend(batch)
cursor = body.get("meta", {}).get("next_cursor")
if not cursor:
break # ★唯一正确的停止条件★
# utf-8-sig 会写入 Excel 需要的 BOM
with open(path, "w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f, quoting=csv.QUOTE_ALL)
w.writerow(["id","created_at","text","likes","reposts",
"replies","quotes","views","url"])
for p in rows:
w.writerow([
safe(p["id"]), safe(p["created_at"]), safe(p["text"]),
p.get("like_count",0), p.get("retweet_count",0),
p.get("reply_count",0), p.get("quote_count",0),
p.get("view_count") or 0,
safe(f"https://x.com/{username}/status/{p['id']}"),
])
return len(rows)
print(f"导出了 {export_posts('nasa', 'nasa.csv')} 条")
这里有三处在真正干活:
- ★
utf-8-sig★ 写入 BOM。一个参数,决定了文件是可读还是乱码。 - ★
QUOTE_ALL★ —— 推文正文里经常有逗号、引号和换行。不加引号的字段会在第一个逗号处把表格撕开。 - ★每个文本字段都过
safe()★,作用在值上,不是表头上。
⚠️ 不要因为"我的数据是干净的"就跳过 QUOTE_ALL。 推文正文是任意的 —— 一条含逗号的推文会裂成两列,并把那一行后面的每个字段都错位。
选什么格式
| 格式 | 适合 | 要注意 |
|---|---|---|
| CSV | 表格、发给不写代码的人 | 上面那两个坑。不支持嵌套数据 |
| JSON | 喂给另一个系统、保留结构 | 不适合表格软件 |
| JSONL | 大批量导出、持续追加 | 同上,外加一行一个对象 |
| Excel (.xlsx) | 需要格式化 | 要用库;★以文本写入时天然免疫公式注入★ |
★任何周期性的导出,JSONL 都胜过 CSV★ —— 它能干净地追加、能在中断后幸存,而且没有转义问题。只在需要人来读的时候再转成 CSV。
大家会问的问题
怎么导出我的推文? 设置 → 你的账号 → 下载你的数据存档。完整、免费,而且是唯一完整的选项。
怎么下载某个用户的全部推文? 翻他的时间线并写出结果。你能拿到的是索引里还留着的部分,通常是几个月而不是几年。
能把推文导到 Excel 吗?
能 —— 写 CSV 时带 UTF-8 BOM,或者直接写 .xlsx。没有 BOM,非英文内容一定乱码。
怎么导出我的推特粉丝? 翻粉丝列表并逐条写出。大号需要很多次请求。
能导出推特列表吗? 列表成员和其它账号列表一样可读,所以同一套"翻页+写出"的做法适用。
怎么导出搜索结果? 跑搜索、翻页、写出 —— 但要把时间区间分块,否则上限会静默截断你的导出。
该用什么格式导? 给人看用 CSV,给系统用 JSON 或 JSONL。持续追加的场景用 JSONL。
我的 CSV 为什么显示乱码?
缺 UTF-8 BOM。 用 utf-8-sig 写入,Excel 就能正确读取。
我的表格为什么执行了推文里的公式?
公式注入 —— 以 =、+、-、@ 开头的单元格会被执行。写入时给这类单元格加单引号前缀。
我的 CSV 列为什么错位了?
字段含逗号却没加引号。 用 QUOTE_ALL;推文正文里逗号非常常见。
一次能导出多少条推文? 是分页的,所以问题是你愿意取多少页,而不是单次请求的硬上限。
能导出已删除的推文吗? 只有你在删除之前捕获过才行 —— 见什么能找回。
怎么导出推特分析数据? 自己的从分析界面导。跨账号的公开互动数据要从导出的推文里自己算 —— 各指标是什么意思。
能定时自动导出吗? X 界面里不能。用代码的话,定时跑你的导出并追加。
怎么导出私信? 只能通过你自己的数据存档。 它们是私密的,没有其它途径。
数据存档里有什么? 推文、回复、私信、点赞和账号历史 —— 可浏览的 HTML 加原始 JSON。要做处理的话 JSON 才是有用的那部分。
存档要等多久? 通常几小时,有时一两天。准备好了 X 会发邮件。
能导出别人的粉丝吗? 公开账号可以 —— 列表是公开的,而且是分页的。
怎么按时间范围导出推文?
加 since: 和 until:,并且把区间切成小到不会撞上限的窗口。
我的导出为什么少了推文? 两个常见原因:★在"短页"处停止翻页而不是在游标为空时停★,或者时间区间撞上了结果上限。两者都是静默截断。
能批量导出很多账号吗? 迭代就行。成本随总页数增长,不随账号个数增长。
导出推特数据被允许吗? 读取公开数据是被广泛实践的。但无论你从哪获取,再分发都受 X 条款限制 —— 见合规讨论。
怎么导出推特关注列表? 对 followings 端点翻页,把每个账号写进 CSV。 ★一份大列表是很多页,不是一个调用★ —— 要按游标翻页来规划。
怎么导出我的粉丝列表? 对 followers 端点用同样的模式。 资料页上看到的数字就是你该预期的行数,减去期间已注销的账号。
能导出别人的粉丝列表吗? ★公开账号可以★ —— 那份列表本来就是公开的。受保护账号不行。
怎么导出推特帖子? 对账号时间线翻页并序列化。 ★真正的约束是存档深度★ —— 能往回翻多远。
怎么从推特抓数据? 要么驱动一个浏览器并持续维护它,要么调接口。 完整的取舍。
有推特抓取接口吗? 一个读取接口实际上就是。 ★区别在于:一个会在页面标记变化时崩掉,另一个不会。★
导出一个大账号最好的方式是什么? ★按游标翻页、边走边记录检查点、失败后从游标续跑★,而不是从头重来。
能直接导成 CSV 吗? 能 —— 响应是 JSON,转 CSV 是本地变换。★如果文件要用 Excel 打开,记得加 UTF-8 BOM★,否则非拉丁文本会乱码。
CSV 里以 = 开头的字段要注意吗?
★要 —— 前面加一个单引号。★ 显示名是任意文本,而一个以 = 开头的名字在表格软件里打开时会作为公式执行。
怎么导出搜索结果? 对搜索端点用同样的翻页方式。 ★搜索能往回覆盖的范围比用户时间线更浅。★
能定时重复导出吗? 能,而且★对任何你可能会失去的东西,都应该这么做★ —— 删除之后是无法追回的。
一个账号的完整导出有多大? 作为 JSON 大约每条帖子几百字节。 几万条帖子仍然是个小文件。
导出包含媒体文件吗? ★是 URL,不是二进制文件本身。★ 下载文件是你自己控制的另一个步骤。
能导出互动计数吗? 能 —— 每条帖子上都带,★但它们是抓取那一刻的快照,不是一份历史★。
怎么从一个已注销的账号导出数据? ★做不到。★ 注销会移除账号及其帖子 —— 什么会留下。
该用什么格式归档? ★按行分隔的 JSON(NDJSON)。★ 追加成本低、能扛住写入中断、之后转成任何格式都容易。
如果你要做导出功能
取数那部分就是一个翻页循环。★决定别人信不信任你导出的,是写文件那部分★ —— BOM、引号、注入转义,这三样全都是静默失败的。
还有一个常坑住团队的:★要翻到游标为空,绝不能翻到"这页看起来短"就停。★ 列表中间出现短页是正常的,在那里停会产生一个缺数据的导出,而且没有任何错误来解释它。
我们的 API 覆盖取数这一段 —— 对时间线、粉丝和搜索做基于游标的翻页,每条推文都带完整互动数据,所以你的导出不必为每一行再查一次。只读、固定按次价格、没有属于你的限流要管。
延伸阅读:一个账号的历史能够到多远 · 为什么时间区间会截断 · 正确导出粉丝列表 · 翻页的成本。