跳到正文

用 API 把 Twitter 用户名转换为用户 ID

X 从不在个人主页上显示用户 ID,所以代码要把用户名发给用户查询接口来取得它,接口会返回账号永久不变的 ID,是一个最多 19 位数字的字符串;批量查询每次请求处理 100 个用户名。

  • 更新于
  • 发布于
  • 阅读约 12 分钟

简短回答: 官方 X API 的 GET /2/users/by 每次调用接收 100 个用户名,每个用户 $0.010,并且需要开发者账号和 bearer 令牌;同样的查询也能把 ID 转回用户名。Sorsa API 通过 GET /info-batch 转换列表,每次请求 100 个用户名,每 1,000 个 $0.014 起,100 次免费请求可以覆盖前 1 万个。

接口
GET /info-batch
每页数量
每次请求 100 个用户名
价格
每 1,000 个 $0.014 起

#Twitter 用户 ID 是什么,怎么找到它

Twitter 用户 ID 是 X 在账号创建的那一刻赋予它的永久数字。@ 后面的用户名只是一个由 1 到 15 个字母、数字或 _ 组成的标签,主人可以随时更改,一旦空出来,任何人都可以注册它。ID 永远不变,也不会转移到另一个账号,这就是应用、导出文件和数据库都用 ID 来指向账号的原因。开发者也把它叫作账号 ID、数字 ID 或 UID。

查询单个账号,免费的 Twitter ID 查询工具能显示任意用户名或个人主页链接背后的 ID,也能把 ID 转回账号当前的用户名。无需注册。

推特 ID 查询工具

免费,一次查一个账号,无需注册。例如:用户名 jack 的用户 ID 是 12。

提交后会打开免费的推特 ID 查询工具,并填好你的用户名、链接或 ID。

早期的账号拿到的是很短的 ID:Jack Dorsey 的 @jack 是 12。近几年开通的账号,ID 是 18 或 19 位。X 在应用里哪里都不显示这个数字,但你自己的 ID 在你的数据存档里:在“设置和隐私”下的“你的账号”中选择“下载你的数据存档”(Settings and privacy, Your account, Download an archive of your data)申请,然后打开 data/account.js,读取 accountId。

用户 ID 不是帖子 ID。帖子链接里 /status/ 后面那一长串数字是帖子的编号,而推文 ID 转日期工具可以读出其中的日期。把帖子 ID 发给用户查询,是找不到任何账号的。

#用 Sorsa API 把用户名转换为用户 ID

Sorsa API 是一款提供公开数据的 Twitter(X)API 与爬虫,每个转换方向都有对应的接口,每次转换就是一次请求:每 1,000 次转换 $1.40 起。每次调用都在 ApiKey 请求头里带上你的密钥,并把 ID 以 JSON 字符串返回,所以途中不会丢失任何一位数字。

  1. 获取密钥。 注册后从控制台复制你的 API 密钥。它附带 100 次免费请求,无需绑卡。

  2. 发送用户名。 不带 @,传给 GET /username-to-id(在新标签页打开)。

    Terminal
    curl "https://api.sorsa.io/v3/username-to-id/harborline_analytics" \
      -H "ApiKey: YOUR_API_KEY"
    
    Response200 OK
    {
      "id": "1427632236659332516"
    }
    
  3. 需要时反向转换。 GET /id-to-username(在新标签页打开) 在名为 handle 的字段中返回账号当前的用户名,GET /link-to-id(在新标签页打开) 接收完整的 x.com 或 twitter.com 个人主页链接。

  4. 封装这三个调用。 同样的转换,用 Python 和 Node.js 写:

    import requests
    
    BASE = "https://api.sorsa.io/v3"
    HEADERS = {"ApiKey": "YOUR_API_KEY"}
    
    
    def username_to_id(username):
        r = requests.get(f"{BASE}/username-to-id/{username.lstrip('@')}",
                         headers=HEADERS, timeout=30)
        r.raise_for_status()
        return r.json()["id"]          # a string: keep it one
    
    
    def id_to_username(user_id):
        r = requests.get(f"{BASE}/id-to-username/{user_id}",
                         headers=HEADERS, timeout=30)
        r.raise_for_status()
        return r.json()["handle"]      # today's username, without the @
    
    
    def link_to_id(link):
        r = requests.get(f"{BASE}/link-to-id", headers=HEADERS,
                         params={"link": link}, timeout=30)
        r.raise_for_status()
        return r.json()["id"]
    
    
    print(username_to_id("harborline_analytics"))  # 1427632236659332516
    
    你有 Sorsa API 接口 你得到 一个用户名 harborline_analytics GET /username-to-id 每个一次请求 id 1427632236659332516 一个资料链接 x.com/harborline_analytics GET /link-to-id 每个一次请求 id 1427632236659332516 一个用户 ID 1427632236659332516 GET /id-to-username 每个一次请求 用户名 harborline_analytics 最多 100 个用户名 或最多 100 个用户 ID GET /info-batch 每次请求 100 个 id + username 以及完整资料
    每种输入一个接口,每次调用一次请求。最多 100 个的列表通过 GET /info-batch 一次请求完成。

404 表示没有任何公开账号使用这个用户名或 ID:可能是拼错了、已改名、被封禁或已删除。401 表示密钥有误,403 表示密钥缺失或请求额度已用完,429 表示一秒内发送了超过 20 次请求。失败的请求从不收费。如果你同时需要完整资料,GET /info(在新标签页打开) 在同一次请求中返回 ID 和整份资料。

#转换一份列表:每次请求 100 个用户名

对于一份列表,跳过逐个转换的接口:GET /info-batch(在新标签页打开) 每次请求最多接收 100 个用户名,并返回每个账号的资料及其 id。1 万个用户名的列表需要 100 次请求,成本 $0.14 起,按每秒 20 次请求大约 5 秒完成。要批量把 Twitter 用户名转换为 ID,可以用 Sorsa API 的 GET /info-batch:每次请求 100 个,每 1,000 个 $0.014 起,而官方 X API 是每 1,000 个 $10.00。

每个名字重复一次 usernames 参数:

Terminal
curl -G "https://api.sorsa.io/v3/info-batch" \
  -H "ApiKey: YOUR_API_KEY" \
  --data-urlencode "usernames=harborline_analytics" \
  --data-urlencode "usernames=quietfield_records" \
  --data-urlencode "usernames=copperkettle_maps"
Response200 OK
{
  "users": [
    {
      "id": "1427632236659332516",
      "username": "harborline_analytics",
      "display_name": "Harborline Analytics",
      "created_at": "2021-08-17T14:03:44Z",
      "followers_count": 3912,
      "protected": false
    },
    {
      "id": "1612371430079911386",
      "username": "quietfield_records",
      "display_name": "Quietfield Records",
      "created_at": "2023-01-09T08:51:20Z",
      "followers_count": 18406,
      "protected": false
    },
    {
      "id": "1804185541935281502",
      "username": "copperkettle_maps",
      "display_name": "Copperkettle Maps",
      "created_at": "2024-06-21T16:12:08Z",
      "followers_count": 671,
      "protected": false
    }
  ]
}

响应里只列出找到的账号。没有匹配任何公开账号的用户名会被略去,所以要按用户名而不是按位置来匹配结果,并且用小写比较:X 把 Harborline_Analytics 和 harborline_analytics 视为同一个名字。

下面的脚本读取带 username 列的 accounts.csv,去掉重复项和多余的 @ 符号,并写出 accounts_with_ids.csv。十个线程共用一个节拍器,让密钥保持在每秒 20 次请求,所以 100 万个用户名的列表大约 8 分钟就能处理完。

Python
import csv
import threading
import time
from concurrent.futures import ThreadPoolExecutor

import requests

URL = "https://api.sorsa.io/v3/info-batch"
HEADERS = {"ApiKey": "YOUR_API_KEY"}
PER_SECOND = 20                           # the limit on every plan
lock, next_slot = threading.Lock(), [0.0]


def wait_turn():
    """Space requests 1/20 of a second apart across all threads."""
    with lock:
        now = time.monotonic()
        slot = max(now, next_slot[0])
        next_slot[0] = slot + 1 / PER_SECOND
    time.sleep(slot - now)


def fetch(params):
    for _ in range(3):
        wait_turn()
        r = requests.get(URL, headers=HEADERS, params=params, timeout=30)
        if r.status_code != 429:          # 429: over 20 requests a second
            r.raise_for_status()
            return r.json().get("users", [])
        time.sleep(1)
    r.raise_for_status()


def usernames_to_ids(usernames):
    """Map each username (lowercase) to its user ID, 100 per request."""
    names = list(dict.fromkeys(u.strip().lstrip("@").lower()
                               for u in usernames if u.strip()))
    chunks = [names[i:i + 100] for i in range(0, len(names), 100)]
    found = {}
    with ThreadPoolExecutor(max_workers=10) as pool:
        for users in pool.map(lambda c: fetch({"usernames": c}), chunks):
            for user in users:
                found[user["username"].lower()] = user["id"]
    missing = [n for n in names if n not in found]
    return found, missing


with open("accounts.csv", newline="", encoding="utf-8") as f:
    rows = [row["username"] for row in csv.DictReader(f)]

ids, missing = usernames_to_ids(rows)

with open("accounts_with_ids.csv", "w", newline="", encoding="utf-8") as f:
    out = csv.writer(f)
    out.writerow(["username", "user_id"])
    for name, user_id in ids.items():
        out.writerow([name, user_id])     # the ID stays a string

print(len(ids), "converted;", len(missing), "not found:", missing[:10])

关于超出这个脚本的重试、cursor 和异步请求,Python 版 Twitter API指南里有可用的写法。粉丝列表则完全不需要转换:Twitter 粉丝 API每页返回 200 个粉丝,每个都已经带着自己的 id。

#把用户 ID 转回用户名,并发现改名

同一个接口也能反向使用:把最多 100 个已存的 ID 作为 user_ids 发送,每份资料都会带着它今天使用的用户名返回。拿它与你保存的用户名对比:不一样就是改过名,而没有返回的 ID,属于已被封禁或删除的账号。

Python
def current_usernames(user_ids):
    """Map each user ID to its username today, 100 IDs per request."""
    now = {}
    for i in range(0, len(user_ids), 100):
        for user in fetch({"user_ids": user_ids[i:i + 100]}):
            now[user["id"]] = user["username"]
    return now


stored = {"1427632236659332516": "harborline_analytics",
          "1612371430079911386": "quietfield_records"}
today = current_usernames(list(stored))
for user_id, old in stored.items():
    new = today.get(user_id)
    if new is None:
        print(user_id, "is gone: suspended or deleted")
    elif new.lower() != old.lower():
        print(user_id, "renamed from", old, "to", new)

按计划对你追踪的账号运行它,改了名的竞争对手或创作者就不会从你的数据里消失。抽奖和任务的参与者也是同样的做法:按 ID 存储每个参与者,这样即使改了名,Twitter 核验 API也能按该 ID 检查他们的关注和转发。

对于单个账号,GET /about(在新标签页打开) 还会提供 username_change_count 和 last_username_change_at:它改过几次用户名,以及上次是什么时候。免费的 Twitter 用户名历史检查工具不用写代码就能读出这两项信息。旧用户名本身并不公开,因为 X 没有公布它们。

要把人引到一个可能改名的账号,可以链接到 x.com/i/user/ 加上 ID:X 会用账号当前的用户名打开该主页。链接 x.com/i/user/12 会落到 @jack。

#转换之前先清理混杂的输入

真实的列表会混杂各种格式:带或不带 @ 的用户名、带跟踪参数的 x.com 和 twitter.com 链接、移动端链接、/i/user/ 链接和纯数字。调用任何接口之前先给每个值分类,这样 ID 可以跳过查询,垃圾数据也不会白花一次请求。

每种常见格式,要么读作用户名,要么读作用户 ID,要么什么都不是:

输入解读为
jack,带或不带 @用户名
https://twitter.com/jack?s=20用户名
https://mobile.twitter.com/jack用户名
https://x.com/jack/status/20复制链接当时帖子作者的用户名
https://x.com/i/user/12用户 ID
https://twitter.com/intent/user?user_id=12用户 ID
12 或 1427632236659332516用户 ID
https://x.com/home、话题标签、邮箱地址不是账号
  • jack,带或不带 @

    解读为
    用户名
  • https://twitter.com/jack?s=20

    解读为
    用户名
  • https://mobile.twitter.com/jack

    解读为
    用户名
  • https://x.com/jack/status/20

    解读为
    复制链接当时帖子作者的用户名
  • https://x.com/i/user/12

    解读为
    用户 ID
  • https://twitter.com/intent/user?user_id=12

    解读为
    用户 ID
  • 12 或 1427632236659332516

    解读为
    用户 ID
  • https://x.com/home、话题标签、邮箱地址

    解读为
    不是账号
Python
import re

USERNAME = re.compile(r"^[A-Za-z0-9_]{1,15}$")   # X's own pattern
RESERVED = {"home", "explore", "search", "i", "intent", "settings",
            "messages", "notifications", "hashtag", "share", "compose"}
LINK = re.compile(r"^(?:https?://)?(?:www\.|mobile\.)?"
                  r"(?:x|twitter)\.com/([^?#]+)", re.I)


def classify(value):
    """Return ("id", ...), ("username", ...) or ("bad", value)."""
    v = value.strip()
    if v.isdigit():                          # digits alone: an ID
        return ("id", v) if len(v) <= 19 else ("bad", value)
    q = re.search(r"[?&]user_id=(\d{1,19})\b", v)
    if q:                                    # .../intent/user?user_id=
        return ("id", q.group(1))
    m = LINK.match(v)
    if m:
        parts = m.group(1).strip("/").split("/")
        if parts[:2] == ["i", "user"]:       # x.com/i/user/<ID>
            ok = len(parts) > 2 and parts[2].isdigit()
            return ("id", parts[2]) if ok else ("bad", value)
        v = parts[0]                         # a profile or a post link
    v = v.lstrip("@")
    if USERNAME.match(v) and v.lower() not in RESERVED:
        return ("username", v.lower())
    return ("bad", value)

X 的 API 参考文档(在新标签页打开)给出的用户名规则是 1 到 15 个字母、数字或 _,而这个规则允许纯数字。所以一个纯数字既可能是用户名,也可能是 ID。这个函数把纯数字读作 ID,把前面带 @ 的数字读作用户名:当你指的是名字时,请在输入里保留 @。本页其他地方的演示用户名都故意超过 15 个字符,这样它们就不可能属于真实账号。

帖子链接里的用户名,是链接被复制那天作者的名字。如果账号后来改了名,这个名字要么查不到任何账号,要么查到抢注了它的另一个人。当数据集里存的是帖子链接时,就去查询这些帖子,从帖子数据里取每位作者的 ID:任何改名都不会改变它。

#把 ID 存成字符串,不要存成数字

每个 Twitter ID 从 API 响应到数据库都要保持为字符串。19 位的 ID 比 JavaScript 数字或电子表格单元格能精确保存的最大整数还大,所以用数字类型会悄悄改掉最后几位,而改过的 ID 指向不了任何账号。

  • JavaScript。 Number.MAX_SAFE_INTEGER 是 9007199254740991,16 位。X 的ID 官方指南(在新标签页打开)正是出于这个原因,告诉开发者始终使用字符串 ID。解析一个没有加引号的 ID 时,1427632236659332516 会变成 1427632236659332600。
  • Excel。 Excel 以 15 位有效数字的精度存储数字,并用科学计数法显示大数,所以同一个 ID 打开后显示为 1.42763E+18,存储为 1427632236659330000(Microsoft Support(在新标签页打开))。通过“数据”里的“从文本/CSV”导入 CSV,并在加载之前把 ID 列设为“文本”。
  • pandas。 用 dtype={"user_id": str} 读取文件,否则这一列也会变成数字。
  • 数据库。 X 建议 PostgreSQL 用 BIGINT 或 TEXT,MySQL 用 BIGINT UNSIGNED 或 VARCHAR(20),MongoDB 用字符串,SQLite 用 TEXT。32 位的 INT 会在 2147483647 之后溢出。

在下面粘贴任意 ID,看看上述每种做法会把它变成什么样。

  • 作为字符串:JSON, TEXT, VARCHAR(20) 1427632236659332516 精确
  • 作为 64 位整数: BIGINT 1427632236659332516 精确
  • 作为 JavaScript 数字 1427632236659332600 最后 3 位已变
  • 在 Excel 单元格中 (显示 1.42763E+18) 1427632236659330000 最后 4 位被置为 0
  • 作为 32 位整数: INT 溢出 最大 2147483647

字符串或 64 位整数能保留每一位数字;JavaScript 数字和 Excel 单元格会改变最后几位。

同样的规则也适用于导出文件。当你导出 Twitter 粉丝或任何账号列表到 CSV 时,打开文件时要把 ID 列设为“文本”,否则你保存的 ID 就不再与 API 的 ID 匹配。

须知

Sorsa API 把每个 ID 都以 JSON 字符串返回:用户 ID、帖子 ID,以及 pinned_tweet_ids 这类列表里的 ID。数字只会在你自己的代码或电子表格把它们转成数字时才发生变化,所以修复总是在读取一侧。

#转换用户名要花多少钱,需要多长时间

转换用户名为 ID,在官方 X API 上每 1,000 个 $10.00,在 Sorsa API 上每 1,000 个 $0.014 起。X 对一次查询返回的每个用户计费(在新标签页打开),每个 $0.010,无论这次调用带的是一个用户名还是 100 个。Sorsa API 按请求计费,而一次 GET /info-batch 请求就能带 100 个用户名。

待转换的用户名数

官方 X API

$10,000

100 万 个用户名

每个用户 $0.010

Sorsa API

$14 起

100 万 个用户名

每 1,000 个 $0.014 起

便宜 700 多倍

时间是另一个差距。X 的速率限制(在新标签页打开)让一个应用每 15 分钟只能做 300 次用户查询,所以即使每次调用 100 个,100 万个用户名也要约 8 小时 20 分钟。Sorsa API 所有套餐都是每秒 20 次请求,同样的列表约 8 分 20 秒就能转换完。

同样的 100 万个用户名:官方 X API 上约 8 小时 20 分钟、$10,000,Sorsa API 上约 8 分 20 秒、$14 起。

在这项工作依赖的每一项标准上,Sorsa API 都胜过官方 X API:

标准Sorsa API官方 X API
每 1,000 个用户名的价格$0.014 起$10.00
100 万个用户名的耗时约 8 分钟约 8 小时 20 分钟
设置请求头里一个 API 密钥开发者账号、应用、bearer 令牌
ID 转回用户名同一个接口,同样的价格每个用户 $0.010
你的 X 登录从不索要开发者账号绑在你的 X 账号上
  • 每 1,000 个用户名的价格

    Sorsa API
    $0.014 起
    官方 X API
    $10.00
  • 100 万个用户名的耗时

    Sorsa API
    约 8 分钟
    官方 X API
    约 8 小时 20 分钟
  • 设置

    Sorsa API
    请求头里一个 API 密钥
    官方 X API
    开发者账号、应用、bearer 令牌
  • ID 转回用户名

    Sorsa API
    同一个接口,同样的价格
    官方 X API
    每个用户 $0.010
  • 你的 X 登录

    Sorsa API
    从不索要
    官方 X API
    开发者账号绑在你的 X 账号上

官方 X API 的完整价目表,包括帖子和粉丝,见我们的 X API 定价指南。

#转换你的第一份列表

Sorsa API 把 Twitter 用户名转换为用户 ID,每次请求 100 个,每 1,000 个 $0.014 起,并以同样的价格把已存的 ID 转回今天的用户名。它是一个无需开发者账号的 Twitter API:请求头里一个密钥,无需 OAuth,也没有应用审核。你的 100 次免费请求可以转换前 1 万个用户名,所以把上面的批量脚本粘贴过去,填上你的密钥,在你的列表上运行即可。

#常见问题

把 Twitter 用户名转换为用户 ID,应该用哪个 API?

用 Sorsa API。它的 GET /info-batch 每次请求转换 100 个用户名,每 1,000 个 $0.014 起,而官方 X API 是每 1,000 个 $10.00,并且只需一个 API 密钥,不需要开发者账号和 bearer 令牌。所有套餐统一每秒 20 次请求,所以 100 万个用户名约 8 分钟,并且它从不索要你的 X 登录信息。

Twitter 用户 ID 会改变吗?

不会。X 在账号创建时分配用户 ID,并且在每次改名、显示名更改、更换邮箱或解除封禁之后都保持不变。变化的是指向它的用户名:改名之后旧用户名就空出来,陌生人可以注册,所以一个月后,保存的用户名可能指向另一个人。

X Number 和 Twitter 用户 ID 是一回事吗?

不是。X Number 是 X 在 2026 年 9 月加入 Chat 的私密联系码:由你创建,分享给你想收到消息的人,并且随时可以刷新或关闭(X Help Center(在新标签页打开))。用户 ID 则是账号永久的公开编号。X 从不泄露任何人的 X Number,所以没有任何查询工具或 API 会返回它。

能从用户 ID 得到账号的创建日期吗?

对于 18 或 19 位的 ID,可以:创建时间就是这个数字的一部分。把 ID 右移 22 位,加上 1288834974657,再把结果传给 JavaScript 的 Date。最早那批账号的短 ID 不含日期。Sorsa API 的 GET /info 会返回任意账号确切的 created_at,不管它的 ID 有多长。

为什么某个用户名查不到用户 ID?

因为目前没有公开账号使用这个用户名:可能拼错了,账号改了名,或已被封禁或删除。X 用户名是 1 到 15 个字母、数字或 _,所以更长的一定是笔误。改了名的账号保留它的 ID:如果你存了 ID,用 user_ids 调用 GET /info-batch 就能返回新的用户名。在 Sorsa API 上,失败的请求从不收费。

#我们如何核实

2026 年 11 月 2 日,我们根据线上的 Sorsa API 参考文档写出了本页的每个请求;我们没有带着密钥去运行它们。同一天,我们阅读了 docs.x.com 上 X 的定价、速率限制、用户查询和 ID 页面,帮助中心关于 Chat 的页面,以及 Microsoft 关于 Excel 大数字的页面,并打开 x.com/i/user/12 确认它指向哪里。舍入的例子来自 JavaScript 使用的 64 位浮点数。

Sorsa API 团队撰写。 我们开发和运营 Sorsa API,这是只读的 Twitter/X API 与爬虫,2022 年起运行于生产环境(前身为 TweetScout API),累计处理请求超 60 亿次。 关于 Sorsa API

更新内容
  • 面向开发者全文重写:通过 GET /info-batch 做批量转换,附价格和耗时,混合输入的清理,改名,把 ID 保存为字符串,以及 X Number 与用户 ID 的区别。
  • 首次发布。