mcpField
G
gsearch

gsearch

github.com/monotykamary生産性484 回インストール1 日前に更新
リポジトリ

gsearch について

gsearchは、CDPを使ってGoogleを検索し、構造化された結果を取得するコマンドラインツールです。並列検索や結果リンク先の本文取得、JSON出力に対応し、ブラウザーを使う調査に利用できます。

主な機能

  1. 01Google検索からタイトル、URL、概要を取得
  2. 02整形テキストまたはJSONで出力
  3. 03互いに独立した並列検索
  4. 04結果リンクを開いてページ本文を抽出
  5. 05セレクター、待機方法、JSONページに対応

活用シーン

  1. 01複数の技術トピックを並行して検索
  2. 02ウェブ記事を検索して本文を読む
  3. 03JSONエンドポイントの応答を抽出

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/monotykamary/browser-harness-js --skill gsearch

インストール後、Claude Code で/gsearchを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

scripts/
gsearch
setup
test

詳細

Google Search

Google を検索し、CDP を介して構造化された結果を抽出します。browser-harness-js(CDP セッションを提供)以外の外部依存関係はありません。各呼び出しで専用のタブと WebSocket セッションを開くため、並列使用しても安全です。

クイック検索

gsearch "your query"            # pretty-printed, up to 10 results
gsearch "your query" 5          # 5 results, pretty-printed
gsearch --json "your query" 3   # raw JSON

並列使用

各 gsearch 呼び出しは共有 WebSocket を再利用しますが、それぞれのタブには呼び出しごとの sessionId を割り当てます。タブ固有の CDP 呼び出しは cdp(sessionId, method, params) を介して行います。複数の呼び出しを互いに干渉させずに同時実行できます。activeSessionId の上書きやタブの取り合い、イベントの誤配信は発生しません。タブは Target.closeTarget を使って非同期で閉じるため、クリーンアップ完了を待つ間、呼び出し元がブロックされることはありません。

gsearch "rust async" 3 &
gsearch "go channels" 3 &
wait

結果の形式

各結果は { title, url, snippet } です。

[
  {
    "title": "TypeScript 5.8",
    "url": "https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-8.html",
    "snippet": "TypeScript 5.8 introduces a stable --module node18 flag..."
  }
]

結果リンクを開く

gsearch follow <url> は結果リンクを直接開きます。再検索したり、Google 経由でアクセスしたりする必要はありません。ページの読み取り可能なテキストを返し、--json を指定した場合は解析済みの JSON を返します。接続、タブ作成、遷移、待機、評価の一連の処理をまとめているため、リンクごとに手作業で実装する必要はありません(ボット対策でブロックするサイトがあるため、curl を使う必要もありません)。

gsearch follow https://example.com/some-article                   # readable text (article/main/body)
gsearch follow https://example.com/some-article --selector "pre"  # custom CSS selector
gsearch follow https://example.com/some-article --settle 2000     # extra ms for lazy/SPA content
gsearch follow https://example.com/some-article --wait almostIdle # networkIdle (default) | almostIdle | load
gsearch follow https://example.com/api.json --json                # URL is JSON: poll, parse, print JSON

フラグは URL の前後どちらにも指定できます。デフォルトのセレクターは article, main, [role=main] です。--json を指定すると、以下の JSON 用の手順に切り替わります。

内部の仕組み(手動)

browser-harness-js を使って結果の url を直接開きます。再検索したり、Google 経由でアクセスしたりする必要はありません。アドホック検索と同じ接続、タブ作成、評価の流れで、リンクに移動してページのテキストを抽出します。

browser-harness-js <<'EOF'
if (!session.isConnected()) {
  try { await session.connect() } catch (e) { throw new Error("Cannot connect: " + e.message) }
}

const url = "https://example.com/some-article"
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
  const result = await cdp(sessionId, "Runtime.evaluate", {
    expression: 'document.querySelector("article, main")?.innerText || document.body.innerText',
    returnByValue: true
  })
  return result.result.value
} finally {
  session.closeTab(t.targetId, sessionId).catch(() => {})
}
EOF
  • article, main を使うとナビゲーションやフッターなどを除外します。フォールバックには document.body.innerText を使います。
  • 待機戦略。 この例では networkIdle(進行中のネットワークリクエストが500ミリ秒間ない状態)を待機します。コンテンツページでのデフォルトに適しています。load 後に発生するため、少なくとも同等の量のコンテンツを取得でき、loadEventFired のように終了しない広告や分析ビーコンにブロックされることもありません。ページの種類に応じた代替手段は次のとおりです。
    • networkAlmostIdle(250ミリ秒間の静止状態):継続的な XHR ポーリングがあり、500ミリ秒間の完全な静止状態にならないページ向けです。
    • loadEventFired:すべてのサブリソースの読み込みが本当に必要な場合に使います(テキスト抽出ではまれです)。
    • 評価の前に、準備完了後の短い待機 await new Promise(r => setTimeout(r, 1000)) を追加します。networkIdle 後にコンテンツを遅延レンダリングするページ(例:SPA のハイドレーションや遅延読み込み画像の一括表示)向けです。
  • JSON エンドポイント(--json): application/json への遷移では Page ライフサイクルイベントが一切発生せず、Chrome の JSON ビューアーは独自のスケジュールで本文を document.body.innerText にレンダリングします。waitFor('networkIdle', …) はハングするため使わないでください。また、固定時刻に innerText を一度だけ読み取ると、ビューアーの処理完了前に '' や一部しか取得できない切り詰められたデータが返されます。gsearch follow --json は JSON.parse が成功するまで innerText をポーリングします(このポーリング自体がヘッド検証です)。また、応答が実際には text/html(エラーページ、ログイン画面、ボット対策チャレンジ)の場合は早期に終了します。生のレシピとボット対策を回避する fetch() の落とし穴については、cdp スキルの json-navigation.md を参照してください。

スクリプトを使わないアドホック検索

gsearch が PATH にない場合も、同じロジックを browser-harness-js 経由で直接実行できます。

browser-harness-js <<'EOF'
if (!session.isConnected()) {
  try { await session.connect() } catch (e) { throw new Error("Cannot connect: " + e.message) }
}

const count = 10
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", {})
  await cdp(sessionId, "Page.setLifecycleEventsEnabled", { enabled: true })
  const ready = session.waitFor({ method: 'Page.lifecycleEvent', sessionId, predicate: (p) => p.name === 'networkIdle', timeoutMs: 30_000 })
  await cdp(sessionId, "Page.navigate", {
    url: "https://www.google.com/search?q=" + encodeURIComponent("your query") + "&num=" + count
  })
  await ready

  const result = await cdp(sessionId, "Runtime.evaluate", {
    expression: 'JSON.stringify([...document.querySelectorAll(".tF2Cxc")].slice(0, 10).map(el => ({ title: el.querySelector("h3")?.textContent?.trim() || "", url: el.querySelector("a[href]")?.href || "", snippet: el.querySelector(".VwiC3b")?.textContent?.trim() || "" })))',
    returnByValue: true
  })
  const results = JSON.parse(result.result.value)
  return results.map(r => r.title + "\n  " + r.url + "\n  " + r.snippet).join("\n\n")
} finally {
  session.closeTab(t.targetId, sessionId).catch(() => {})
}
EOF

仕組み

手順CDP 呼び出し内容
1session.connect() (once)共有 WebSocket をブラウザーに接続します
2Target.createTarget({ background: true })分離されたバックグラウンドタブを作成します
3Target.attachToTargetタブ単位のルーティングに使用する呼び出しごとの sessionId を取得します
4cdp(sessionId, "Page.enable", …)ページイベントを購読します
5cdp(sessionId, "Page.setLifecycleEventsEnabled", …)ライフサイクルイベントを有効にします。これを有効にしないと networkIdle は発生しません
6session.waitFor('Page.lifecycleEvent' networkIdle) armed BEFORE cdp(sessionId, "Page.navigate", …)競合を回避します。ナビゲーション前に networkIdle の待機を開始し(読み込みがすでに完了している競合を防ぎます)、その後 google.com/search?q=…&num=N に移動します(JS の encodeURIComponent で URI エンコードします)
7cdp(sessionId, "Runtime.evaluate", …)1 回の DOM クエリですべての結果を抽出します
8closeTab (fire-and-forget)応答をブロックせずにタブを破棄します

各呼び出しには約 2〜3 秒かかります(主に networkIdle の待機時間です)。WebSocket を共有するため、権限のポップアップが繰り返し表示されることはありません。URI エンコードと出力形式の整形は JS で行うため、jq は不要です。

アクセシビリティツリーではなく Runtime.evaluate を使う理由

Google の検索ページの AX ツリーには 1300 個を超えるノードがあり、結果の階層を再構築するにはノードごとに親を検索する必要があります。querySelectorAll('.tF2Cxc') を使う単一の Runtime.evaluate で、同じデータを 1 回の CDP 呼び出しで取得できます(AX ツリーの走査に約 200ms かかるのに対し、約 5ms です)。CSS セレクター(結果コンテナーの .tF2Cxc、タイトルの h3、スニペットの .VwiC3b)は、現在の Google の HTML 構造で安定しています。

jq は不要

URI エンコードには JS の encodeURIComponent() を使用し、出力形式の整形には heredoc 内の .map().join() を使用します。生のクエリは sed で JS の文字列補間用にエスケープします(bash 用のバックスラッシュ、$、バッククォート、および JS 用のシングルクォート)。REPL の renderResult は文字列の戻り値をそのまま渡し、JSON でラップしないため、bash はそのまま出力するだけです。

注意点

  • タブのクリーンアップには、fire-and-forget の closeTab を使う try/finally を使用します — closeTab は window.close() と Target.closeTarget を実行して確実にクリーンアップし、エラー時にも実行されるよう finally で囲んでいます。この呼び出しは応答をブロックしないよう、完了を待ちません。短時間に並列呼び出しを行うと、クローズ処理はセッションの closeQueue 内で順番に実行されますが、結果をブロックすることはありません。
  • 新しいタブごとに Page.enable() と Page.setLifecycleEventsEnabled({ enabled: true }) の両方を呼び出す必要があります。Chrome が Page.lifecycleEvent を発行するには後者が必要です。これを呼び出さないと、networkIdle の待機は毎回タイムアウトします。
  • networkIdle の待機には 30 秒のタイムアウトがあります — 生のプロミスではなく session.waitFor() を使用するため、ページが応答しなくなってもタブが残りません。XHR ポーリングが継続するページでは、500ms の静止期間に入らないことがあります。代替手段としての networkAlmostIdle については、「Following a result link」の待機戦略に関する注記を参照してください。
  • 結果数が num= より少ない場合があります — Google は要求した数より少ない結果を返すことがあります。
  • 地域によっては、Google が同意画面や Cookie の壁を表示することがあります — 以前の方法と同様に、結果は 0 件になります。結果が空の場合は、スクリーンショットで確認してください。
  • 複数ステートメントを含む heredoc では return が必要です — browser-harness-js が自動的に戻り値を設定するのは、単一の式だけです。

コメント