@finch_ren/x-scraper 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +175 -97
- package/README.zh-CN.md +337 -0
- package/dist/openapi/placeholder.json +2285 -0
- package/dist/src/openapi/src/models/ItemResult.js +8 -1
- package/dist/src/openapi/src/models/TimelineAddEntry.js +8 -1
- package/dist/src/openapi/src/models/TimelineTimelineItem.js +8 -1
- package/dist/src/openapi/src/models/TimelineTimelineModule.js +8 -1
- package/dist/src/openapi/src/models/TimelineTweet.js +16 -2
- package/dist/src/openapi/src/models/Tweet.js +16 -2
- package/package.json +4 -3
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# x-scraper
|
|
2
|
+
|
|
3
|
+
[English](./README.md) | 简体中文
|
|
4
|
+
|
|
5
|
+
`x-scraper` 是一个面向 X/Twitter 内部 GraphQL 接口的 TypeScript SDK。
|
|
6
|
+
重点是让常见场景开箱即用,同时保持类型清晰、便于扩展。
|
|
7
|
+
|
|
8
|
+
## 特性
|
|
9
|
+
|
|
10
|
+
- 自动 Guest Token 初始化(无需手动配置 token)
|
|
11
|
+
- 支持 Cookie 登录态客户端
|
|
12
|
+
- 面向常用场景提供开箱即用调用入口(用户、推文、互动等)
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
<details open>
|
|
17
|
+
<summary><strong>npm</strong></summary>
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @finch_ren/x-scraper
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
</details>
|
|
24
|
+
|
|
25
|
+
<details>
|
|
26
|
+
<summary><strong>pnpm</strong></summary>
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pnpm add @finch_ren/x-scraper
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
</details>
|
|
33
|
+
|
|
34
|
+
<details>
|
|
35
|
+
<summary><strong>yarn</strong></summary>
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
yarn add @finch_ren/x-scraper
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
</details>
|
|
42
|
+
|
|
43
|
+
## 简调用示例
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { XScraper } from '@finch_ren/x-scraper';
|
|
47
|
+
|
|
48
|
+
async function main() {
|
|
49
|
+
const scraper = new XScraper();
|
|
50
|
+
const client = await scraper.getClientFromCookies({
|
|
51
|
+
__cf_bm: '<__cf_bm>',
|
|
52
|
+
__cuid: '<__cuid>',
|
|
53
|
+
_ga: '<_ga>',
|
|
54
|
+
_ga_BLY4P7T5KW: '<_ga_BLY4P7T5KW>',
|
|
55
|
+
_twitter_sess: '<_twitter_sess>',
|
|
56
|
+
auth_token: '<auth_token>',
|
|
57
|
+
ct0: '<ct0>',
|
|
58
|
+
guest_id: '<guest_id>',
|
|
59
|
+
guest_id_ads: '<guest_id_ads>',
|
|
60
|
+
guest_id_marketing: '<guest_id_marketing>',
|
|
61
|
+
kdt: '<kdt>',
|
|
62
|
+
lang: '<lang>',
|
|
63
|
+
personalization_id: '<personalization_id>',
|
|
64
|
+
twid: '<twid>',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const res = await client.getTweetDetail({
|
|
68
|
+
focalTweetId: '2018440335140024383',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
main().catch(console.error);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
<details open>
|
|
76
|
+
<summary><strong>返回结果示例(JSON)(仅作展示,请以实际返回结果为准)</strong></summary>
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"raw": {
|
|
81
|
+
"instruction": [
|
|
82
|
+
{ "type": "TimelineClearCache" },
|
|
83
|
+
{ "type": "TimelineAddEntries", "entries": [/* ... */] },
|
|
84
|
+
{ "type": "TimelineTerminateTimeline" }
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
"cursor": {
|
|
88
|
+
"bottom": {
|
|
89
|
+
"typename": "TimelineTimelineCursor",
|
|
90
|
+
"cursorType": "Bottom",
|
|
91
|
+
"entryType": "TimelineTimelineCursor",
|
|
92
|
+
"value": "DAAKCgABHB6qs_W__pULAAIAAAGoRW1QQzZ3QUFBZlEvZ0dKTjB2R3AvQUFBQUNVY0F2UXhkOXN4dEJ3Qzl4Y3gyNkJ0SEFNbGdrOFhvU3djQXZHZ0hCZWhBeHdDODE5MVd1SGRIQUwzU3c4Ym9UMGNBdlkrcGhyeDBCd0M4VXJZMXFFZEhBTTJhdnlhTVVBY0F2Z24zSnN3TUJ3Qys2aFJHbEdvSEFOYjhqZmFrVjRjQXZtYzBadkF3UndDOE96NDJsRmtIQUwvWUxVV3NkY2NBeEM2dFJ0d1N4d0MrdFY4R3BFYUhBTDdySXNhMGNzY0F2REthRmR3UHh3REEwNnAydEFxSEFOSDA4eWFZV3NjQXhFN3I5c1JTaHdEQVpFSkc4RXFIQUwwNXpmYTBZd2NBdmJ6SHBxeFJod0M5V1BmVzREWkhBTUJ6b3ZhRVl3Y0F2R3Z0TmN4MGh3QzgzM0Myb0V0SEFMOGNBYmJzUDRjQTdxQm1ScUEyQndDK0Y1SzI4QnJIQUx5OFl5YkVaQWNBdjY5THRyeGJSd0RCQ24wVzBGdUhBTDR6TGJiRVBvY0F2UFZqRnRCVWc9PQgAAwAAAAILAAQAAAAGQm90dG9tAAA"
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"data": [
|
|
96
|
+
{
|
|
97
|
+
"raw": { "typename": "Tweet", "restId": "2018440335140024383" },
|
|
98
|
+
"tweet": {
|
|
99
|
+
"id": "2018440335140024383",
|
|
100
|
+
"text": "SpaceX has acquired xAI, forming one of the most ambitious...",
|
|
101
|
+
"createdAt": "Mon Feb 02 21:44:11 +0000 2026",
|
|
102
|
+
"favoriteCount": 44183,
|
|
103
|
+
"retweetCount": 7746,
|
|
104
|
+
"replyCount": 3354
|
|
105
|
+
},
|
|
106
|
+
"user": {
|
|
107
|
+
"id": "34743251",
|
|
108
|
+
"name": "SpaceX",
|
|
109
|
+
"screenName": "SpaceX",
|
|
110
|
+
"followersCount": 41074731
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"raw": { /* ... */ },
|
|
115
|
+
"tweet": { /* ... */ },
|
|
116
|
+
"user": { /* ... */ }
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
</details>
|
|
123
|
+
|
|
124
|
+
## 快速开始
|
|
125
|
+
|
|
126
|
+
### Guest 模式
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { XScraper } from '@finch_ren/x-scraper';
|
|
130
|
+
|
|
131
|
+
async function main() {
|
|
132
|
+
const scraper = new XScraper();
|
|
133
|
+
const client = await scraper.getGuestClient();
|
|
134
|
+
|
|
135
|
+
const user = await client.getUserByScreenName({ screenName: 'elonmusk' });
|
|
136
|
+
console.log(user.data?.user?.legacy?.screenName);
|
|
137
|
+
|
|
138
|
+
const tweets = await client.getUserTweets({ userId: '44196397' });
|
|
139
|
+
console.log(tweets.raw.response.status);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
main().catch(console.error);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Cookie 模式
|
|
146
|
+
|
|
147
|
+
方式 A:浏览器导出的 Cookie 数组
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { XScraper } from '@finch_ren/x-scraper';
|
|
151
|
+
|
|
152
|
+
async function main() {
|
|
153
|
+
const scraper = new XScraper();
|
|
154
|
+
|
|
155
|
+
const client = await scraper.getClientFromCookies([
|
|
156
|
+
{
|
|
157
|
+
domain: '.x.com',
|
|
158
|
+
name: 'ct0',
|
|
159
|
+
value: '<csrf_token>',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
domain: '.x.com',
|
|
163
|
+
name: 'auth_token',
|
|
164
|
+
value: '<auth_token>',
|
|
165
|
+
},
|
|
166
|
+
...
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
const profile = await client.getUserByScreenName({ screenName: 'jack' });
|
|
170
|
+
console.log(profile.raw.response.status);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
main().catch(console.error);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
方式 B:对象映射(`name -> value`)
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { XScraper } from '@finch_ren/x-scraper';
|
|
180
|
+
|
|
181
|
+
async function main() {
|
|
182
|
+
const scraper = new XScraper();
|
|
183
|
+
|
|
184
|
+
const client = await scraper.getClientFromCookies({
|
|
185
|
+
__cf_bm: '<__cf_bm>',
|
|
186
|
+
__cuid: '<__cuid>',
|
|
187
|
+
_ga: '<_ga>',
|
|
188
|
+
_ga_BLY4P7T5KW: '<_ga_BLY4P7T5KW>',
|
|
189
|
+
_twitter_sess: '<_twitter_sess>',
|
|
190
|
+
auth_token: '<auth_token>',
|
|
191
|
+
ct0: '<ct0>',
|
|
192
|
+
guest_id: '<guest_id>',
|
|
193
|
+
guest_id_ads: '<guest_id_ads>',
|
|
194
|
+
guest_id_marketing: '<guest_id_marketing>',
|
|
195
|
+
kdt: '<kdt>',
|
|
196
|
+
lang: '<lang>',
|
|
197
|
+
personalization_id: '<personalization_id>',
|
|
198
|
+
twid: '<twid>',
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const profile = await client.getUserByScreenName({ screenName: 'jack' });
|
|
202
|
+
console.log(profile.raw.response.status);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
main().catch(console.error);
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## 常用 API 入口
|
|
209
|
+
|
|
210
|
+
同一能力通常有两种调用方式:
|
|
211
|
+
|
|
212
|
+
- 顶层快捷方法:`client.getUserByScreenName(...)`
|
|
213
|
+
- 分组方法:`client.user.getUserByScreenName(...)`
|
|
214
|
+
|
|
215
|
+
### 两种写法对照(等价)
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
// User
|
|
219
|
+
const userA = await client.getUserByScreenName({ screenName: 'elonmusk' });
|
|
220
|
+
const userB = await client.user.getUserByScreenName({ screenName: 'elonmusk' });
|
|
221
|
+
|
|
222
|
+
// Tweet timeline
|
|
223
|
+
const tweetsA = await client.getUserTweets({ userId: '44196397' });
|
|
224
|
+
const tweetsB = await client.tweet.getUserTweets({ userId: '44196397' });
|
|
225
|
+
|
|
226
|
+
// Post actions
|
|
227
|
+
await client.createTweet({ tweetText: 'hello' });
|
|
228
|
+
await client.post.postCreateTweet({ tweetText: 'hello' });
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### 顶层快捷方法(高频)
|
|
232
|
+
|
|
233
|
+
- `client.getUserByScreenName(...)`
|
|
234
|
+
- `client.getUserTweets(...)`
|
|
235
|
+
- `client.getTweetDetail(...)`
|
|
236
|
+
- `client.createTweet(...)`
|
|
237
|
+
- `client.deleteTweet(...)`
|
|
238
|
+
- `client.likeTweet(...)`
|
|
239
|
+
- `client.unlikeTweet(...)`
|
|
240
|
+
- `client.retweet(...)`
|
|
241
|
+
....
|
|
242
|
+
|
|
243
|
+
### 分组 API(完整入口)
|
|
244
|
+
|
|
245
|
+
- `client.tweet`
|
|
246
|
+
- `client.user`
|
|
247
|
+
- `client.users`
|
|
248
|
+
- `client.userList`
|
|
249
|
+
- `client.post`
|
|
250
|
+
- `client.space`
|
|
251
|
+
- `client.v11Get`
|
|
252
|
+
- `client.v11Post`
|
|
253
|
+
- `client.v20Get`
|
|
254
|
+
- `client.default`
|
|
255
|
+
- `client.initialState`
|
|
256
|
+
....
|
|
257
|
+
|
|
258
|
+
## 认证与环境说明
|
|
259
|
+
|
|
260
|
+
### 1) 关于 Cookie
|
|
261
|
+
|
|
262
|
+
- 可使用浏览器插件 `Cookie-Editor` 导出 JSON 格式 Cookie,再传给 `getClientFromCookies([...])`。
|
|
263
|
+
- 不要把 Cookie 写入仓库,建议走本地文件或环境变量注入。
|
|
264
|
+
|
|
265
|
+
### 2) 平台 Header 一致性
|
|
266
|
+
|
|
267
|
+
某些账号 Cookie 与 `sec-ch-ua-platform` 不一致时可能失败,可手动覆盖:
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
import { XScraper } from '@finch_ren/x-scraper';
|
|
271
|
+
|
|
272
|
+
const scraper = new XScraper();
|
|
273
|
+
scraper.setAdditionalApiHeaders({
|
|
274
|
+
'sec-ch-ua-platform': '"Windows"',
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### 3) 风险与限制
|
|
279
|
+
|
|
280
|
+
- 该库依赖 X 私有接口,接口字段和行为可能随时变化。
|
|
281
|
+
- 登录态调用存在账号风控风险,请自行评估并使用低风险账号。
|
|
282
|
+
- 可能触发动态限流,建议在业务侧做重试与退避(backoff)。
|
|
283
|
+
|
|
284
|
+
## 本地开发
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
pnpm install
|
|
288
|
+
pnpm build
|
|
289
|
+
pnpm test
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## 重新生成 OpenAPI 代码
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
pnpm generate
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## openapi/placeholder.json 说明
|
|
299
|
+
|
|
300
|
+
`openapi/placeholder.json` 是运行时使用的 GraphQL 操作模板表,不是普通示例文件。
|
|
301
|
+
SDK 会在请求时读取这里的配置来组装参数与请求上下文。
|
|
302
|
+
|
|
303
|
+
每个条目通常包含:
|
|
304
|
+
|
|
305
|
+
- `@path`:接口路径(用于 transaction id 生成等)
|
|
306
|
+
- `@method`:HTTP 方法(用于 transaction id 生成等)
|
|
307
|
+
- `queryId`:GraphQL queryId
|
|
308
|
+
- `variables`:默认变量模板(可被调用参数覆盖)
|
|
309
|
+
- `features` / `fieldToggles`:默认开关参数
|
|
310
|
+
|
|
311
|
+
代码关联关系:
|
|
312
|
+
|
|
313
|
+
- `src/api.ts` 会加载该文件作为 `flagData`
|
|
314
|
+
- `src/utils/api.ts#getKwargs` 会把 `variables/features/fieldToggles` 转成请求参数
|
|
315
|
+
- 各 API utils(如 `client.tweet.*`、`client.user.*`、`client.space.*`)通过 `this.flag[...]` 取对应模板
|
|
316
|
+
|
|
317
|
+
注意:
|
|
318
|
+
|
|
319
|
+
- `client.space.getAudioSpaceById` 与 `client.space.getBroadcastQuery` 依赖该文件条目
|
|
320
|
+
- `client.getLiveVideoStreamStatus`(v1.1 接口)不依赖 `placeholder.json`
|
|
321
|
+
|
|
322
|
+
## 鸣谢
|
|
323
|
+
|
|
324
|
+
本项目核心代码来源于:
|
|
325
|
+
|
|
326
|
+
- [fa0311/twitter-openapi-typescript](https://github.com/fa0311/twitter-openapi-typescript)
|
|
327
|
+
|
|
328
|
+
在上游基础上,本项目做了工程整合与二次封装,包括:
|
|
329
|
+
|
|
330
|
+
- 整合 `twitter-openapi` 相关生成内容到当前 SDK 结构
|
|
331
|
+
- 提供更直接的调用入口与项目化组织方式
|
|
332
|
+
|
|
333
|
+
另外补充了 Space 相关接口能力(`client.space`):
|
|
334
|
+
|
|
335
|
+
- `getAudioSpaceById`
|
|
336
|
+
- `getLiveVideoStreamStatus`
|
|
337
|
+
- `getBroadcastQuery`
|