mcpField
G
gsearch

gsearch

github.com/monotykamary生产力484 次安装1 天前更新
代码仓库

关于 gsearch

gsearch 是通过 CDP 搜索 Google 并提取结构化结果的命令行工具,适合需要在浏览器会话中检索和阅读网页的工作流。它支持并行搜索、直接跟进结果链接,并可输出 JSON。

主要功能

  1. 01搜索 Google 并提取标题、网址和摘要
  2. 02以格式化文本或 JSON 输出结果
  3. 03并行运行互不干扰的搜索
  4. 04打开结果链接并提取网页文本
  5. 05支持选择器、等待策略及 JSON 页面

适用场景

  1. 01并行查询多个技术主题
  2. 02搜索并阅读网页文章内容
  3. 03提取 JSON 端点的响应内容

安装

复制下面的命令,在终端中运行。

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

安装后在 Claude Code 中输入/gsearch确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

scripts/
gsearch
setup
test

详细介绍

Google 搜索

通过 CDP 搜索 Google 并提取结构化结果。除 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 会轮询 innerText,直到 JSON.parse 成功(轮询本身就是响应头验证),如果响应实际为 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) 在 cdp(sessionId, "Page.navigate", …) 之前启动修复竞态:在导航前启动 networkIdle 等待(避免页面加载已完成的竞态),然后访问 google.com/search?q=…&num=N(在 JS 中通过 encodeURIComponent 进行 URI 编码)
7cdp(sessionId, "Runtime.evaluate", …)通过单次 DOM 查询提取所有结果
8closeTab(即发即弃)关闭标签页,不阻塞响应

每次调用耗时约 2–3 秒(主要耗在等待 networkIdle 上)。共享 WebSocket 无需反复弹出权限提示。URI 编码和输出格式化都在 JS 中完成——不依赖 jq。

为什么使用 Runtime.evaluate 而不是无障碍树

Google 搜索页面的 AX 树有 1300 多个节点——遍历它需要逐个查询节点的父节点来重建结果层级。使用一次 Runtime.evaluate 调用 querySelectorAll('.tF2Cxc'),即可通过单次 CDP 调用返回相同数据(约 5 ms,而 AX 树遍历约需 200 ms)。CSS 选择器(用于结果容器的 .tF2Cxc、用于标题的 h3、用于摘要的 .VwiC3b)在 Google 当前的 HTML 结构中保持稳定。

不依赖 jq

URI 编码在 JS 中使用 encodeURIComponent(),输出格式化则通过 heredoc 中的 .map().join() 完成。使用 sed 对原始查询进行转义,以便插入 JS 字符串(bash 中转义反斜杠、$ 和反引号;JS 中转义单引号)。REPL 的 renderResult 会直接传递字符串返回值,不会套上 JSON,因此 bash 只需打印即可。

注意事项

  • 使用 try/finally 和即发即弃的 closeTab 清理标签页——closeTab 会执行 window.close() 和 Target.closeTarget,以彻底清理;它被封装在 finally 中,因此即使发生错误也会运行。该调用不会等待完成,因此不会阻塞响应。快速并行调用时,关闭操作会在会话的 closeQueue 中排队执行,但不会阻塞结果。
  • 每个新标签页都必须调用 Page.enable() 和 Page.setLifecycleEventsEnabled({ enabled: true })。后者是 Chrome 发出任何 Page.lifecycleEvent 的必要条件——不调用时,networkIdle 等待每次都会超时。
  • networkIdle 等待有 30s 超时——使用 session.waitFor() 而不是原始 Promise,因此页面卡住时不会导致标签页泄漏。持续进行 XHR 轮询的页面可能永远无法达到 500 ms 的静默窗口——若要使用 networkAlmostIdle 作为备用方案,请参阅“Following a result link”中的等待策略说明。
  • 结果数量可能少于 num= 指定的数量——Google 有时返回的结果会少于请求数量。
  • Google 在某些地区可能会显示同意/ Cookie 墙——这会返回 0 条结果,与旧方案相同。如果结果为空,请用截图确认。
  • 多语句 heredoc 需要使用 return——browser-harness-js 仅会自动返回单个表达式。

评论

支持 Markdown