高度検索で Donald Trump の X 投稿を収集する方法
X 上の公人を追跡するとき、難しいのは最初のリクエストではありません。重要なのは、検索条件を再現できる形で残し、ページングを途中で失わず、後から別の人がデータセットを検証できる状態にしておくことです。
この記事では、TwexAPI の Advanced Twitter Search by Page エンドポイントを使い、@realDonaldTrump の公開投稿を収集します。同じ手順は他の公開アカウントにも使えます。高度検索クエリを決め、最初のページを取得し、返された next_cursor を保存し、has_next_page が false になるか対象範囲に到達するまで次のページを取得します。
このワークフローを使う場面
高度検索式に一致する公開 X 投稿を、再現しやすい形で取得したいときにページング版の Advanced Search が向いています。Trump の監視や調査であれば、主な用途は次のようなものです。
- 指定した期間で
from:realDonaldTrumpの投稿を収集する。 economy、election、tradeなどのトピック語でデータセットを絞る。-filter:retweetsでリポストを除外し、元投稿と引用コメントを中心に見る。- 要約、感情分析、ダッシュボード作成の前に、生の API レスポンスを保存する。
1 ページだけを完全なアーカイブとして扱わないでください。各ページは現在の結果セットの一部であり、意図した cursor チェーンを最後まで進めて初めて、その範囲のデータセットとして説明できます。
エンドポイントとリクエスト本文
POST リクエストを次の URL に送ります。
https://api.twexapi.io/twitter/advanced_search/pageリクエスト本文には 3 つのフィールドがあります。
| フィールド | 必須 | 使い方 |
|---|---|---|
searchTerms | はい | 高度検索クエリ文字列の配列。この用途では from:realDonaldTrump -filter:retweets から始めると扱いやすいです。 |
sortBy | はい | 時系列で収集するなら Latest。ランキング結果が必要な場合だけ Top を使います。 |
next_cursor | いいえ | 初回は空にします。次ページでは前回レスポンスの next_cursor を渡します。 |
レスポンスには data、has_next_page、next_cursor が含まれます。エクスポートのメタデータとして、この 3 つも保存しておくと後から検証しやすくなります。
最初のページを取得する
まずは狭く明確なクエリを使い、sortBy: "Latest" を指定します。これにより、次ページが新しい順の結果セットに沿って続くため、収集結果を説明しやすくなります。
curl --request POST \
--url https://api.twexapi.io/twitter/advanced_search/page \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{
"searchTerms": [
"from:realDonaldTrump -filter:retweets"
],
"sortBy": "Latest"
}'短くしたレスポンス例は次の通りです。
1{
2 "code": 200,
3 "msg": "success",
4 "data": [
5 {
6 "tweet_id": "1234567890123456789",
7 "created_at": "Mon Jul 01 12:34:56 +0000 2025",
8 "text": "<API が返した投稿本文>",
9 "favorite_count": 1000,
10 "retweet_count": 200
11 }
12 ],
13 "has_next_page": true,
14 "next_cursor": "cursor_from_this_response"
15}next_cursor で次ページへ進む
has_next_page が true の場合は、前回レスポンスの cursor を次のリクエストに渡します。
curl --request POST \
--url https://api.twexapi.io/twitter/advanced_search/page \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{
"searchTerms": [
"from:realDonaldTrump -filter:retweets"
],
"sortBy": "Latest",
"next_cursor": "cursor_from_this_response"
}'cursor を自作したり、別のクエリで返された cursor を再利用したりしないでください。cursor は、それを生成した検索条件とソート方法に紐づいています。
Python エクスポートスクリプト
次のスクリプトは、各投稿を JSONL に 1 行ずつ書き出し、tweet_id で重複排除し、cursor の流れを小さなメタデータファイルに保存します。
1import json
2import time
3from datetime import datetime, timezone
4from pathlib import Path
5
6import requests
7
8TOKEN = "<your_bearer_token>"
9URL = "https://api.twexapi.io/twitter/advanced_search/page"
10QUERY = "from:realDonaldTrump -filter:retweets"
11SORT_BY = "Latest"
12OUT = Path("realDonaldTrump-posts.jsonl")
13META = Path("realDonaldTrump-posts-meta.json")
14
15headers = {
16 "Authorization": f"Bearer {TOKEN}",
17 "Content-Type": "application/json",
18}
19
20seen_ids = set()
21cursor = None
22page = 0
23cursor_log = []
24
25with OUT.open("w", encoding="utf-8") as f:
26 while True:
27 payload = {
28 "searchTerms": [QUERY],
29 "sortBy": SORT_BY,
30 }
31 if cursor:
32 payload["next_cursor"] = cursor
33
34 response = requests.post(URL, headers=headers, json=payload, timeout=30)
35 response.raise_for_status()
36 body = response.json()
37
38 page += 1
39 items = body.get("data") or []
40 for item in items:
41 tweet_id = item.get("tweet_id")
42 if not tweet_id or tweet_id in seen_ids:
43 continue
44 seen_ids.add(tweet_id)
45 f.write(json.dumps(item, ensure_ascii=False) + "\n")
46
47 cursor_log.append({
48 "page": page,
49 "items": len(items),
50 "next_cursor": body.get("next_cursor"),
51 "has_next_page": body.get("has_next_page"),
52 })
53
54 if not body.get("has_next_page") or not body.get("next_cursor"):
55 break
56
57 cursor = body["next_cursor"]
58 time.sleep(1)
59
60META.write_text(json.dumps({
61 "query": QUERY,
62 "sortBy": SORT_BY,
63 "exported_at": datetime.now(timezone.utc).isoformat(),
64 "unique_tweets": len(seen_ids),
65 "pages": cursor_log,
66}, ensure_ascii=False, indent=2), encoding="utf-8")
67
68print(f"Saved {len(seen_ids)} unique posts to {OUT}")長時間動くジョブでは、一時的なネットワーク失敗に備えてリトライを入れ、次ページを取得する前に最後に成功した cursor を保存しておくと安心です。中断しても 1 ページ目からやり直す必要がありません。
クエリ例
最初は単純に始め、フィルタは 1 つずつ追加します。どの条件が結果セットを変えたのか追いやすくなります。
| 目的 | searchTerms の値 |
|---|---|
| アカウントの最新公開投稿 | from:realDonaldTrump -filter:retweets |
| 経済に言及した投稿 | from:realDonaldTrump economy -filter:retweets |
| 月単位の固定期間 | from:realDonaldTrump since:2024-01-01 until:2024-02-01 -filter:retweets |
| メディア付き投稿 | from:realDonaldTrump filter:media -filter:retweets |
| 投票関連の表現 | from:realDonaldTrump (vote OR voting OR ballot) -filter:retweets |
複数トピックを比較する場合は、トピックごとに別々のクエリを実行し、各結果にクエリ文字列を保存するのがおすすめです。多くのトピックを 1 つのクエリに詰め込むと、後の分析を説明しにくくなります。
保存しておきたい項目
最低限、次を保存します。
- API が返した生の tweet オブジェクト。
tweet_id、created_at、本文、作者フィールド、レスポンスに含まれるエンゲージメント数。- 正確な
searchTerms、sortBy、エクスポート時刻、cursor チェーン。 https://x.com/realDonaldTrump/status/<tweet_id>のような正規化済み投稿 URL。
分析では、ある時点で取得したエンゲージメント数だけに依存しすぎないようにします。数値は変わるため、各エクスポートにタイムスタンプを付け、後の更新で上書きするのか、新しいスナップショットとして追加するのかを決めておきます。
よくある落とし穴
- ページングの途中で
searchTermsを変えると、cursor チェーンの意味が失われます。 - 履歴エクスポートで
Topを使うと、監査しにくい並びになることがあります。ランキングが目的でない限りLatestを使います。 - 生レスポンスを早く捨てると、後からフィールドマッピングを再確認しにくくなります。
- 公人の投稿を完全に中立なデータとして扱うと、文脈が抜け落ちます。日付、検索語、フィルタ判断もデータと一緒に残してください。
まとめ
Trump 投稿の収集では、TwexAPI の明快な流れは「高度検索クエリを書く、POST /twitter/advanced_search/page を呼ぶ、next_cursor でページングする、生データとメタデータを保存する」です。こうしておくと、データセットを再実行し、検査し、説明しやすくなります。
最初のエクスポートをベースラインにして、その後に定期更新、トピック別データセット、新規投稿アラートが必要かを決めると進めやすいです。