🎯 一款可定制、具备反检测功能的云浏览器,由自主研发的 Chromium驱动,专为网页爬虫AI 代理设计。👉立即试用
返回博客

如何使用Scrapeless Browserless构建Google Maps爬虫:2026年的终极AI准备指南

Ethan Brown
Ethan Brown

Advanced Bot Mitigation Engineer

15-Apr-2026

关键要点:

  • Google Maps 是一个无与伦比的数据源,用于 B2B 潜在客户生成、本地 SEO 和市场情报,但其防御措施非常复杂。
  • Scrapeless Scraping Browser 作为一款强大的 AI 浏览器基础设施,提供自动化的 CAPTCHA 解决方案、反检测指纹识别和住宅代理,以绕过 Google 强大的反爬虫措施。
  • 地理网格平铺 是克服固有的 120 个位置搜索限制的基本技术,能够从任一特定区域提取成千上万个独特的商业列表。
  • Scrapeless Scraping Browser 平台提供 AI 代理准备的结构化 JSON/HTML/Markdown 输出,无缝集成到先进的 AI 工作流和数据管道中。
  • 精通 Google Maps 动态渲染的细微差别并实现强大的重试逻辑,对于构建一个可扩展的爬虫浏览器解决方案,在生产规模下可靠运行至关重要。

引言:在 AI 时代解锁 Google Maps 金矿

根据对网络爬虫的广泛实践经验,我们发现每当你从零开始构建一个爬虫时,总会有一种一致的模式。第一个小时感觉很好。第二个小时,你遇到了第一个瓶颈。到一天结束时,你意识到只为了可靠地提取一个标题,你已经写了三百行代码。

Google Maps 是网络上最丰富的公共商业数据集。每个列表都有一个名称、电话、网站、地址、分类、评分、评论数量、营业时间、GPS、照片、服务属性和实时评论流。而 Google 不希望你大规模拥有这些数据。Places API 按请求收费,限制很严,并且省略了一些常规网页界面免费显示的字段。因此,真正需要这些数据的人最终会选择爬取。

本指南将通过一个单文件的 TypeScript 爬虫,基于 Scrapeless Scraping Browser,处理那些通常需要数周的部分:反检测、住宅代理、CAPTCHA、120 个位置限额,以及当你直接打开某个地点URL时,Google Maps 提供的简化面板。一个文件,一次运行,每个字段都在其中。


你可以用它做什么

Google Maps 数据是一种多用途资产,驱动从潜在客户生成到高级 AI 分析的高影响商业应用。

从 Google Maps 数据中获得的价值远不止简单的联系方式列表。其结构化的特点和实时更新使其成为多种商业应用不可或缺的资源。以下是五个实际的商业用途,均可通过相同的代码库实现,通常仅需更改配置:

  1. B2B 潜在客户生成。 运行爬虫针对凤凰城的“水管工”或芝加哥的“牙医”,设置 maxPlaces: 500 和 3×3 网格,你将获得一份包含该地区每个商业名称、电话、网站、地址、分类和评级的 CSV 文件。将其导入你的 CRM 或外发电子邮件工具。这是人们爬取 Google Maps 的最常见原因,也是最容易变现的。
  2. 评论和声誉监控。 每天安排爬虫运行你的企业以及三到五个竞争对手,存储带时间戳的 JSON,并在快照之间通过 publishedAtDatereviews[] 进行差异比较。竞争对手有新的 1 星评论?发送 Slack 警报。你那边有新的 5 星评论?推送到你的营销网站。输出中包含完整的评论文本——作者、星级、日期、业主回复,所有信息都在其中。
  3. 房地产和位置智能。 以 500 米单元格半径对一个社区进行平铺,提取每家咖啡店、健身房和杂货店,然后在地图上绘制密度图。房地产投资者使用此方法比较候选地址之间的便利设施覆盖情况;零售连锁品牌用于选择地点。每个地点的 location.{lat,lng} 字段使得这成为一行的分组处理。
  4. 本地 SEO 排名跟踪。 爬虫记录每个位置每个单元格的排名。以 1 公里单元格对城市进行平铺,从每个单元格提取“水管工”,查看你的客户业务在网格上的排名。按单元格排名的地图就是 Whitespark 和 BrightLocal 作为产品销售的本地 SEO 热图。将其在五十行代码之内自己实现,基于 JSON 输出。
  5. 机器学习和分析数据集。 在四十个查询 × 二十个城市中运行爬虫,你将获得一个包含数以万计的具有结构化属性、营业时间和评论文本的商业数据集。将 reviews[].text 输入情感分类器,针对 categories × additionalInfo 训练推荐器,或仅将其用作基准语料库。

为什么选择 Scrapeless

在你自己的笔记本电脑上运行无头浏览器大约能持续十分钟。之后,Google 会识别 TLS 指纹、WebGL 输出、时间模式,你开始看到同意页面、验证码和“异常流量”警告。你可以用隐蔽插件暂时应对,但这是一场注定要失败的游戏。

Scrapeless Scraping Browser 是一款在 Google 看起来像真实浏览器的云浏览器。它配备以下功能:

  • 反检测指纹,在真实负载下表现稳定
  • 在195多个国家的住宅代理
  • 自动验证码解决
  • 会话录制以进行调试
  • 支持CDP基础框架的WebSocket端点,如 Puppeteer 和 Playwright,因此无需学习SDK

你可以与通常连接的CDP基础框架连接,只需针对不同的URL。其他所有内容都是标准的Puppeteer。

app.scrapeless.com 获取免费计划的API密钥。


先决条件

  • Node 18或更新版本。
  • 一个Scrapeless密钥(免费层即可)。
  • 一些Puppeteer的熟悉程度会有帮助,但不是必需的。
  • **没有本地Chrome。**浏览器在云中运行。

安装

bash Copy
mkdir google-maps-pro && cd google-maps-pro
npm init -y
npm install puppeteer-core dotenv
npm install -D tsx typescript @types/node

以及 .env 文件:

env Copy
SCRAPELESS_API_KEY=your_key_here

步骤 1 — 连接

每个会话的打开方式都是一样的。使用你的令牌、国家和TTL构建WSS URL,然后交给 puppeteer.connect

typescript Copy
import "dotenv/config";
import puppeteer from "puppeteer-core";

function connectionURL(sessionName: string, proxyCountry = "US", sessionTTL = 600) {
  const qs = new URLSearchParams({
    token: process.env.SCRAPELESS_API_KEY!,
    proxyCountry,
    sessionTTL: String(sessionTTL),
    sessionName,
  });
  return `wss://browser.scrapeless.com/api/v2/browser?${qs.toString()}`;
}

async function openBrowser(sessionName: string) {
  return puppeteer.connect({
    browserWSEndpoint: connectionURL(sessionName),
    defaultViewport: null,
  });
}

这就是整个Scrapeless特定的表面区域。之后的一切都是标准Puppeteer。存储库中的模板将这些选项作为单个cfg对象传递,而不是位置参数——效果相同,只是在你有十多个控制项时更简洁。


步骤 2 — 120个地点的上限

在浏览器中打开Google地图并搜索“纽约的餐厅”。随意滚动。你永远不会看到超过大约120个结果。用户界面会停止给你更多。

这是抓取Google地图时最令人沮丧的事情,很多第一次的项目就默默地接受了这一点。你无法通过它进行滚动。你无法翻页超出这个限制。这个上限是嵌入在用户界面中的。

解决方法是地理网格切割。将目标区域划分为更小的格子,每个格子进行一次搜索,并使用不同的中心坐标,合并结果,然后根据placeId进行去重。在奥斯丁市中心的2×2网格使一次搜索变成四次,并获得大约400个独特地点,而不是120个。3×3的网格轻松突破一千。

网格数学非常简单:

typescript Copy
function kmPerDegLng(lat: number) { return 111.32 * Math.cos((lat * Math.PI) / 180); }
function kmPerDegLat() { return 111.32; }

function buildGrid(centerLat: number, centerLng: number, cellRadiusKm: number, maxCells: number) {
  const side = Math.max(1, Math.floor(Math.sqrt(maxCells)));
  const cells: { lat: number; lng: number }[] = [];
  const stepLat = (cellRadiusKm * 2) / kmPerDegLat();
  const stepLng = (cellRadiusKm * 2) / kmPerDegLng(centerLat);
  const offset = (side - 1) / 2;
  for (let i = 0; i < side; i++) {
    for (let j = 0; j < side; j++) {
      cells.push({
        lat: centerLat + (i - offset) * stepLat,
        lng: centerLng + (j - offset) * stepLng,
      });
      if (cells.length >= maxCells) return cells;
    }
  }
  return cells;
}

function searchUrlForCell(query: string, lat: number, lng: number) {
  const q = encodeURIComponent(query).replace(/%20/g, "+");
  return `https://www.google.com/maps/search/${q}/@${lat.toFixed(6)},${lng.toFixed(6)},15z`;
}

URL中的@lat,lng,15z片段是为每次搜索重新定位地图。15z是缩放级别。提高它以缩小网格,降低它以扩大网格。


步骤 3 — 抓取结果馈送

对于每个单元URL,滚动结果侧边栏,直到它停止加载新卡片,然后抓取每个a.hfpxzc链接。

typescript Copy
async function collectSearchResults(page: Page, searchUrl: string, target: number) {
  await page.goto(searchUrl, { waitUntil: "domcontentloaded", timeout: 60000 });
  await page.waitForSelector('div[role="feed"], h1.DUwDvf', { timeout: 25000 });

  let last = 0, stable = 0;
  for (let i = 0; i < 40; i++) {
    const n = await page.$$eval(".Nv2PK", (els) => els.length);
    if (n >= target) break;

如果 (n === last) { stable++; if (stable >= 3) break; } else stable = 0;
last = n;
await page.evaluate(() => {
const feed = document.querySelector('div[role="feed"]');
if (feed) (feed as HTMLElement).scrollTop = (feed as HTMLElement).scrollHeight;
});
await new Promise(r => setTimeout(r, 1500));
}

return page.$$eval(".Nv2PK a.hfpxzc", (links) =>
links.map((el, idx) => ({
name: el.getAttribute("aria-label") || "",
url: (el as HTMLAnchorElement).href,
rank: idx + 1,
})),
);
}

Copy
**稳定计数器模式**(三次滚动没有新结果意味着信息流结束)每次都优于固定休眠。如果信息流真的停止加载,等待更长时间没有帮助。

`placeId` 嵌入在每个 URL 中,位于 `!1s0x...:0x....` 后面。这是去重的键。

---

## 第四步 — 奇怪的谷歌地图渲染问题

这一点让我花了大部分时间来理清。

如果你直接导航到一个地方的 URL,比如 `https://www.google.com/maps/place/Haraz+Coffee+House/...`,谷歌地图会渲染出一个简化版的信息面板。`h1` 标题在。这有评分。 但是评论数、开放时间表、服务属性、照片 — 一半的内容缺失或为空。整页的 DOM 大约有三千个字符。

如果你改为导航到搜索结果,并点击同一个地方的卡片,你会得到完整的丰富面板。相同的 URL,完全不同的渲染。

解决方法是:**总是通过搜索 URL 进入,点击你想要的地方卡片,并等待面板。**

```typescript
const clicked = await page.evaluate((placeName) => {
  const cards = document.querySelectorAll(".Nv2PK a.hfpxzc");
  for (const c of Array.from(cards)) {
    if (c.getAttribute("aria-label") === placeName) {
      (c as HTMLElement).click();
      return true;
    }
  }
  return false;
}, hit.name);

await page.waitForSelector("h1.DUwDvf", { timeout: 15000 });

在选择器出现后进行快速的合理性检查:在继续之前检查 h1 是否有非空文本内容。有时该元素显示出来时,谷歌还没有实际填充内容。


第五步 — 提取概述

一旦你在真实的丰富面板上,一个 page.evaluate 就可以获取大多数基本字段。

typescript Copy
const overview = await page.evaluate(() => {
  const $ = (s: string) => document.querySelector(s) as HTMLElement | null;
  const txt = (s: string) => $(s)?.textContent?.trim() || null;

  return {
    title: txt("h1.DUwDvf"),
    totalScore: parseFloat(txt('div.F7nice span[aria-hidden="true"]') || "") || null,
    categoryName: txt("button.DkEaL"),
    address: txt('button[data-item-id="address"] .Io6YTe'),
    phone: txt('button[data-item-id*="phone"] .Io6YTe'),
    website: txt('a[data-item-id="authority"] .Io6YTe'),
    plusCode: txt('button[data-item-id="oloc"] .Io6YTe'),
  };
});

纬度、经度以及 placeId 哈希都在 URL 中,而不在 DOM 中:

typescript Copy
const at = page.url().match(/@(-?\d+\.\d+),(-?\d+\.\d+)/);
const location = at ? { lat: parseFloat(at[1]), lng: parseFloat(at[2]) } : null;
const placeId = page.url().match(/!1s(0x[0-9a-f]+:0x[0-9a-f]+)/i)?.[1] ?? null;

关于类名的提醒。DUwDvfF7niceIo6YTe,这些都是谷歌构建自动生成的,并且会发生变化。每周或每两周检查一次你的输出与已知地点的对照,并做好在某些内容变为 null 时更新选择器的准备。


第六步 — 评论、照片、菜单、问答、关于

地点面板有标签。你点击每一个,等待,滚动,提取。顺序在这里很重要,这在你第一次遇到问题之前并不明显。

首先做关于/属性。 关于内容在概述面板内部。如果你在提取属性之前点击评论或照片,这些节点会被移除,你将失去它们。

评论。 点击标签,如果在意选择排序顺序(最新、最高、最低或相关),然后滚动面板直到加载出你想要的卡片数量。每个卡片都有作者姓名、文本、星级、相对日期、评论者照片,以及有时业主的回复。

typescript Copy
const reviews = await page.evaluate((max) => {
  const cards = Array.from(document.querySelectorAll(".Nv2PK"));
  return cards.slice(0, max).map((c) => ({
    name: c.querySelector(".d4r55")?.textContent?.trim() || null,
    text: c.querySelector(".wiI7pd")?.textContent?.trim() || null,
    publishedAtDate: c.querySelector(".rsqaWe")?.textContent?.trim() || null,
    stars: (() => {
      const m = (c.querySelector(".kvMYJc")?.getAttribute("aria-label") || "").match(/(\d)/);
      return m ? parseInt(m[1], 10) : null;
    })(),
    responseFromOwnerText: c.querySelector(".CDe7pd .wiI7pd")?.textContent?.trim() || null,
  }));
}, 30);

一个陷阱:较长的评论内容会被截断并显示“更多”按钮。在提取之前点击所有这些,否则你会得到不完整的评论。
照片: 滚动图库,然后提取每个CSS背景图像的URL。URL以=w150-h150结尾以获取缩略图;将其更改为=w1600-h1600以获取全尺寸。相同的URL,不同的尺寸后缀。
菜单,问答: 每个都是带有其自己选择器包的选项卡点击。并非所有地方都有这些。例如,咖啡店通常在Google地图上不会显示菜单。


第7步 — 实现可扩展性

这是将一个临时有效的脚本与可以连续运行的脚本区分开的部分。

Google地图有时不稳定。在选项卡点击后,单页应用状态变得奇怪。有时点击有效,但选项卡从未填充。提要偶尔会以零结果加载,原因不明,而重新加载会解决问题。如果你手动处理一两个地方,这些都是小烦恼,但遇到大量时,这些问题会很快加剧。

处理此问题的两件事:

  1. 每个地方使用新浏览器。 收集完搜索结果后,为每个地方打开一个新的抓取浏览器会话。在地方之间重用会话听起来像是一个显而易见的优化,但地方N的面板状态会以难以调试的方式影响地方N+1。每次都使用新会话,这个问题就消失了。
  2. 最多重试三次。 几乎所有失败都是暂时的。
typescript Copy
for (let attempt = 1; attempt <= 3; attempt++) {
  const eb = await openBrowser(`gmp-enrich-${attempt}`);
  const ep = await eb.newPage();
  try {
    await ep.goto(searchUrl, { waitUntil: "domcontentloaded", timeout: 60000 });
    await ep.waitForSelector('div[role="feed"]', { timeout: 20000 });
    const place = await enrichPlaceOnSearchPage(ep, hit, cfg);
    if (place.title) return place;
  } finally {
    await ep.close();
    await eb.close();
  }
  await new Promise(r => setTimeout(r, 2000));
}

在我构建时进行的测试运行中——奥斯汀的咖啡店,曼哈顿的餐馆,芝加哥的牙医——每个地方的成功率在75%到100%之间。失败几乎总是因为Google在卡片点击后没有渲染面板,而重试循环能够捕捉到大多数情况。


您将获得的结果

每个地方返回一个扁平的JSON对象。它故意设计得很宽,以便您不必第二次运行抓取器来获取不同的信息——潜在客户生成、评论、属性,所有信息均在一个有效负载中。

以下是近期对奥斯汀的Haraz Coffee House的真实结果:

json Copy
{
  "title": "Haraz Coffee House",
  "placeId": "0x8644b52f8462f95f:0xa572bbcb1887b9bb",
  "cid": "11921797644567427515",
  "url": "https://www.google.com/maps/place/Haraz+Coffee+House/...",
  "rank": 2,
  "address": "500 W Martin Luther King Jr Blvd Suite A, Austin, TX 78701",
  "plusCode": "77J4+WJ Austin, Texas",
  "location": { "lat": 30.2823385, "lng": -97.7434096 },
  "phone": "(512) 243-5667",
  "phoneUnformatted": "5122435667",
  "website": "harazcoffeehouse.com",
  "domain": "harazcoffeehouse.com",
  "categoryName": "咖啡店",
  "categories": ["咖啡店"],
  "totalScore": 4.7,
  "reviewsCount": 47239,
  "openingHours": [
    { "day": "星期一", "hours": "早上7点到晚上10点" },
    { "day": "星期二", "hours": "早上7点到晚上10点" },
    "... 还有5天"
  ],
  "additionalInfo": {
    "服务选项": [
      { "name": "堂食", "value": true },
      { "name": "外卖", "value": true }
    ],
    "无障碍设施": [
      { "name": "轮椅可进入的入口", "value": true }
    ],
    "亮点": [{ "name": "绝佳的咖啡", "value": true }],
    "... 还有10个部分": {}
  },
  "images": [
    "https://lh3.googleusercontent.com/.../=w1600-h1600",
    "... 还有4个URLs"
  ],
  "reviews": [
    {
      "name": "Maria T",
      "text": "奥斯汀最好的拿铁,毋庸置疑...",
      "publishedAtDate": "14小时前",
      "stars": 5,
      "reviewerPhotoUrl": "https://lh3.googleusercontent.com/...",
      "responseFromOwnerText": "谢谢,Maria!"
    },
    "... 还有9条评论"
  ],
  "menu": [],
  "questionsAndAnswers": [],
  "popularTimesLiveText": null,
  "scrapedAt": "2026-04-13T13:23:18.450Z"
}

您将获得的内容 — 每次运行返回带有以下字段的结构化JSON:title、placeId、cid、address、plusCode、location.{lat,lng}、phone、phoneUnformatted、website、domain、categoryName、categories、price、totalScore、reviewsCount、openingHours[]、additionalInfo{}、images[]、reviews[]、questionsAndAnswers[]、menu[]、popularTimesLiveText、plus状态标志。对于Google不为该地方类型显示的字段,返回空数组/空值。


结论:用真实世界数据为您的AI代理提供动力

掌握使用Scrapeless Scraping Browser进行Google地图抓取是构建能够真正理解和与物理世界互动的自主AI代理的明确路径。
从Google Maps提取有价值数据的旅程充满了技术挑战,从120个地点的限制到复杂的反机器人防御。然而,通过利用Scrapeless Scraping Browser——您的专用AI浏览器基础设施的先进功能,您可以将这些障碍转化为机遇。本指南为您提供了一种强大的、AI就绪的解决方案,该方案结合了地理网格切分、智能数据提取和生产级最佳实践。

无论您的目标是生成高质量的B2B潜在客户、监控本地SEO表现、进行深入市场研究,还是为您的AI代理提供实时、结构化的关于物理世界的信息,Scrapeless都提供了您所需的可靠基础。停止与反机器人措施作斗争,开始专注于真正重要的事情:从您的数据中获取的见解。

准备好建立您的AI驱动的数据管道了吗?

加入我们充满活力的社区,领取一个免费计划,并与其他创新者连接:


常见问题

问:我需要代理才能从Google Maps抓取数据吗?
答: 是的。没有代理,您将在几次请求后开始遇到速率限制,接下来就是429错误和同意墙。Scrapeless Scraping Browser内置了居民代理,因此无需额外集成。您打开的每个会话都会通过您选择的国家中的不同居民IP进行。

问:我实际可以提取多少个地点?
答: 这取决于城市。一个2×2的2公里单元的网格在一个中型美国市中心可以为您提供200-400个独特地点(去重后)。在密集的地铁区域扩展到3×3或4×4,您可以轻松进入数千个地点。

问:当Google更改类名时会发生什么?
答: 每隔几个月就会发生一次这种情况。抓取程序对每个字段使用多个后备选择器,因此通常只有一个字段会在一次中破坏,而其余字段则正常工作。监视您的日志,当某个字段变为null时,更新选择器。

问:为什么使用Scrapeless Scraping Browser而不是本地无头浏览器?
答: Scrapeless Scraping Browser提供企业级的反检测、自动化的验证码解决和集成的居民代理——这些功能在本地无头浏览器设置中实现和维护起来极其困难且成本高昂。它提供一个真正的AI浏览器基础设施,让您可以专注于数据提取逻辑,而不是与反机器人措施作斗争。

问:reviewsCount怎么办?
答: 抓取程序从概览标签上的F7nice块中读取reviewsCount——即位于评分(“4.7 (1,234)”)后面的数字。当Google渲染计数时,它是准确的。对于抓取程序成功提取到评价卡的任何地点,reviews.length也是一个可靠的计数。

在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。

最受欢迎的文章

目录