Google Maps 是开放网络上最丰富的本地商业信息来源之一。在某座城市搜索一个类别,你会得到一份按排名列出的地点清单,每条包含名称、地址、电话、类别、星级评分和评论数。对于本地潜在客户开发、市场调研和竞品分析而言,这些公开列表数据极具价值:它告诉你某个区域内有哪些经营者、评价如何,以及如何联系他们。
本指南将展示如何用 Python 以可靠的方式抓取 Google Maps 数据。你将构建一个小型、可运行的爬虫,通过 Crawling API 获取渲染后的 Google Maps 结果页,用 BeautifulSoup 解析每个地点卡片,采集核心商业字段,处理多条结果,并将数据导出为 JSON 和 CSV 格式。整个演示仅限于公开商业列表,任何人无需账号即可在地图上看到这些信息。接近结尾的合法性章节并非样板文字,请在大规模使用之前认真阅读。
你将构建什么
一个 Python 脚本,接受一个 Google Maps 公开搜索 URL,通过 Crawling API 检索渲染后的 HTML,并为结果面板中的每个地点提取结构化记录。本文以"纽约餐厅"作为贯穿全文的示例(与本教程原版使用的目标相同),并从每条列表中提取以下字段:
- Name 地点卡片上显示的商家名称。
- Address 该地点的街道地址或区域标签。
- Phone 联系电话(当列表提供时)。
- Category 商业类型标签,例如"意大利餐厅"。
- Rating 平均星级评分(数字形式)。
- Review count 该评分所基于的评论数量。
最终你将获得页面上每个地点的这些记录,写入 maps_results.json 和 maps_results.csv,可直接用于 CRM、电子表格或数据库。
普通请求为何在 Google Maps 上失效
如果你用脚本向 Google Maps URL 发起普通 HTTP 请求,得到的不会是浏览器里看到的整洁地点列表。结果面板由 JavaScript 在初始页面加载后构建:简单的 requests.get 返回的原始 HTML 基本上是一个空壳,实际的地点卡片由脚本在真实浏览器中运行后填充。解析这个壳,你找不到任何所需字段。
此外,Google 对自动化流量的监测非常严格。不像真实浏览器的请求,或从数据中心 IP 批量发出的请求,在到达任何列表之前就会收到 CAPTCHA 挑战、同意页面或直接封锁。本教程原版依赖内部 JSON 端点来规避渲染,但这些私有 URL 会在 Google 重新整理时不经通知地更改并失效。更持久的方法是像浏览器一样渲染公开页面。
因此,一个可用的 Google Maps 爬虫在单次请求中需要两样东西:一个平台认为是真实访客的 IP,以及一个能运行页面 JavaScript 使地点卡片真正出现的浏览器。你可以自己用无头浏览器加住宅代理池来实现,但维护这套方案本身就是大部分工作量所在。Crawling API 将两者合并为一次调用:你发送 URL,它从可信住宅 IP 获取并渲染页面,返回供你解析的完整 HTML。
前提条件
在编写任何代码前,你需要准备好以下几项,都不费时。
基础 Python 知识。 你应当能够编写和运行 Python 脚本,并使用 pip 安装包。如果你对 BeautifulSoup 还不熟悉,我们的 BeautifulSoup 使用指南涵盖了本教程所假设的解析基础知识。
Python 3.8 或更高版本。 通过 python --version 确认你的版本。如果尚未安装,请从 python.org 或 Anaconda 等发行版安装。
Crawlbase 账号与 token。 注册账号,打开控制台,从账号文档页复制你的请求 token。前 1,000 次请求免费,无需信用卡。Google Maps 需要 JavaScript 渲染,因此这些请求请使用你的 JavaScript token。请像保护密码一样保管 token:它用于验证你的请求,切勿提交到版本控制。
搭建项目
创建虚拟环境以隔离项目依赖,然后安装爬虫所需的两个库。
python --version python -m venv maps_env source maps_env/bin/activate pip install requests beautifulsoup4
在 Windows 上,用 maps_env\Scripts\activate 代替 source 那行来激活环境。两个依赖各司其职:requests 发送对 Crawling API 的 HTTP 调用,beautifulsoup4 解析返回的 HTML,让你能通过 CSS 选择器提取各个字段。
步骤 1:获取搜索 URL
在浏览器中打开 Google Maps,搜索"restaurants in New York"。地点出现在左侧可滚动的面板中,地址栏的 URL 也会更新以编码查询。复制该 URL。它就是你将传给爬虫的地址,因此浏览器中看到的内容与 API 渲染的内容一致。
https://www.google.com/maps/search/restaurants+in+New+York
/maps/search/ 路径是类别搜索的稳定公开入口,查询词以 + 号连接。你可以将类别和地点替换为任意内容,例如 plumbers+in+Chicago 或 coffee+shops+in+Austin,其余爬虫代码保持不变。避免使用浏览器有时生成的含有 pb= 参数的长内部 URL;这些是私有格式,会无预警地失效。
步骤 2:通过 Crawling API 获取渲染后的页面
从获取 HTML 开始。编写一个小的 crawl() 函数,将目标 URL 和你的 JavaScript token 发送给 Crawling API,要求其渲染页面,检查底层页面是否返回 200 状态,并返回 HTML 正文。在解析前检查状态可以让失败信号响亮而不是静默。
import json import requests API_TOKEN = "YOUR_CRAWLBASE_TOKEN" # use your JavaScript token API_ENDPOINT = "https://api.crawlbase.com/" def crawl(url): params = { "token": API_TOKEN, "url": url, "page_wait": 4000, } response = requests.get(API_ENDPOINT, params=params) response.raise_for_status() data = json.loads(response.text) if data["original_status"] != 200: raise Exception(f"Unable to crawl '{url}'") return data["body"] if __name__ == "__main__": url = "https://www.google.com/maps/search/restaurants+in+New+York" html = crawl(url) print(html[:500])
API 返回一个 JSON 封装,因此你用 json.loads 加载响应并读取两个字段:original_status 是 Google 自身返回的状态码,body 是渲染后的页面 HTML。page_wait 参数告知渲染器在加载后等待几秒,让地点卡片有时间填充后再捕获 HTML。对 original_status 的检查意味着同意页面或封锁会以异常形式出现,而不是把空壳喂给解析器。用 python crawling.py 运行脚本,前 500 个字符中应当能看到真实的地图标记,这在编写任何选择器之前就确认了抓取和渲染是可用的。
original_status 检查只能读到 200,因为请求以真实访客身份到达 Google Maps,页面首先在真实浏览器中完成渲染。Crawling API 从轮换的住宅 IP 发起请求,运行页面 JavaScript 使地点卡片真正出现,并返回完整的 HTML,让你无需自己运行无头浏览器集群和住宅代理池。先在免费层将其指向一个 Maps 公开搜索 URL 试试。
步骤 3:用 BeautifulSoup 解析每个地点卡片
拿到渲染后的 HTML 后,将其载入 BeautifulSoup 并按选择器提取每条结果。Google Maps 将结果面板中的每个地点包裹在一个 article 元素中,每个字段都位于卡片内部可预测的位置。在浏览器开发者工具中右键点击一个地点卡片并选择"检查",以确认当前的类名和属性名;以下选择器与撰写时的布局相符。
import re from bs4 import BeautifulSoup def parse_place(card): name_el = card.select_one("div.qBF1Pd") rating_el = card.select_one("span.MW4etd") reviews_el = card.select_one("span.UY7F9") info_rows = card.select("div.W4Efsd > div.W4Efsd") category, address, phone = None, None, None for row in info_rows: text = row.get_text(" ", strip=True) phone_match = re.search(r"(\(?\d[\d\-\s\(\)]{7,}\d)", text) if phone_match and not phone: phone = phone_match.group(1).strip() elif "·" in text and not category: parts = [p.strip() for p in text.split("·")] category = parts[0] address = parts[-1] if len(parts) > 1 else None return { "name": name_el.get_text(strip=True) if name_el else None, "category": category, "address": address, "phone": phone, "rating": float(rating_el.get_text(strip=True)) if rating_el else None, "reviews": parse_reviews(reviews_el), } def parse_reviews(el): if not el: return 0 digits = re.sub(r"[^\d]", "", el.get_text(strip=True)) return int(digits) if digits else 0 def scrape_html(html): soup = BeautifulSoup(html, "html.parser") cards = soup.select("div[role='article']") return [parse_place(card) for card in cards if card.select_one("div.qBF1Pd")]
选择器 div[role='article'] 匹配结果面板中的每个地点卡片,辅助函数从中读取各个命名字段。商家名称位于 div.qBF1Pd,星级评分位于 span.MW4etd,评论数位于 span.UY7F9,文本形如"(1,234)",parse_reviews 将其剥离为整数。类别、地址和电话共享 div.W4Efsd 下的次要信息行,以中点符号分隔,因此我们按"·"分割来提取类别和地址,并用正则表达式识别电话号码。对每个字段进行防御性读取(缺失时回退到 None)意味着一条缺少电话或评分的列表仍能生成干净的记录,而不会导致循环崩溃。
Google 的类名(如 qBF1Pd 和 MW4etd)是混淆的构建产物,会在 Google 重新部署前端时更改。将上面的选择器视为起始模板,而非固定契约。当某个字段对所有结果都返回空时,在浏览器开发者工具中重新检查实时地点卡片并更新选择器。定期进行选择器维护对任何生产爬虫来说都是正常的,不意味着哪里出了问题。
步骤 4:处理多条结果并组装脚本
结果面板包含多个地点,而解析器已经为每张卡片返回一条记录,因此多结果处理本身已内置。现在将 fetch 和 parse 串联成一个可运行的脚本,并将记录导出为 JSON 和 CSV。CSV 是大多数潜在客户开发和研究工作流所需的格式,可直接导入电子表格或 CRM。
import csv import json import re import requests from bs4 import BeautifulSoup API_TOKEN = "YOUR_CRAWLBASE_TOKEN" API_ENDPOINT = "https://api.crawlbase.com/" FIELDS = ["name", "category", "address", "phone", "rating", "reviews"] def crawl(url): params = {"token": API_TOKEN, "url": url, "page_wait": 4000} response = requests.get(API_ENDPOINT, params=params) response.raise_for_status() data = json.loads(response.text) if data["original_status"] != 200: raise Exception(f"Unable to crawl '{url}'") return data["body"] def parse_reviews(el): if not el: return 0 digits = re.sub(r"[^\d]", "", el.get_text(strip=True)) return int(digits) if digits else 0 def parse_place(card): name_el = card.select_one("div.qBF1Pd") rating_el = card.select_one("span.MW4etd") reviews_el = card.select_one("span.UY7F9") info_rows = card.select("div.W4Efsd > div.W4Efsd") category, address, phone = None, None, None for row in info_rows: text = row.get_text(" ", strip=True) phone_match = re.search(r"(\(?\d[\d\-\s\(\)]{7,}\d)", text) if phone_match and not phone: phone = phone_match.group(1).strip() elif "·" in text and not category: parts = [p.strip() for p in text.split("·")] category = parts[0] address = parts[-1] if len(parts) > 1 else None return { "name": name_el.get_text(strip=True) if name_el else None, "category": category, "address": address, "phone": phone, "rating": float(rating_el.get_text(strip=True)) if rating_el else None, "reviews": parse_reviews(reviews_el), } def scrape_html(html): soup = BeautifulSoup(html, "html.parser") cards = soup.select("div[role='article']") return [parse_place(c) for c in cards if c.select_one("div.qBF1Pd")] def main(): url = "https://www.google.com/maps/search/restaurants+in+New+York" html = crawl(url) places = scrape_html(html) with open("maps_results.json", "w", encoding="utf-8") as f: json.dump(places, f, ensure_ascii=False, indent=2) with open("maps_results.csv", "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=FIELDS) writer.writeheader() writer.writerows(places) print(f"Saved {len(places)} places to JSON and CSV") if __name__ == "__main__": main()
用 python main.py 运行完整脚本。它获取"纽约餐厅"的渲染结果页,为每个地点卡片提取一条记录,并将所有内容写入 maps_results.json 和 maps_results.csv。DictWriter 使用相同的 FIELDS 列表作为 CSV 表头和列顺序,因此两个输出始终保持同步。替换 URL 中的查询,解析器会处理返回的任何内容。
输出结果示例
你将得到一个干净的地点记录列表,每条包含六个字段,可写入 JSON、CSV 或数据库。以下是精简后的 JSON 示例。
[ { "name": "Carmine's Italian Restaurant", "category": "Italian restaurant", "address": "200 W 44th St", "phone": "(212) 221-3800", "rating": 4.5, "reviews": 12873 }, { "name": "Katz's Delicatessen", "category": "Deli", "address": "205 E Houston St", "phone": "(212) 254-2246", "rating": 4.6, "reviews": 48091 } ]
CSV 以 FIELDS 顺序(name,category,address,phone,rating,reviews)呈现相同数据,每行一个地点。该格式可直接导入电子表格进行去重,或作为本地潜在客户列表的种子导入 CRM。
跨城市和查询扩展
在一个城市做一次搜索只是演示;真实任务往往需要对多个类别和地点组合运行同一个爬虫。URL 是唯一需要变化的部分,因此只需维护一份查询列表并循环遍历,对每个查询使用相同的函数进行解析。保持健康长期运行的关键是控制节奏,请求之间需要停顿,而不是密集循环发送。
import time from urllib.parse import quote_plus queries = [ "restaurants in New York", "coffee shops in Austin", "plumbers in Chicago", ] all_places = [] for q in queries: url = f"https://www.google.com/maps/search/{quote_plus(q)}" html = crawl(url) all_places.extend(scrape_html(html)) time.sleep(3) print(f"Collected {len(all_places)} places across {len(queries)} queries")
Crawlbase 默认每秒最多处理 20 个请求,对于有节奏的爬虫来说余量充足;如果确实需要更多,可联系支持团队提升上限。API 返回的 5XX 响应不收费,因此重试被封锁或不可用的 URL 不会产生额外费用。结果面板会加载第一批地点,随着滚动才显示更多,因此单次获取会返回某个查询的头部结果;若需要更深覆盖,建议按街区细化查询,而不是试图在一次请求中滚动无限列表。更多此类模式,请参见我们的抓取本地商业列表指南。
保持不被封锁
即使有可信 IP 和渲染加持,Google 仍会监测爬虫特征流量。养成几个习惯有助于保持运行健康。
- 控制请求节奏。 在紧密循环中密集请求 Maps 是最快被挑战的方式。分散请求,改变查询内容,而不是以全速循环同一关键词。
- 依赖 IP 轮换。 住宅 IP 池将请求分散到众多真实用户地址上,使单个地址不会触发限制。Crawling API 已为你处理这一切;如果你自己搭建技术栈,这部分是关键所在。
- 关注状态码。 运行过程中开始返回同意页面或 CAPTCHA,说明当前速率或 IP 层级已不够用。将其视为需要降速的信号,而不是可忽略的噪声。
- 字段为空时重新检查。 Google 会定期更改其标记。如果地点卡片停止解析,请在开发者工具中打开实时页面并更新选择器。
完整策略手册请参见如何在不被封锁的情况下抓取网站。由于 Maps 面板是 JavaScript 渲染的,爬取 JavaScript 网站指南解释了渲染的重要性以及如何开启它。如果你的项目还涉及普通搜索结果页,抓取 Google 搜索页面指南涵盖了该场景。
抓取 Google Maps 是否合法?
抓取 Google Maps 是否被允许,取决于 Google 的服务条款、你所在的司法管辖区,以及你对数据的用途。Google 的条款对其产品的自动化访问设有限制,因此无论工具多么谨慎,爬取都可能违反这些条款。本文中的代码并不改变这一点,它只是让技术部分可行。请阅读 Google 的条款及其 robots.txt,并将两者视为采集内容的边界。
以下几条值得坚守。只采集公开商业列表数据:无需账号即可在地图上看到的商家名称、地址、电话、类别、评分和评论数。这是商家为了让顾客找到自己而公开发布的信息。不要抓取个人评论内容、评论者姓名或资料,也不要抓取任何其他个人数据,更不要转载从列表中获取的照片或其他受版权保护的媒体。保持足够低的请求量,以免给 Google 的服务器造成压力,并控制爬取节奏而非全速运行。
如果你需要大规模地点数据或合同保障,Google 提供官方 Places API,这是查询商业详情的正式授权途径,也是任何超出轻量公开研究需求的正确选择。本指南刻意将范围限定在结果面板上可见的公开列表字段,因为这是让工作具备合理性的边界。仅限公开商业数据。如果你的项目需要更多内容,正确路径是官方 API 或数据协议,而不是更巧妙的爬虫。
核心要点
- 面板是 JavaScript 渲染的。 普通请求只能得到空壳,因此在地点卡片可供解析之前,你需要真实的浏览器来运行页面。
- Crawling API 负责获取和渲染。 使用 JavaScript token 向其发送 Maps URL,它会轮换住宅 IP 并运行页面脚本,返回完整的 HTML。
-
BeautifulSoup 每张卡片提取六个字段。 选择每个
div[role='article'],读取名称、类别、地址、电话、评分和评论数,并预期混淆的类名会发生漂移。 - 导出为 JSON 和 CSV。 每个地点一条记录,可直接导入电子表格或 CRM,用于本地潜在客户开发和市场研究。
- 只处理公开数据。 遵守 Google 的服务条款和 robots.txt,跳过评论和个人数据,大规模使用时优先考虑官方 Places API。
常见问题
为什么普通请求从 Google Maps 返回不了地点数据?
结果面板由 JavaScript 在页面加载后构建,因此普通的 requests.get 返回的大多是空壳,没有你在浏览器中看到的地点卡片。Google 同样会挑战不像真实浏览器的流量。通过 Crawling API 启用渲染进行获取,可从可信住宅 IP 运行页面脚本,使地点卡片出现在你得到的 HTML 中。
能用 Python 抓取 Google Maps 吗?
可以。借助 requests 和 BeautifulSoup,你可以获取渲染后的结果页,并为每个地点提取名称、地址、电话、类别、评分和评论数。Crawling API 充当桥梁,从可信 IP 将你的请求发送给 Google 并渲染 JavaScript,使请求顺利处理而不被封锁。更全面的 Python 入门,请参见我们的用 Python 抓取网站指南。
能从 Google Maps 列表中提取哪些商业字段?
本教程从每个地点卡片中提取六个字段:商家名称、类别、地址、电话、星级评分和评论数。这些是任何访客无需账号即可在地图上看到的公开列表字段。请在这些公开数据范围内操作,不要触碰评论内容、评论者资料或任何个人信息。
抓取 Google Maps 需要 JavaScript 渲染吗?
是的。与普通搜索结果页不同,Maps 结果面板不存在于初始 HTML 中,而是由浏览器中的脚本渲染的。请使用你的 Crawlbase JavaScript token,让 Crawling API 在捕获页面前先完成渲染。用 Python 抓取 JavaScript 页面指南说明了何时需要渲染。
如何跨多个城市抓取 Google Maps?
构建一个类别和地点查询列表,将每个查询转换为 /maps/search/ URL,然后循环遍历,通过 Crawling API 获取每个并用同一函数解析。请求之间停顿几秒,保持爬取节奏而不是密集发送,并在导出前将所有记录收集到一个合并列表中。
有官方替代方案可以代替抓取 Google Maps 吗?
有。Google 官方 Places API 是查询商家名称、地址、电话、评分和评论等商业详情的授权途径,背后有合同和配额保障。对于超出轻量公开研究的用途,或生产级别的使用量,官方 API 是正确路径;爬取公开面板最适合小规模、谨慎的、仅针对地图上任何人都能看到的数据的工作。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
