TwexAPI で X 投稿のリプライをページ単位で取得する方法
リプライは、投稿が会話へ変わる場所です。ローンチ反応、コミュニティの反応、モデレーションレビュー用データを扱うなら、root tweet だけでは足りません。必要なのは、リプライ、取得時の並び順、そしてデータセットがどのように収集されたかを説明できる cursor の履歴です。
新しい実装では、TwexAPI のページング対応リプライエンドポイント POST /twitter/tweets/{tweet_id}/replies/page を使います。リプライをページ単位で返すため、大きなスレッドを 1 回で取得しようとしてタイムアウトするリスクを避けやすくなります。
エンドポイントとソート
POST リクエストを次の URL に送ります。
https://api.twexapi.io/twitter/tweets/<tweet_id>/replies/pagepath パラメータは root tweet の ID です。JSON 本文では次を指定できます。
| フィールド | 必須 | 使い方 |
|---|---|---|
sort_by | いいえ | Recency、Relevance、Likes。1 回のエクスポートでは同じ sort mode を使い続けます。 |
next_cursor | いいえ | 初回は空にします。次ページでは前回レスポンスの next_cursor を渡します。 |
最初のページには root_tweet と最初のリプライ一覧 data が含まれることがあります。後続ページでは、さらにリプライ、has_next_page、next_cursor が返ります。
最初のページを取得する
時系列に近いエクスポートが必要なら Recency を使います。Relevance や Likes は、ランキング自体が分析対象のときに使います。
curl --request POST \
--url https://api.twexapi.io/twitter/tweets/<tweet_id>/replies/page \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{
"sort_by": "Recency"
}'簡略化したレスポンスは次のようになります。
1{
2 "code": 200,
3 "msg": "success",
4 "root_tweet": {
5 "tweet_id": "1803006263529541838",
6 "text": "<root tweet text>"
7 },
8 "data": [
9 {
10 "tweet_id": "1234567890123456789",
11 "text": "<reply text>",
12 "created_at": "Mon Jul 01 12:34:56 +0000 2025",
13 "user": {
14 "screen_name": "example_user"
15 }
16 }
17 ],
18 "has_next_page": true,
19 "next_cursor": "cursor_from_this_response"
20}next_cursor で続きへ進む
has_next_page が true の場合は、cursor を次のリクエストに渡します。ページング中は同じ tweet_id と sort_by を維持してください。
curl --request POST \
--url https://api.twexapi.io/twitter/tweets/<tweet_id>/replies/page \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{
"sort_by": "Recency",
"next_cursor": "cursor_from_this_response"
}'途中で sort mode を変えると、結果を説明しにくくなります。別のランキング視点が必要なら、別のエクスポートとして実行し、明確にラベルを付けます。
Python エクスポートスクリプト
次のスクリプトは、リプライを JSONL に書き出し、root tweet を 1 回だけ保存し、tweet_id で重複排除し、監査用の page メタデータを保存します。
1import json
2import time
3from datetime import datetime, timezone
4from pathlib import Path
5
6import requests
7
8TOKEN = "<your_bearer_token>"
9TWEET_ID = "<tweet_id>"
10SORT_BY = "Recency"
11URL = f"https://api.twexapi.io/twitter/tweets/{TWEET_ID}/replies/page"
12REPLIES_OUT = Path(f"{TWEET_ID}-replies.jsonl")
13ROOT_OUT = Path(f"{TWEET_ID}-root.json")
14META_OUT = Path(f"{TWEET_ID}-replies-meta.json")
15
16headers = {
17 "Authorization": f"Bearer {TOKEN}",
18 "Content-Type": "application/json",
19}
20
21cursor = None
22page_number = 0
23seen_reply_ids = set()
24page_log = []
25
26with REPLIES_OUT.open("w", encoding="utf-8") as replies_file:
27 while True:
28 payload = {"sort_by": SORT_BY}
29 if cursor:
30 payload["next_cursor"] = cursor
31
32 response = requests.post(URL, headers=headers, json=payload, timeout=30)
33 response.raise_for_status()
34 body = response.json()
35
36 page_number += 1
37
38 if page_number == 1 and body.get("root_tweet"):
39 ROOT_OUT.write_text(
40 json.dumps(body["root_tweet"], ensure_ascii=False, indent=2),
41 encoding="utf-8",
42 )
43
44 replies = body.get("data") or []
45 for reply in replies:
46 reply_id = reply.get("tweet_id") or reply.get("id")
47 if not reply_id or reply_id in seen_reply_ids:
48 continue
49 seen_reply_ids.add(reply_id)
50 replies_file.write(json.dumps(reply, ensure_ascii=False) + "\n")
51
52 page_log.append({
53 "page": page_number,
54 "items": len(replies),
55 "has_next_page": body.get("has_next_page"),
56 "next_cursor": body.get("next_cursor"),
57 })
58
59 if not body.get("has_next_page") or not body.get("next_cursor"):
60 break
61
62 cursor = body["next_cursor"]
63 time.sleep(1)
64
65META_OUT.write_text(json.dumps({
66 "tweet_id": TWEET_ID,
67 "sort_by": SORT_BY,
68 "exported_at": datetime.now(timezone.utc).isoformat(),
69 "unique_replies": len(seen_reply_ids),
70 "pages": page_log,
71}, ensure_ascii=False, indent=2), encoding="utf-8")
72
73print(f"Saved {len(seen_reply_ids)} replies to {REPLIES_OUT}")定期ジョブでは、各ページの取得に成功するたびに最後の cursor を保存します。失敗しても、すでに収集したページを捨てずに再開できます。
保存しておきたいフィールド
最低限、次を保存します。
- 生の reply オブジェクト。
tweet_id、created_at、本文、作者フィールド、レスポンスに含まれるエンゲージメント数。- 返却される場合は
in_reply_to、reply_to、conversation_id、関連 thread フィールド。 - root
tweet_id、sort_by、エクスポート時刻、ページ番号、cursor メタデータ。 https://x.com/i/web/status/<reply_id>のような正規化済み reply URL。
正規化済み行だけでなく、raw data も残します。reply schema は変わることがあり、後続分析で想定外のフィールドが必要になることもあります。
分析時の注意
リプライエクスポートは次に役立ちます。
- プロダクトやキャンペーン公開直後の反応を測る。
- アナウンス後の顧客の懸念を確認する。
- 特定投稿を中心にモデレーションキューを作る。
- 時系列順といいね順で議論の見え方がどう変わるか比較する。
- 感情分析やトピックモデリングの前に、手動ラベル用のサンプルを作る。
リプライデータを完全な世論調査として扱わないでください。リプライはランキング、削除、アカウント可視性、返信する人の偏りに影響されます。データセットの限界が見えるよう、十分な文脈を保存します。
よくある落とし穴
- 最初の 1 ページだけを取得して、会話全体として扱う。
- ページング途中で
sort_byを変える。 root_tweetを捨ててしまい、リプライデータセットの文脈を失う。- 言語判定や手動確認なしに感情分析を実行する。
- タイムスタンプなしでエンゲージメント数を上書きする。
まとめ
リプライ収集では、POST /twitter/tweets/{tweet_id}/replies/page を使い、sort_by を固定し、next_cursor で続きのページを取得し、root tweet と raw reply 行を保存します。1 回の固定件数リクエストより、監査、再試行、説明がしやすい流れになります。