为任意 X 账号或列表生成 Twitter RSS 订阅源
X 不提供任何 RSS 订阅源,所以 Twitter RSS 订阅源要自己生成:脚本以 JSON 读取账号的最新帖子(每次请求约 20 条),再改写成 RSS 2.0 文件,供你的阅读器定时拉取。
简短回答: 每 15 分钟刷新一次,一个订阅源每月约需 2,880 次请求,而一个公开的 X 列表可以把多达 5,000 个账号折叠进同样的一次请求。要搭建它,可以用 Sorsa API:POST /user-tweets 每次请求返回 20 条帖子,每 1,000 条 $0.07 起,100 次免费请求可以拉取你的前 2,000 条帖子。
- 接口
POST /user-tweets- 每页条数
- 每次请求 20 条帖子
- 价格
- 每 1,000 条帖子 $0.07 起
#X 有 RSS 订阅源吗?
没有。X 对个人主页、搜索和列表都不提供 RSS 订阅源:在个人主页地址后面加上 /rss,x.com 返回的只是它平常的网页,我们在 2026 年 11 月 2 日确认过这一点。如今所有能用的 Twitter RSS 订阅源,都是有人用 X 数据生成的,所以真正的选择在于:用什么来搭,以及谁来维护它的运行。
现成的方案都有附带条件:
- RSSHub 的 X 路由现在只能靠已登录 X 账号的
auth_tokenCookie 或官方 X API 密钥运行,这是它的路由源码里写明的。用 Cookie 会把订阅源绑在一个真实账号上,让这个账号面临风险。 - 托管的订阅源生成器把订阅源当作订阅服务来卖,每个订阅源保留多少帖子、多久刷新一次,都由它们的套餐决定。
RSS 本身仍然活着。Feedly、Inoreader、NetNewsWire、FreshRSS、Miniflux 和 n8n 的 RSS Read 节点都支持订阅 RSS 2.0,所以你搭出来的订阅源能接入你已经在用的工具。如果你想要的是手机或 Slack 里的提醒,而不是订阅源,请看有人发推时收到通知。
#Twitter RSS 订阅源是怎么搭起来的
一个 Twitter RSS 订阅源分五步,只有第一步接触 X 数据,其余只是一段短脚本和一个定时器。
-
以 JSON 获取最新帖子。 一次请求返回账号最近的 20 条帖子,包含正文、日期、媒体和作者。
-
保留已有的内容。 一个小小的状态文件可以避免重复,并让订阅源容纳不止一页的帖子。
-
把每条帖子映射为一个 RSS 条目。 标题、链接、日期和正文,放进每个阅读器都能识别的元素里。
-
写出 XML 文件。 先写到临时文件,再替换过去,这样阅读器不会拉到只有一半的订阅源。
-
按计划刷新。 大多数订阅源每 15 到 60 分钟刷新一次就合适。
只有第一步读取 X 数据;脚本、文件和阅读器都由你掌控。
数据这一步,可以用 Sorsa API,一款 Twitter(X)API 与爬虫:一次 POST /user-tweets 请求,用一个 API 密钥就以干净的 JSON 返回账号最新的 20 条帖子,不需要开发者账号,也从不要求你的 X 登录信息。响应结构在各个版本之间保持不变,所以你今天写好的订阅源脚本会一直能用。
阅读器显示的全部内容靠五个 RSS 元素承载,每个元素都对应响应里的一个字段:
| RSS 元素 | Sorsa API 字段 | 作用 |
|---|---|---|
title | full_text 的前 80 个字符 | 阅读器按标题列出条目 |
link 和 guid | https://x.com/ + user.username + /status/ + id | 稳定的 guid 可避免重复 |
pubDate | created_at,ISO 8601 | RSS 需要 RFC 822 格式的日期 |
description | full_text 加上 entities[].preview 的图片 | 以 HTML 呈现的帖子 |
dc:creator | user.display_name | RSS 的 author 必须是邮箱 |
title- Sorsa API 字段
full_text的前 80 个字符- 作用
- 阅读器按标题列出条目
link和guid- Sorsa API 字段
https://x.com/+user.username+/status/+id- 作用
- 稳定的 guid 可避免重复
pubDate- Sorsa API 字段
created_at,ISO 8601- 作用
- RSS 需要 RFC 822 格式的日期
description- Sorsa API 字段
full_text加上entities[].preview的图片- 作用
- 以 HTML 呈现的帖子
dc:creator- Sorsa API 字段
user.display_name- 作用
- RSS 的
author必须是邮箱
#以 JSON 获取账号的帖子
把账号的用户名发给 POST /user-tweets,最新的 20 条帖子就会在一次响应里返回。三步就能从零走到第一次响应。
-
获取密钥。 注册后,在控制台复制你的 API 密钥。前 100 次请求免费,永不过期。
-
调用接口。 把用户名放在 JSON 请求体里,密钥放在
ApiKey请求头中。完整的参数列表见用户推文参考(在新标签页打开)。Terminal curl -X POST "https://api.sorsa.io/v3/user-tweets" \ -H "ApiKey: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"username": "nasa_launch_desk"}'Response200 OK { "tweets": [ { "id": "1985012345678901234", "full_text": "Static fire complete. Launch window opens Thursday.", "created_at": "2026-11-02T14:05:00Z", "lang": "en", "likes_count": 1840, "retweet_count": 312, "reply_count": 95, "view_count": 120400, "is_reply": false, "entities": [ { "type": "photo", "link": "https://t.co/k3x9demo", "preview": "https://pbs.twimg.com/media/demo-static-fire.jpg" } ], "user": { "id": "1700000000000000042", "username": "nasa_launch_desk", "display_name": "Launch Desk" } } ], "next_cursor": "DAABCgABG8a1demoAAA" } -
选择订阅源显示什么。
type可选posts(默认)、replies或retweets,with_replies: true会把回复混入帖子。next_cursor用来获取更早的下一批 20 条帖子。
每条帖子还带有作者的完整资料,所以订阅源不需要再发第二次请求去取名字或头像。返回 403 表示缺少密钥或请求已用完,返回 429 表示一秒内超过了 20 次请求,失败的请求永远不计费。
#用 Python 把 JSON 转成 RSS
下面的脚本只用 Python 标准库,就能把这些 JSON 转成合法的 RSS 2.0 文件,所以无需安装任何东西。它既适用于账号,也适用于列表或搜索,并且会在两次运行之间记住最近的 50 条帖子。
"""Write an RSS 2.0 feed of an X account, List or search.
python x_rss.py user nasa_launch_desk
python x_rss.py list 1700000000000000001
python x_rss.py search "from:nasa_launch_desk OR #artemis"
"""
import json
import os
import sys
import urllib.parse
import urllib.request
from datetime import datetime
from email.utils import format_datetime
from xml.sax.saxutils import escape
API = "https://api.sorsa.io/v3"
KEY = os.environ.get("SORSA_API_KEY", "YOUR_API_KEY")
KEEP = 50 # posts the feed keeps
MAX_PAGES = 5 # pages one run may read to catch up
def page(kind, target, cursor):
if kind == "list":
query = {"list_id": target}
if cursor:
query["next_cursor"] = cursor
url = API + "/list-tweets?" + urllib.parse.urlencode(query)
req = urllib.request.Request(url)
else:
if kind == "user":
path, body = "/user-tweets", {"username": target}
else:
path, body = "/search-tweets", {"query": target,
"order": "latest"}
if cursor:
body["next_cursor"] = cursor
req = urllib.request.Request(
API + path, data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"})
req.add_header("ApiKey", KEY)
with urllib.request.urlopen(req, timeout=30) as resp:
return json.load(resp)
def fetch_new(kind, target, seen):
new, cursor = [], None
for _ in range(MAX_PAGES):
data = page(kind, target, cursor)
tweets = data.get("tweets") or []
new += [t for t in tweets if t["id"] not in seen]
cursor = data.get("next_cursor")
caught_up = any(t["id"] in seen for t in tweets)
if not cursor or not seen or caught_up:
break
return new
def to_item(t):
user = t.get("user") or {}
name = user.get("username", "")
link = f"https://x.com/{name}/status/{t['id']}"
text = " ".join(t.get("full_text", "").split())
when = datetime.fromisoformat(t["created_at"].replace("Z", "+00:00"))
pics = "".join(f'<p><img src="{escape(e["preview"])}"></p>'
for e in t.get("entities") or []
if e.get("type") == "photo" and e.get("preview"))
author = escape(user.get("display_name") or name)
xml = ("<item>"
f"<title>{escape(text[:80])}</title>"
f"<link>{link}</link>"
f'<guid isPermaLink="true">{link}</guid>'
f"<pubDate>{format_datetime(when)}</pubDate>"
f"<dc:creator>{author}</dc:creator>"
f"<description>{escape(escape(text) + pics)}</description>"
"</item>")
return {"id": t["id"], "date": when.isoformat(), "xml": xml}
def main(kind, target):
safe = (c if c.isalnum() or c in "_-" else "-" for c in target)
name = "".join(safe)[:60]
state, feed = name + ".json", name + ".xml"
items = []
if os.path.exists(state):
with open(state, encoding="utf-8") as f:
items = json.load(f)
seen = {i["id"] for i in items}
items += [to_item(t) for t in fetch_new(kind, target, seen)]
items = sorted(items, key=lambda i: i["date"], reverse=True)[:KEEP]
with open(state, "w", encoding="utf-8") as f:
json.dump(items, f)
home = {"user": f"https://x.com/{target}",
"list": f"https://x.com/i/lists/{target}"}
title = escape(f"X {kind}: {target}")
xml = ('<?xml version="1.0" encoding="UTF-8"?>'
'<rss version="2.0" '
'xmlns:dc="http://purl.org/dc/elements/1.1/"><channel>'
f"<title>{title}</title>"
f"<link>{home.get(kind, 'https://x.com/')}</link>"
f"<description>{title}, built with Sorsa API</description>"
+ "".join(i["xml"] for i in items) + "</channel></rss>")
with open(feed + ".tmp", "w", encoding="utf-8") as f:
f.write(xml)
os.replace(feed + ".tmp", feed)
print(f"{feed}: {len(items)} posts")
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2])
有三个细节,让这个订阅源比单纯转换一页数据表现得更好:
- 不会漏掉突发的大量发帖。 当账号在两次运行之间发了超过 20 条帖子时,脚本会顺着
next_cursor往回翻,直到遇到已有的帖子,最多翻 5 页。 - 它保留 50 条帖子,而不是 20 条。 JSON 状态文件保存了更早的帖子,所以哪怕某天账号没发帖,订阅源也不会被清空。
- 它能通过订阅源校验器的常规检查。 日期是 RFC 822 格式,每个条目都有固定的
guid,作者放在dc:creator里,帖子的 HTML 只为 XML 转义一次。
运行 python x_rss.py user nasa_launch_desk,脚本旁边就会出现文件 nasa_launch_desk.xml。第一次运行花费一次请求;之后每次运行花费一次请求,外加追赶所需的每个额外页面一次请求。
#一个订阅源涵盖整个列表或一次搜索
一个公开的 X 列表可以把多达 5,000 个账号变成一个订阅源,花费和单个账号的订阅源一样。用 Sorsa API 的话,GET /list-tweets 会按时间顺序在一页里返回列表中所有成员的最新帖子,所以不管订阅源关注多少账号,每次刷新都只花一次请求。
每 15 分钟把 200 个账号当作 200 个独立订阅源来关注,每月需要 57.6 万次请求;同样的 200 个账号放进一个列表,只需要 2,880 次。要设置一个,就在 X 应用里创建一个公开列表(或者用别人的公开列表),复制它地址末尾的数字(x.com/i/lists/<number>),并把它作为 list_id 传入。参数见列表推文参考(在新标签页打开),Twitter 列表 API 指南还讲了成员和关注者。
python x_rss.py list 1700000000000000001
搜索订阅源的做法一样,通过 POST /search-tweets 并把 order 设为 latest。想关注少数几个账号又不想建列表,或者想关注某个话题,都可以用它:
from:nasa_launch_desk OR from:orbit_news_digest
#artemis -filter:replies
"launch window" lang:en min_faves:50
X 的所有搜索运算符在这里都能用,可以回溯到 2006 年;Twitter 搜索运算符指南列出了它们并附有示例,请求格式见搜索推文参考(在新标签页打开)。一个热闹的列表或话题标签,在两次刷新之间发帖可能超过 20 条:这时脚本会多读几页,每页一次请求,所以热闹的订阅源应该刷新得更勤,而不是更少。
#按计划运行并订阅
脚本在定时器上运行,再由网络服务器把文件交给你的阅读器,订阅源才能保持新鲜。在任何一台 Linux 服务器上,crontab 里的两行就能保存密钥,并每 15 分钟刷新一次订阅源:
SORSA_API_KEY=YOUR_API_KEY
*/15 * * * * cd /srv/feeds && python3 x_rss.py user nasa_launch_desk
然后把这个文件夹发布出去。机器上已经在运行的任何网络服务器(nginx、Caddy、Apache)都可以发布 /srv/feeds;同一台服务器上自建的阅读器,比如 FreshRSS 或 Miniflux,可以从本地地址读取这个文件。想快速测试,可以用 python3 -m http.server 8080 --directory /srv/feeds,它会在 http://localhost:8080/nasa_launch_desk.xml 提供这个文件。
把订阅源的地址粘贴到阅读器的“添加订阅”输入框里。把阅读器的刷新频率设成不比 cron 更频繁:每 5 分钟检查一次的阅读器,四次里有三次看到的是同一个文件。
须知
订阅源的网址只供你自己的阅读器或团队的工具使用。请把它放在一段很长的随机路径后面,或者放在阅读器的登录之后,而不是放在公开页面上:订阅源里的帖子属于它们的作者,而且订阅源只含公开帖子。
#改用 n8n 提供订阅源
n8n 用四个节点就能在它自己的网址上提供同样的订阅源,不需要你自己的服务器。每次你的阅读器拉取这个网址,工作流就调用一次 Sorsa API,并返回新的 XML。
-
添加一个 Webhook 节点。 把 HTTP Method 设为 GET,给它一段很长的随机路径,并把 Respond 设为 “Using 'Respond to Webhook' Node”。
-
添加一个 HTTP Request 节点。 用 POST 请求
https://api.sorsa.io/v3/user-tweets,带上请求头ApiKey,并发送 JSON 请求体{"username": "nasa_launch_desk"}。 -
添加一个 Code 节点。 它把
tweets数组转成 RSS 条目;代码在下面的工作流里。 -
添加一个 Respond to Webhook 节点。 把 Respond With 设为 Text,把 Code 节点的
xml字段拖进 Response Body,并添加响应头Content-Type,值为application/rss+xml; charset=utf-8。 -
启用工作流。 把你的阅读器订阅到 Webhook 节点的 Production URL。
n8n 工作流:四个节点,一个订阅地址- WebhookFeed URLGET,通过最后一个节点响应
- HTTP RequestSorsa API user tweetsPOST /user-tweets,请求头 ApiKey
- CodeBuild RSS把推文转成 RSS 2.0 条目
- Respond to WebhookReturn the feed文本,application/rss+xml
const tweets = $input.first().json.tweets || []; const esc = (s) => String(s).replace(/&/g, '&') .replace(/</g, '<').replace(/>/g, '>'); const items = tweets.map((t) => { const link = 'https://x.com/' + t.user.username + '/status/' + t.id; const text = t.full_text.replace(/\s+/g, ' '); return '<item><title>' + esc(text.slice(0, 80)) + '</title>' + '<link>' + link + '</link>' + '<guid isPermaLink="true">' + link + '</guid>' + '<pubDate>' + new Date(t.created_at).toUTCString() + '</pubDate>' + '<description>' + esc(esc(text)) + '</description></item>'; }).join(''); const xml = '<?xml version="1.0" encoding="UTF-8"?>' + '<rss version="2.0"><channel><title>X feed</title>' + '<link>https://x.com/</link>' + '<description>Built with Sorsa API</description>' + items + '</channel></rss>'; return [{ json: { xml } }];{ "name": "X account to RSS feed (Sorsa API)", "nodes": [ { "parameters": { "httpMethod": "GET", "path": "x-feed-3f9c1a7e5b2d", "responseMode": "responseNode", "options": {} }, "name": "Feed URL", "type": "n8n-nodes-base.webhook", "typeVersion": 2, "position": [ 0, 0 ], "webhookId": "3f9c1a7e-5b2d-4c8e-9a61-7d0e2b4f6c13" }, { "parameters": { "method": "POST", "url": "https://api.sorsa.io/v3/user-tweets", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "ApiKey", "value": "YOUR_API_KEY" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "{\"username\": \"nasa_launch_desk\"}", "options": {} }, "name": "Sorsa API user tweets", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 220, 0 ] }, { "parameters": { "jsCode": "const tweets = $input.first().json.tweets || [];\nconst esc = (s) => String(s).replace(/&/g, '&')\n .replace(/</g, '<').replace(/>/g, '>');\nconst items = tweets.map((t) => {\n const link = 'https://x.com/' + t.user.username + '/status/' + t.id;\n const text = t.full_text.replace(/\\s+/g, ' ');\n return '<item><title>' + esc(text.slice(0, 80)) + '</title>' +\n '<link>' + link + '</link>' +\n '<guid isPermaLink=\"true\">' + link + '</guid>' +\n '<pubDate>' + new Date(t.created_at).toUTCString() + '</pubDate>' +\n '<description>' + esc(esc(text)) + '</description></item>';\n}).join('');\nconst xml = '<?xml version=\"1.0\" encoding=\"UTF-8\"?>' +\n '<rss version=\"2.0\"><channel><title>X feed</title>' +\n '<link>https://x.com/</link>' +\n '<description>Built with Sorsa API</description>' +\n items + '</channel></rss>';\nreturn [{ json: { xml } }];" }, "name": "Build RSS", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 440, 0 ] }, { "parameters": { "respondWith": "text", "responseBody": "={{ $json.xml }}", "options": { "responseHeaders": { "entries": [ { "name": "Content-Type", "value": "application/rss+xml; charset=utf-8" } ] } } }, "name": "Return the feed", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.1, "position": [ 660, 0 ] } ], "connections": { "Feed URL": { "main": [ [ { "node": "Sorsa API user tweets", "type": "main", "index": 0 } ] ] }, "Sorsa API user tweets": { "main": [ [ { "node": "Build RSS", "type": "main", "index": 0 } ] ] }, "Build RSS": { "main": [ [ { "node": "Return the feed", "type": "main", "index": 0 } ] ] } } }在 n8n 中,把 JSON 粘贴到空白画布,在 HTTP Request 节点里填入你的密钥,激活工作流,然后订阅 Production URL。
Code 节点把 Sorsa API 返回的 tweets 数组转成 RSS 2.0;工作流 JSON 一次导入全部四个节点。
这里由阅读器的刷新间隔决定费用:每小时检查一次的阅读器,每月为这个订阅源花 720 次请求。要做列表或搜索订阅源,就修改 HTTP Request 节点的网址和请求体。如果这些帖子只是喂给工作流的其余部分(Slack、Sheets、AI 步骤),那就跳过 RSS,直接读取 JSON,在 n8n 中使用 Twitter API指南里有演示。
#Twitter RSS 订阅源要花多少钱
一个 Twitter RSS 订阅源每次刷新花一次请求,所以它的价格取决于刷新的频率和你运行的订阅源数量,而不是账号发了多少帖子。以 30 天的一个月计:
| 刷新间隔 | 一个订阅源每月请求数 | Starter 的 1 万次请求可支持的订阅源数 |
|---|---|---|
| 5 分钟 | 8,640 | 1 |
| 15 分钟 | 2,880 | 3 |
| 30 分钟 | 1,440 | 6 |
| 1 小时 | 720 | 13 |
| 6 小时 | 120 | 83 |
5 分钟
- 一个订阅源每月请求数
- 8,640
- Starter 的 1 万次请求可支持的订阅源数
- 1
15 分钟
- 一个订阅源每月请求数
- 2,880
- Starter 的 1 万次请求可支持的订阅源数
- 3
30 分钟
- 一个订阅源每月请求数
- 1,440
- Starter 的 1 万次请求可支持的订阅源数
- 6
1 小时
- 一个订阅源每月请求数
- 720
- Starter 的 1 万次请求可支持的订阅源数
- 13
6 小时
- 一个订阅源每月请求数
- 120
- Starter 的 1 万次请求可支持的订阅源数
- 83
每个 List 或每次搜索算一个订阅源,不论它关注多少个账号。
当每页都返回满页数据时(比如热闹的列表,或第一次运行),Sorsa API 提供帖子的价格是每 1,000 条 $0.07 起。官方 X API 的按量付费价格(在新标签页打开)是每读取 1,000 条帖子 $5.00,而且要先有开发者账号和 OAuth 应用,按量付费每月最多读取 300 万条帖子。从 2026 年 11 月 22 日起,X 还会对通过 expansions 返回的每条帖子的作者和媒体另行计费,每位用户 $0.010,见 X 在 10 月 23 日的通知(在新标签页打开)。
#今天就搭建你的第一个订阅源
Twitter RSS 订阅源就是一个约 100 行的脚本加一个定时器,唯一没法自己写的部分是数据。这部分可以用 Sorsa API:每次刷新一次请求,每次请求 20 条帖子,多达 5,000 个账号的整个列表放进一个订阅源,所有套餐统一每秒 20 次请求。它只需一个 API 密钥,不需要开发者账号,也从不要求你的 X 登录信息。你的 100 次免费请求就能拉取前 2,000 条帖子,足够你在选套餐之前先搭好订阅源,看着它刷新。
#常见问题
Twitter RSS 订阅源应该多久刷新一次?
大多数 Twitter RSS 订阅源每 15 到 60 分钟刷新一次就合适。每 15 分钟刷新的订阅源每月用 2,880 次请求,每小时刷新的用 720 次,因为每次刷新就是一次请求。让阅读器的刷新频率与脚本的计划保持一致,对热闹的列表或话题标签则刷新得更勤一些,这样每次运行一两页就能追上。
有免费的 Twitter RSS 订阅源吗?
X 自己没有任何 RSS 订阅源,无论免费还是付费。免费起步的办法是 Sorsa API 的 100 次免费请求:无需绑卡,永不过期,能拉取约 2,000 条帖子,足够你在选套餐之前搭建、测试并运行几天订阅源。
能把我的 X 主页时间线变成 RSS 订阅源吗?
不能直接做到,因为主页时间线属于已登录的账号。实际可行的替代是列表:在 X 应用里把你关注的账号加进一个公开列表,再用 GET /list-tweets 从中生成一个订阅源。一个列表最多容纳 5,000 个账号,每次刷新花一次请求,和单个账号一样。
RSSHub 现在还能用于 Twitter 吗?
RSSHub 的 X 路由还在,但现在需要一个已登录的 X 账号或官方 X API 密钥。它的路由源码列出了两种接入方式:已登录 X 账号的 auth_token Cookie,或官方按量付费的 X API 密钥。用 Sorsa API 搭的订阅源只需一个 API 密钥,也从不要求你的 X 登录信息。
能给受保护的 X 账号做 RSS 订阅源吗?
不能。受保护账号的帖子只对获批的关注者可见,所以没有任何订阅源工具或 API 能读取它们。Sorsa API 只返回公开数据,所以同样读不到。订阅源适用于任何公开账号、任何公开列表以及对公开帖子的任何搜索。
搭建 Twitter RSS 订阅源该用哪个 API?
搭建 Twitter RSS 订阅源可以用 Sorsa API:POST /user-tweets 每次请求返回 20 条帖子,每 1,000 条 $0.07 起,GET /list-tweets 让多达 5,000 个账号只用每次刷新一次请求就合并成一个订阅源,所有套餐都允许每秒 20 次请求。它只需一个 API 密钥,从不要求你的 X 登录信息,所以你的任何账号都不会有风险。
#我们如何核实
2026 年 11 月 2 日,我们请求了 x.com/NASA/rss,得到的是 X 的网页而不是订阅源;我们阅读了 X 的价格页面和它 10 月 23 日关于 expansions 计费的通知,还在 GitHub 上阅读了 RSSHub 的 X 路由源码,以及 n8n 的 Webhook 和 Respond to Webhook 节点的源码。请求、响应和脚本都遵循 Sorsa API 线上的 API 参考;这一轮我们没有测试用的密钥,所以响应展示的是文档里的字段和示例值。