gsearch について
gsearchは、CDPを使ってGoogleを検索し、構造化された結果を取得するコマンドラインツールです。並列検索や結果リンク先の本文取得、JSON出力に対応し、ブラウザーを使う調査に利用できます。
主な機能
- 01Google検索からタイトル、URL、概要を取得
- 02整形テキストまたはJSONで出力
- 03互いに独立した並列検索
- 04結果リンクを開いてページ本文を抽出
- 05セレクター、待機方法、JSONページに対応
活用シーン
- 01複数の技術トピックを並行して検索
- 02ウェブ記事を検索して本文を読む
- 03JSONエンドポイントの応答を抽出
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ monotykamary/ browser-harness-js --skill gsearchインストール後、Claude Code で/gsearchを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
scripts/gsearchsetuptest詳細
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 呼び出し | 内容 |
|---|---|---|
| 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) armed BEFORE cdp(sessionId, "Page.navigate", …) | 競合を回避します。ナビゲーション前に networkIdle の待機を開始し(読み込みがすでに完了している競合を防ぎます)、その後 google.com/search?q=…&num=N に移動します(JS の encodeURIComponent で URI エンコードします) |
| 7 | cdp(sessionId, "Runtime.evaluate", …) | 1 回の DOM クエリですべての結果を抽出します |
| 8 | closeTab (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が自動的に戻り値を設定するのは、単一の式だけです。