关于 gmaps
gmaps 是通过 CDP 搜索 Google Maps 并获取路线的命令行技能。它支持多种出行方式,也可按直线距离估算固定起点的站点顺序,再查询实际路线。
主要功能
- 01搜索 Google Maps 本地商家
- 02获取多种出行方式的路线
- 03估算固定起点的站点顺序
- 04以文本或 JSON 返回结果
- 05通过浏览器标签页并行执行
适用场景
- 01查找地点附近的商家
- 02比较路线和出行方式
- 03规划多个地点的访问顺序
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ monotykamary/ browser-harness-js --skill gmaps安装后在 Claude Code 中输入/gmaps确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
scripts/gmapssetuptest详细介绍
gmaps — 通过 CDP 使用 Google Maps(搜索、路线规划、尽力而为的 TSP)
通过 CDP 免费、免密钥地访问 Google Maps——直接从渲染后的页面获取数据,与付费的 Google Places 和 Directions API 所售的数据相同。无需 API 密钥、无需配额、无需 jq。有三种模式:
- search(默认)——根据查询查找本地商家。
--route——为按指定顺序排列的地点提供真实驾车路线(总时间和距离,含当前路况)。--optimize——尽力计算最快的访问顺序(开放路径 TSP,起点固定为第一个地点)。
每次调用都会单独打开一个后台标签页和 WebSocket 会话,可安全并行使用。
命令
搜索(默认)
gmaps "coffee shops in Austin TX" # up to 10 results, pretty
gmaps "sushi near Times Square" 5 # 5 results
gmaps "pharmacy open now Berlin" 20 # 20 (scrolls the feed)
gmaps --json "coffee shops in Austin TX" 5 # raw JSON
| 参数 | 含义 |
|---|---|
--json | 输出原始 JSON,而不是排版后的文本 |
<query> | Google Maps 搜索内容,例如 coffee shops in Austin TX |
[count] | 结果数量(默认 10,最多 30——会滚动信息流以加载更多结果) |
路线规划(--route)
gmaps --route "Austin, TX" "Houston, TX" # real time + distance (driving)
gmaps --route --mode transit "London, UK" "Paris, France" # by travel mode
gmaps --route "Austin, TX" "Houston, TX" "Dallas, TX" # ordered multi-stop, total
gmaps --route --json --mode cycling "Austin, TX" "Houston, TX" # raw JSON
按给定顺序和出行 --mode(默认 driving)规划真实路线。读取总时间和距离(含当前路况)以及解析后的途经点名称。第一个地点为起点。最多支持 25 个地点。折叠的路线摘要中不含每段路程的时间(Maps 只显示总时间)——打开 url 可查看完整的逐向导航。
尽力计算顺序(--optimize)
gmaps --optimize "Austin, TX" "Houston, TX" "Dallas, TX" "San Antonio, TX"
gmaps --optimize --mode transit "Austin, TX" "Houston, TX" "Dallas, TX"
gmaps --optimize --json "Austin, TX" "Houston, TX" "Dallas, TX"
计算最快的访问顺序,作为起点固定(第一个地点)的开放路径 TSP。第一个地点是起点;其余地点按总直线距离最短的顺序排列,然后针对该顺序进行一次真实路线规划调用(使用所选的 --mode,默认 driving)——因此,尽管顺序是根据直线距离估算的,报告的时间和距离仍然是真实值。
- 并行将每个地点解析为精确的经纬度(后台标签页轮询解析后地点 URL 中的
!3d!4d坐标)——共 N 次查询,而非 N² 次。 - 路段距离虚拟化为直线距离(haversine),并报告每段路程的罗盘方位。出行
--mode只影响最后一次真实路线规划调用,不影响 TSP 顺序。 - 最多 12 个地点时,使用 Held-Karp 算法精确求解 TSP。
- 浏览器调用总数:N + 1(N 次地点解析 + 1 次路线渲染)。
出行模式(--mode)
--mode | 返回内容 |
|---|---|
driving (默认) | 时间 + 距离 + 途经点 + 过路费(常规 Maps 路线) |
transit | 最佳公交车/火车/渡轮/地铁选项的时间(无距离/途经点) |
walking | 时间 + 距离(时长以小时显示,例如 62 hr) |
cycling | 时间 + 距离(可能包含渡轮/汽车运输说明) |
flights | 仅直飞时间(尽力而为——航班卡片位于路线面板之外) |
best | Maps“最佳”标签页选择的路线(大多数路线为驾车;更快时会选择公共交通/航班) |
- 不支持
motorcycle或ferry模式。 Maps 顶层标签页只有上述六种;摩托车路线通过driving规划,渡轮则作为driving/transit路线中的路段出现。(在越南或美国,Maps 不会显示专门的两轮车标签页——只有部分地区/国家会显示。) - 页面默认以
best模式加载;对于其他模式,gmaps会点击对应的出行模式标签页。不可用的模式(例如岛屿路线的cycling)会返回明确的错误。 - 适用于
--route和--optimize。
并行使用
每次调用都会单独打开一个后台标签页,可安全并发运行:
gmaps "coffee Austin" 5 & gmaps "coffee Seattle" 5 & wait
gmaps --route "Austin, TX" "Houston, TX" & gmaps --route "Dallas, TX" "San Antonio, TX" & wait
返回结果格式
搜索 — 排版模式(默认)
coffee shops in Austin TX · 5 results
1. Jo's Coffee – South Congress ★4.4 (1972) $1–10 Coffee shop
1300 S Congress Ave · Open · Closes 7 PM
30.2510458, -97.7493717 · ChIJ1S_Bov20RIYRe7MiR8
https://www.google.com/maps/place/Jo%27s+Coffee...
搜索 — --json
{
"query": "coffee shops in Austin TX",
"count": 5,
"results": [
{
"name": "Jo's Coffee – South Congress",
"rating": 4.4,
"review_count": 1972,
"price": "$1–10",
"category": "Coffee shop",
"address": "1300 S Congress Ave",
"hours": "Open · Closes 7 PM",
"lat": 30.2510458,
"lng": -97.7493717,
"place_id": "ChIJ1S_Bov20RIYRe7MiR8",
"url": "https://www.google.com/maps/place/Jo%27s+Coffee…"
}
]
}
每条结果包含:name, rating, review_count, price, category, address, hours, lat, lng, place_id, url。缺失字段为 null(排版模式下为 -)——例如,没有价格等级或街道地址的地点。
路线 — 排版模式
Route (driving): Austin, TX → Houston, TX
2 hr 31 min 165 miles via State Hwy 71 E and I-10 E
Fastest route, the usual traffic · tolls
1. Austin, Texas, USA
2. Houston, Texas, USA
https://www.google.com/maps/dir/Austin,+TX/Houston,+TX/
(driving) 表示 --mode(默认为该值)。对于非常短的路线以及 transit/flights(无距离),distance(和 via)可能为 null。只有路线包含收费路段时才会出现 tolls。
路线 — --json
{
"mode": "route",
"travel_mode": "driving",
"places": ["Austin, TX", "Houston, TX"],
"route": {
"duration": "2 hr 31 min",
"distance": "165 miles",
"via": "via State Hwy 71 E and I-10 E",
"label": "Fastest route, the usual traffic",
"tolls": true,
"waypoints": ["Austin, Texas, USA", "Houston, Texas, USA"],
"url": "https://www.google.com/maps/dir/Austin,+TX/Houston,+TX/"
}
}
优化 — pretty
Optimized route · best-effort (straight-line TSP) · driving · fixed start: Austin, TX, USA
1. Austin, TX, USA (30.267153, -97.7430608) start
2. San Antonio, TX, USA (29.4251905, -98.4945922) ← 118.4 km SW (218°)
3. Houston, TX, USA (29.7600771, -95.3701108) ← 304.4 km E (82°)
4. Dallas, TX, USA (32.7766642, -96.7969879) ← 361.8 km N (338°)
Straight-line total: 784.6 km
Real driving for this order:
7 hr 50 min 516 miles via I-35 S
Fastest route now due to traffic conditions · tolls
Austin, TX, USA → San Antonio, TX, USA → Houston, TX, USA → Dallas, TX, USA
https://www.google.com/maps/dir/Austin,+TX/San+Antonio,+TX/Houston,+TX/Dallas,+TX/
Note: order is a best-effort estimate from straight-line (haversine) distances;
the driving time above is the real route for that order.
优化 — --json
{
"mode": "optimize",
"travel_mode": "driving",
"places": [{ "query": "Austin, TX", "name": "Austin, TX, USA", "lat": 30.267153, "lng": -97.7430608, "url": "…" }, …],
"order": ["Austin, TX, USA", "San Antonio, TX, USA", "Houston, TX, USA", "Dallas, TX, USA"],
"order_indices": [0, 3, 1, 2],
"legs": [{ "from": "Austin, TX, USA", "to": "San Antonio, TX, USA", "km": 118.4, "bearing": 218, "dir": "SW" }, …],
"straight_line_total_km": 784.6,
"route": { "duration": "7 hr 50 min", "distance": "516 miles", "via": "via I-35 S", "label": "…", "tolls": true, "waypoints": […], "url": "…" },
"note": "Order is a best-effort estimate from straight-line (haversine) distances; the driving time/distance is the real route for that order."
}
工作原理
搜索
| 步骤 | 执行内容 |
|---|---|
| 1 | session.connect()(一次)——连接浏览器的共享 WebSocket |
| 2 | Target.createTarget({ background: true }) + attachToTarget ——隔离的后台标签页 + 每次调用专属的 sessionId |
| 3 | 导航至 https://www.google.com/maps/search/<query>(URI 编码;空格用 + 表示) |
| 4 | 轮询 a[href*='/maps/place/'] 的数量,直到大于 0——这是真正的就绪信号。Chrome 不会为 Maps 触发 networkIdle(持续的 XHR 轮询会让 500ms 静默窗口始终无法开启),因此等待生命周期事件会超时。轮询信息流更快也更可靠。 |
| 5 | 滚动信息流,加载最多 count 条结果。信息流最初会渲染约 7 张卡片;继续滚动至底部时会加载更多。达到 count、连续两次滚动后数量没有增长(结果已到底),或达到 25 次滚动上限时停止。 |
| 6 | 使用一次 Runtime.evaluate 解析所有结果卡片并返回 JSON 数组。 |
| 7 | 在 finally 中以即发即弃方式调用 closeTab——清理操作不会阻塞响应。 |
路线(--route / --optimize)
| 步骤 | 执行内容 |
|---|---|
| 1 | 导航至 https://www.google.com/maps/dir/<p0>/<p1>/…/(路径形式,空格用 + 表示)。?query= 形式不会渲染。页面会以 Maps 的“最佳”模式加载。 |
| 2 | **轮询路线面板,直到出现路线耗时。**与信息流一样,networkIdle 永远不会触发。路线列表中的耗时以 min/hr 结尾;出行方式标签页使用简写形式(2h 30m、16 hr),不含 min 标记,因此在 [aria-label="Directions"] 面板中轮询 /\d+\s*min\b/ 可等待真实路线,同时忽略出行方式标签页。也接受距离叶节点(miles/km),因此整小时路线(2 hr,不含 min)也能触发。地图比例尺中的 50 km 位于面板之外。 |
| 3 | 选择出行 --mode(除非为 best)。出行方式标签页是包含 [aria-label] 图标的 button[role=radio],顺序固定为 [Best, Driving, Transit, Walking, Cycling, Flights](地图类型的 Default/Satellite 单选按钮没有图标 aria-label,因此会被排除)。标签页的 aria-label 会本地化(“Driving” → “Lái xe”),因此按索引而不是标签选择标签页;会等待标签页出现(它们可能晚于初始绘制),并检测不可用的标签页(DOM disabled 属性;备用判断方式:点击后没有路线 ⇒“不可用”)。实际点击后,面板会清空(耗时 → null),然后重新填充新模式的耗时,因此通过轮询 null → 非 null 等待重渲染,避免与先前模式的耗时发生竞争。 |
| 4 | 使用一次 Runtime.evaluate 提取主要路线:第一个不在出行方式 BUTTON[role=radio] 内的完整格式耗时叶节点(其中存放各出行方式的简写最佳耗时);最近祖先节点中的距离叶节点;以及唯一的 via … / Fastest route … / This route has tolls. 叶节点(公交的备用路线,如 RedCoach/FlixBus,不包含这些内容,因此不会造成干扰)。解析出的途经点名称来自 input[aria-label](Starting point … / Destination …)。对于 flights,耗时从整个页面正文中读取(航班卡片位于 Directions 面板之外),距离/via/收费信息均为 null。 |
| 5 | 在 finally 中以即发即弃方式调用 closeTab。 |
优化额外选项
| 步骤 | 发生的情况 |
|---|---|
| A | 在并行后台标签页中解析每个地点:地点名称搜索不会渲染信息流,而是解析为一个地点页面,其 URL 会更新(约 3-7 s 后)为 /maps/place/<name>/@<view>/data=…!8m2!3d<lat>!4d<lng>…。每个标签页都会轮询 location.href,查找 !3d!4d 标记(地点的精确坐标——@lat,lng 仅表示视口)。地点名称取自 /maps/place/<name>/ 路径段。 |
| B | 构建 haversine 距离矩阵,并求解固定起点 = place[0] 的开放路径 TSP(精确 Held-Karp,最多 12 个地点)。 |
| C | 计算每段的直线距离(km)和罗盘方位角。 |
| D | 按选定顺序进行一次真实的 --route 调用(使用选定的 --mode,默认为 driving)→ 报告的时间/距离是真实数据;只有顺序是直线距离估算。 |
每次调用:搜索约 2-3 s(渲染信息流并滚动);路线约 2-3 s;优化约 5-9 s(并行解析 N 个地点 + 渲染 1 次路线)。后台标签页不会打扰使用(与 ytdl 不同,Maps 不需要播放或 poToken)。
为什么 Maps 需要真实浏览器
- Google Places 和 Directions API 按量计费且受到限制——需要密钥、计费账户,并且每次调用都会产生成本。Maps 网页会免费渲染相同的数据;使用自己的浏览器读取数据不需要密钥,也没有配额限制。付费 Directions API 唯一能提供、而免费网页确实无法提供的功能是
optimizeWaypoints(针对 ≤9 个停靠点的 TSP)——--optimize会用尽力而为的直线距离 TSP 取代它。 - 真实浏览器会渲染信息流和路线面板。 Maps 是一个庞大的 SPA:结果会在滚动时流式载入
div[role=feed],而路线面板会以经过混淆且不断轮换的类名渲染路线时长。单次Runtime.evaluate即可通过一次 CDP 调用提取所有卡片/主要路线——无需获取 HTML、代理或应对 Cloudflare 拦截(页面由你自己的浏览器以你的身份渲染)。 - 按查询使用,而非批量抓取。 这就是 Agent 调用的形式:一次查询(或一条最多包含 25 个地点的路线,或一个最多包含 12 个地点的 TSP),返回结构化 JSON 或文本。它不是批量抓取工具(批量抓取需要轮换代理并管理速率;而每次调用都使用真实浏览器的方式无法扩展到这种规模)。
陷阱
搜索
- Maps 不会触发
networkIdle。 持续的 XHR 轮询会使网络一直处于繁忙状态,因此 gsearch/findata 使用的生命周期事件等待每次都会超时。gmaps改为轮询a[href*='/maps/place/']的数量——这是实际的就绪信号。 - 不使用类名选择器。 Google 的类名经过混淆(
.Nv2PK、.bfdHYd、.MW4etd……),并且会随版本更新而变化。解析器只依赖稳定信号:结果链接的aria-label(名称)、卡片叶子文本的 DOM 顺序(评分/评论/价格/类别/地址/营业时间),以及href(坐标对应!3d<lat>!4d<lng>,地点 ID 对应!19sChIJ…/!1s0x..:0x..)。如果 Maps 改变卡片文本顺序,地址/营业时间启发式规则是首先需要重新检查的部分。 ?query=URL 格式不会渲染结果列表(只会返回没有结果的重定向外壳),只有路径格式/maps/search/<query>会渲染。空格会转换为+。- 地址是通过启发式规则推断的。 取第一个以街道号码开头的叶子(
^\d{1,5}[-A-Za-z]*\s)。没有街道地址的地点(区域/地区结果或具名建筑)会返回address: null,而不是猜测一个地址。 - 营业时间是通过启发式规则推断的。 取
Open/Closed/Open 24 hours/Temporarily closed叶子,以及存在时紧随其后的· Closes/Opens …叶子。如果Open 24 hours被渲染为两个叶子(Open+24 hours),则只会读取为Open。 - 结果卡片中没有电话、网站和完整的每周营业时间——这些信息位于地点详情页,需要逐个导航到地点页面才能读取。目前尚未实现(可作为
--details模式);目前可使用url打开地点。 - 可能会出现赞助结果(Maps 有时会显示一个广告,它也会使用
/maps/place/链接)。这些结果不会被过滤——如果这很重要,请检查数据。 - 同意提示/ Cookie 墙(在已预热的浏览器中很少见)会导致结果数量为零。如果返回空结果,请先在浏览器中打开一次搜索 URL 以清除提示。
路线
- 折叠后的路线摘要中没有每段路程的耗时。 对于有多个有序停靠点的路线,Maps 只显示总耗时。每段耗时位于“详情”展开区域中(尚未实现);请使用
url查看逐向导航。 - 对于极短路线,
distance可能为 null(Maps 可能不会渲染距离叶子)。 - 主要路线是“不在出行方式单选按钮中的第一个完整格式耗时”。 出行方式标签页(
BUTTON[role=radio])会显示紧凑的最佳时间(2h 30m),必须排除;路线列表使用完整格式(2 hr 31 min)。公交替代路线(RedCoach/FlixBus)会出现在同一面板中,但没有via/Fastest route/ 距离叶子,因此不会干扰主要路线的提取。 - 时间反映当前路况(“现在”)。 页面提供的是现在,而不是预测的未来出发时间(付费 API 的
departure_time/traffic_model是页面无法提供的内容)。标签文本可能不同:“Fastest route, the usual traffic” 或 “Fastest route now due to traffic conditions”——两者都会匹配^Fastest route。 - 地点名称搜索不会渲染结果列表——这就是为什么
--optimize通过轮询地点页面 URL 中的!3d!4d来解析地点,而不是解析结果卡片。后台标签页更新 URL 需要约 3-7 s;resolvePlace最多轮询 20 s。
出行方式(--mode)
best是 Maps 的默认选项,但不一定是驾车。 对于公共交通或航班更快的路线(例如伦敦→巴黎,Best = 欧洲之星),best会返回公共交通所需的时间。传入--mode driving可强制选择驾车。出于这个原因,此技能的默认选项是driving(而不是best)。- 方式标签页通过索引选择,而非 aria-label——标签会本地化(在 vi 中,"Driving" → "Lái xe")。顺序
[Best, Driving, Transit, Walking, Cycling, Flights]在各语言区域中一直保持稳定,但如果 Maps 调整顺序或移除标签页,索引映射就会失效。 - 通过两种方式检测不可用的出行方式——标签页 DOM 的
disabled属性(其状态可能落后于渲染),以及备用检测:如果点击后没有路线(面板被清空但未重新填充),则报告该出行方式“此路线不可用”。 flights尽力而为。 航班卡片渲染在路线规划面板之外,因此会在整个页面正文中读取直飞航班时间;没有距离、途经点或过路费信息。不会解析中转航班和预订详情。- 没有
motorcycle或ferry出行方式。 Maps 仅提供以上六个标签页;摩托车路线使用driving,渡轮则显示为driving/transit路线中的路段。(在越南或美国,Maps 不会显示两轮车标签页。) - 步行/骑行时长在路线列表中以小时显示,例如
62 hr,而不是方式标签页中显示的“天数”——两种格式都会匹配。
优化
- 路线顺序是直线距离估算,而非真正的驾车最优顺序。 直线距离(haversine)是“虚拟化”的边成本;道路网络的绕行可能会改变真正的最优顺序。报告的驾车时间/距离是真实值(针对选定顺序进行一次路线规划调用)——只有顺序是尽力而为。
- 起点固定,路径开放。 第一个地点是固定起点;路径会经过每个地点一次,并在任意地点结束(不返回起点)。这不是往返行程。
- 地点数 ≤12 时保证精确。 Held-Karp 算法可得出精确结果,但复杂度呈指数增长;超过 12 个地点时,命令会报错,而不是猜测。N 个地点的解析会并行运行(N 个后台标签页),再加 1 次路线规划调用,共计 N+1 次浏览器调用。
常规说明
- 不依赖
jq——解析、TSP 和美化输出均在页面内 / 使用 JS 完成。 - 输入通过占位符替换传入 heredoc(node
JSON.stringify+ 函数替换),不会受地点名称中的&/$/\影响(例如AT&T Stadium、Café du Monde)——搜索查询也使用相同机制。