xsearch について
xsearchは、browser-harness-jsを通じてXを検索し、投稿を構造化して取得するコマンドラインスキルです。ブラウザーでXにログインしたユーザー向けで、整形出力やJSON出力、並列検索に対応します。
主な機能
- 01Xの該当投稿を検索
- 02投稿者、ハンドル、本文、URL、時刻を抽出
- 03整形結果またはJSONを出力
- 04複数の検索を並列実行
- 05browser-harness-jsで投稿URLを開く
活用シーン
- 01検索語に合う最近の投稿を収集
- 02後続処理向けに投稿の詳細を取得
- 03複数トピックを同時に検索
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ monotykamary/ browser-harness-js --skill xsearchインストール後、Claude Code で/xsearchを入力して読み込まれたか確認します。
同梱ファイルscripts/setupscripts/xsearch
詳細
X 検索
⚠️ ブラウザーで X にログインしている必要があります。 X の検索ページは、ログアウト中の訪問者には検索結果を表示せず、ログイン画面にリダイレクトします。
browser-harness-jsが使用するブラウザーセッションで、X にログイン済みである必要があります。
X(Twitter)を検索し、CDP 経由で構造化された結果を抽出します。browser-harness-js(CDP セッションを提供)以外に外部依存関係はありません。各呼び出しで専用のタブと WebSocket セッションが開かれるため、並列で安全に使用できます。
クイック検索
xsearch "your query" # pretty-printed, up to 10 results
xsearch "your query" 5 # 5 results, pretty-printed
xsearch --json "your query" 3 # raw JSON
並列での使用
各 xsearch 呼び出しは共有 WebSocket を再利用しますが、呼び出しごとの sessionId を使用して専用のタブに接続します。タブは Target.closeTarget を使って、呼び出し元がクリーンアップの完了を待たずに済むよう、完了を待たずに閉じられます。
xsearch "rust async" 3 &
xsearch "go channels" 3 &
wait
結果の形式
各結果は { author, handle, text, url, time } です。
[
{
"author": "Zane Chee",
"handle": "@injaneity",
"text": "people of pi, i'm excited to finally introduce browser use in pi-computer-use!",
"url": "https://x.com/injaneity/status/2065110712511500620",
"time": "2026-06-11T16:35:36.000Z"
}
]
URL から X の投稿を表示する
結果の url(または任意の x.com/<handle>/status/<id> パーマリンク)を
browser-harness-js 経由で直接開きます。再検索は不要です。アドホック検索と同じ接続/タブ作成/評価の流れを使い、
[data-testid="tweet"] セレクターを再利用しますが、フォーカス対象の投稿のみを取得します(パーマリンクでは DOM 順の先頭):
browser-harness-js <<'EOF'
if (!session.isConnected()) {
try { await session.connect() } catch (e) { throw new Error("Cannot connect: " + e.message) }
}
const url = "https://x.com/injaneity/status/2065110712511500620"
const t = await session.Target.createTarget({ url: "about:blank", background: true })
const { sessionId } = await session.Target.attachToTarget({ targetId: t.targetId, flatten: true })
try {
await cdp(sessionId, "Page.enable", {})
// Required — without this Chrome emits zero Page.lifecycleEvent, so networkIdle
// would never fire.
await cdp(sessionId, "Page.setLifecycleEventsEnabled", { enabled: true })
// Arm the wait BEFORE Page.navigate: lifecycle events fire once, and a fast
// load can fire networkIdle between navigate returning and the listener subscribing.
const ready = session.waitFor({ method: 'Page.lifecycleEvent', sessionId, predicate: (p) => p.name === 'networkIdle', timeoutMs: 30_000 })
await cdp(sessionId, "Page.navigate", { url })
await ready
await new Promise(r => setTimeout(r, 4000)) // React hydration; see Traps
const result = await cdp(sessionId, "Runtime.evaluate", {
expression: `(() => {
const el = document.querySelector('[data-testid="tweet"]')
if (!el) return JSON.stringify(null)
const textEl = el.querySelector('[data-testid="tweetText"]')
const timeEl = el.querySelector('time')
const userNamesEl = el.querySelector('[data-testid="User-Name"]')
const nameLinks = userNamesEl ? [...userNamesEl.querySelectorAll('a[role="link"]')] : []
return JSON.stringify({
author: nameLinks[0]?.textContent?.trim() || "",
handle: nameLinks[1]?.textContent?.trim() || "",
text: textEl?.innerText?.trim() || "",
time: timeEl?.getAttribute('datetime') || ""
})
})()`,
returnByValue: true
})
return result.result.value
} finally {
session.closeTab(t.targetId, sessionId).catch(() => {})
}
EOF
- フォーカス対象の投稿(パーマリンクが指す投稿)は、DOM 順で最初の
[data-testid="tweet"]です。返信と引用投稿はその下に表示されます。 - 4 秒のハイドレーション待機時間は
xsearchと同じです。networkIdleは React が投稿をレンダリングする前に発火します。ログアウト中の訪問者は、検索ページと同様に、パーマリンクでもログイン画面に移動します。 - 待機方法。 この例では
networkIdle(進行中のネットワークリクエストが 500ms 間ない状態)を待機します。X のページや多くのコンテンツサイトに適したデフォルトです。loadの後に発火するため、少なくとも同等のコンテンツを取得でき、loadEventFiredのように終わらない広告/分析ビーコンによって待機がブロックされることもありません。特定のページタイプでは、次の方法も利用できます。networkAlmostIdle(250ms 間リクエストがない状態):継続的な XHR ポーリングがあり、500ms の無通信状態に到達しないページに使用します。loadEventFired:すべてのサブリソースを確実に読み込む必要がある場合に使用します(テキスト抽出ではまれです)。- 評価の前に、準備完了後の短い
await new Promise(r => setTimeout(r, 1000))を追加します。networkIdleの後にコンテンツを遅延レンダリングするページで使用します。
仕組み
| 手順 | CDP 呼び出し | 内容 |
|---|---|---|
| 1 | session.connect() (once) | 共有 WebSocket をブラウザーに接続します |
| 2 | Target.createTarget({ background: true }) | 独立したバックグラウンドタブを作成します |
| 3 | Target.attachToTarget | タブ単位のルーティングに使う、呼び出しごとの sessionId を取得します |
| 4 | cdp(sessionId, "Page.enable", …) | ページイベントを購読します |
| 5 | cdp(sessionId, "Page.setLifecycleEventsEnabled", …) | ライフサイクルイベントを有効にします。これがないと networkIdle は発火しません |
| 6 | session.waitFor('Page.lifecycleEvent' networkIdle) を cdp(sessionId, "Page.navigate", …) の前に設定します | 競合状態を修正します。ナビゲーションの前に networkIdle の待機を設定することで、読み込み完了後に待機が開始される競合を防ぎ、その後 x.com/search?q=…&src=typed_query&f=top に移動します |
| 7 | setTimeout(4000) | React のハイドレーションと投稿のレンダリングを待ちます |
| 8 | スクロールループ(件数が 6 を超える場合) | スクロールして、さらに投稿を読み込みます(スクロールごとに約 3 件) |
| 9 | cdp(sessionId, "Runtime.evaluate", …) | data-testid セレクターを使って DOM を一度だけ検索します |
| 10 | closeTab(完了を待たずに実行) | 応答をブロックせずにタブを終了します |
使用する DOM セレクター
X の検索ページでは、安定した data-testid 属性が使われています。
| セレクター | 用途 |
|---|---|
[data-testid="tweet"] | 投稿コンテナー |
[data-testid="tweetText"] | 投稿本文 |
[data-testid="User-Name"] | 投稿者名/ハンドルのブロック |
User-Name 内の a[role="link"] | リンク:[0]=名前、[1]=ハンドル、[2]=パーマリンク+タイムスタンプ |
time | datetime 属性を使った ISO タイムスタンプ |
注意点
- ログインが必要です。 X はログアウト状態の訪問者には検索結果を表示せず、サインインを促す画面にリダイレクトします。ブラウザーセッションで X にログインしている必要があります。
- React のハイドレーションに時間がかかります。
networkIdleは React がツイートを描画する前に発生します。初期バッチ(約 6 件のツイート)が表示されるまで、準備完了シグナルの後に 4 秒待つ必要があります。 - 結果をさらに取得するにはスクロールします。 X は無限スクロールを使用します。初期表示には約 6 件のツイートが含まれます。
count > 6の場合、スクリプトは下へスクロールします(各スクロールで約 3 件のツイートが 1.5 秒の遅延後に読み込まれます)。 - 新しいタブごとに
Page.enable()とPage.setLifecycleEventsEnabled({ enabled: true })の両方を呼び出す必要があります。 後者は、Chrome がPage.lifecycleEventを発行するために必要です。これを呼び出さないと、networkIdleの待機は毎回タイムアウトします。 networkIdleの待機には 30s のタイムアウトがあります。 生の Promise の代わりにsession.waitFor()を使用するため、ページが応答しなくなってもタブが残りません。X は継続的に読み込みを行いますが、実際には初期のネットワーク通信は約 2~3 秒で落ち着きます。ページが 500ms の静穏時間に一度も達しない場合は、代わりにnetworkAlmostIdleを使用してください(上記の待機戦略に関する注記を参照)。- 結果数が要求数を下回る場合があります。 X に一致するツイートが十分にない場合や、スクロールループで時間内に読み込めない場合があります。
- ツイートのテキストが省略される場合があります。 長いツイートでは、X は「さらに表示」ボタンを表示します。抽出される
textは、「さらに表示」をクリックせずに表示される部分です。 jqへの依存はありません。 URI のエンコードには JS のencodeURIComponent()を使用し、出力の整形にはヒアドキュメント内の.map().join()を使用します。