Google 购物是网络上最密集的电商平台之一。单一结果页面就将数十家零售商的商品标题、价格、卖家、评分和链接并排呈现,这正是价格监控或市场研究任务所需的数据形态。如果你在线销售任何商品,这些公开列表能让你实时了解竞争对手的定价以及谁在销售特定商品。

本指南展示如何使用 Python 以可靠的方式抓取 Google 购物数据。你将构建一个小型可运行爬虫,通过 Crawling API 获取已渲染的购物结果页面,使用 BeautifulSoup 解析每条列表,处理分页,并将记录导出为 JSON 和 CSV 用于价格追踪。整个教程范围限定在任何人无需账号即可看到的公开购物列表,末尾的法律部分并非套话,请在对任何真实流量执行操作之前先行阅读。

你将构建什么

一个 Python 脚本,接收公开的 Google 购物搜索 URL,通过 Crawling API 获取 HTML,并为页面上的每个商品提取一条结构化记录。我们将使用 "louis vuitton bags" 作为贯穿全文的示例查询,并从每条列表中提取以下字段:

  • 标题:列表中显示的商品名称。
  • 价格:显示的价格文字,是价格监控的核心信号。
  • 卖家:提供该商品的商家或零售商。
  • 评分:当列表展示时的平均顾客评分。
  • 链接:该商品 Google 购物页面的 URL。

为什么普通请求在 Google 购物上会失败

如果你从脚本向 Google 购物 URL 发起一个裸 HTTP 请求,很少能得到在浏览器中看到的整洁页面。两个问题与你作对。首先,Google 使用 JavaScript 渲染购物网格的大部分内容,并根据请求 IP 和地区定制返回内容,因此来自境外数据中心地址的请求可能收到同意声明页、货币错误或内容不完整的情况。其次,Google 会监控自动化流量:不像真实浏览器的请求会遭遇挑战、CAPTCHA 或在到达列表之前被拦截。

因此,一个有效的 Google 购物爬虫需要在单次请求中同时具备两项能力:一个被平台视为真实访客的 IP,以及一个在页面依赖脚本时能够渲染它的浏览器。你可以自行组合无头浏览器加轮换住宅代理池,但维持这些设施健康运行才是绝大部分工作量。Crawling API 将两者集成到单次调用中:你发送 URL,它从可信住宅 IP 获取页面并在需要时进行渲染,然后将渲染完毕的 HTML 返回给你进行解析。

地区和 IP 轮换在这里很重要

Google 购物根据请求来源的国家显示不同的商品、货币和卖家。来自目标国家住宅 IP 的请求看起来像普通购物者,而境外数据中心地址则是一个明显的信号。Crawling API 在服务器端通过住宅地址进行轮换,并允许你指定国家,让你获得本地购物者所见的列表。你可以从最多 20,000 次免费请求开始,无需信用卡。

前置条件

在编写任何代码之前,你需要准备几样东西。都不会花太长时间。

基础 Python。你应该能够编写和运行 Python 脚本,并使用 pip 安装包。如果你对 BeautifulSoup 还不熟悉,我们的 Python BeautifulSoup 使用指南涵盖了本教程所假设的解析基础知识。

Python 3.8 或更高版本。使用 python --version 确认你的版本。如果没有,可从 python.org 安装或通过 Anaconda 等发行版获取。

Crawlbase 账号和 token。注册后打开控制台,复制你的请求 token。Crawlbase 提供两种 token 类型:用于静态页面的普通 token,以及用于浏览器渲染页面的 JavaScript token。Google 购物在大多数地区可使用普通 token。最多 20,000 次免费请求:注册赠送 1,000 次,完成引导步骤即可获得更多。请像对待密码一样保管 token:它用于验证你的请求,因此不要将其提交到版本控制中。

设置项目

创建虚拟环境以隔离项目依赖,然后安装爬虫所需的两个库。

bash
python --version

python -m venv shopping_env
source shopping_env/bin/activate

pip install crawlbase beautifulsoup4

在 Windows 上,使用 shopping_env\Scripts\activate 代替 source 行来激活环境。两个依赖各司其职:crawlbase 是官方客户端,向 Crawling API 发送请求并返回渲染后的 HTML;beautifulsoup4 解析该 HTML,让你可以通过 CSS 选择器提取各字段。

步骤 1:通过 Crawling API 获取页面

先获取 HTML。使用 token 初始化 CrawlingAPI 客户端,然后编写一个小的 scrape_google_shopping() 函数,用一组选项发送目标 URL,检查底层页面是否以 200 状态返回,并返回解析后的 HTML。选项指定国家、设置真实的 user agent,并在捕获 HTML 之前给页面几秒钟进行渲染。

python
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup
import json

# Initialize CrawlingAPI with your access token
crawling_api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})

# Request options: pin the region, look like a real browser, let the page render
options = {
    "country": "US",
    "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
    "page_wait": 5000,
}

def fetch_html(url):
    response = crawling_api.get(url, options)
    if response["headers"]["cb_status"] != "200":
        print(f"Failed to fetch the page. Status: {response['headers']['cb_status']}")
        return None
    return response["body"].decode("utf-8")

if __name__ == "__main__":
    url = "https://www.google.com/search?q=louis+vuitton+bags&tbm=shop&num=20"
    html = fetch_html(url)
    if html:
        print(html[:500])

客户端返回一个响应,其 headers["cb_status"] 是 Crawlbase 获取页面时看到的状态,对其进行判断意味着拦截或同意声明页会作为明确消息暴露出来,而不是将错误数据传入解析器。搜索 URL 包含三个关键内容:q 是查询词,tbm=shop 将 Google 切换到购物模式,num=20 请求每页 20 条结果。使用 python shopping.py 运行脚本,你应该在前 500 个字符中看到真实的购物标记,这在你编写任何选择器之前就确认了获取是否成功。

Crawlbase Crawling API

cb_status(legacy pc_status) 返回 200,正是因为请求在第一时间就以真实购物者身份到达了 Google。Crawling API 从你指定国家的轮换住宅 IP 获取页面,为你渲染 JavaScript 繁重的购物网格,并返回渲染完毕的 HTML,让你无需自行运行无头浏览器集群和住宅代理池。先在免费层将其指向公开购物 URL 试试看。

了解 Google 购物结果页面

在编写选择器之前,了解结果页面的布局很有帮助。在浏览器中打开一个购物搜索,右键点击一个商品,选择"检查"在开发者工具中查看结构。每个结果由几个关键部分组成。

  • 商品列表。每张卡片包含标题、图片、价格、卖家或零售商名称,以及存在时的评分。这是抓取的主要内容。
  • 分页。结果分布在多个页面,通过更改 URL 中的 start 偏移量来访问。
  • 筛选和排序。价格区间、品牌和类别筛选会改变页面返回的内容,当你想获取特定片段时这一点很重要。
  • 赞助列表。部分卡片是广告。如果你只想要自然列表,需要将它们与赞助内容区分开来。

步骤 2:使用 BeautifulSoup 解析列表

获取到 HTML 后,将其加载到 BeautifulSoup 并通过选择器提取每个商品。Google 将每个网格结果包装在 .sh-dgr__grid-result 容器中,各字段位于其内嵌套元素中。在浏览器开发者工具中检查实时页面以确认当前的类名;以下选择器与撰写本文时的布局相匹配。

python
def parse_products(html):
    soup = BeautifulSoup(html, "html.parser")

    products = []
    for item in soup.select(".sh-dgr__grid-result"):
        title_el = item.select_one("h3.tAxDx")
        price_el = item.select_one("span.a8Pemb.OFFNJ")
        seller_el = item.select_one(".aULzUe.IuHnof")
        rating_el = item.select_one(".Rsc7Yb")
        link_el = item.select_one("a.Lq5OHe")

        products.append({
            "title": title_el.get_text(strip=True) if title_el else None,
            "price": price_el.get_text(strip=True) if price_el else None,
            "seller": seller_el.get_text(strip=True) if seller_el else None,
            "rating": rating_el.get_text(strip=True) if rating_el else None,
            "link": "https://www.google.com" + link_el["href"] if link_el else None,
        })

    return products

每个字段都有自己的选择器:h3.tAxDx 存放标题,span.a8Pemb.OFFNJ 存放价格,.aULzUe.IuHnof 存放卖家或零售商,.Rsc7Yb 存放评分,锚点 a.Lq5OHe 携带商品链接。Google 存储的是相对路径的 href,因此加上 https://www.google.com 前缀可将其转为完整 URL。每个字段都有守卫,... if el else None,这样缺失的评分或卖家在记录中留下 None,而不是引发异常中断整个运行。

选择器会发生变化

Google 的类名,如 tAxDxa8Pemb,是自动生成的,会在 Google 重新部署前端时变化。将上述选择器视为起始模板,而非固定契约。当某个字段对每个商品都返回 None 时,在浏览器开发者工具中重新检查实时页面并更新选择器。定期维护选择器是任何生产爬虫的正常操作,不是出了问题的迹象。

步骤 3:处理分页

二十个商品是演示;价格监控需要完整的数据集。Google 购物使用 start 查询参数进行分页,这是结果中的偏移量:start=0 是第一页,start=20 是第二页,start=40 是第三页,以步长 20 递增。每个页面的结构相同,因此你构建每个 URL,通过 Crawling API 获取,并用同一函数解析。在循环中加入短暂停顿可以让长时间运行保持健康。

python
import time

def scrape_multiple_pages(base_url, pages=3):
    all_products = []
    for page in range(pages):
        start_index = page * 20
        paginated_url = f"{base_url}&start={start_index}"
        html = fetch_html(paginated_url)
        if html:
            all_products.extend(parse_products(html))
        time.sleep(3)
    return all_products

循环将页面索引乘以 20 得到每个 start 偏移量,将其附加到基础 URL,并将解析后的商品收集到一个平铺列表中。页面之间的 time.sleep(3) 是长时间爬取中最有用的习惯:分散请求才能让 Google 将你视为购物者而非机器人。

步骤 4:整合并导出 JSON 和 CSV

现在将获取、解析和分页整合为一个可运行脚本,然后将结果写入 JSON 和 CSV。JSON 保留嵌套结构供代码使用;CSV 可直接在电子表格中打开,这正是大多数价格监控工作流实际使用的格式。

python
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup
import json
import csv
import time

crawling_api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})

options = {
    "country": "US",
    "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
    "page_wait": 5000,
}

def fetch_html(url):
    response = crawling_api.get(url, options)
    if response["headers"]["cb_status"] != "200":
        print(f"Failed to fetch the page. Status: {response['headers']['cb_status']}")
        return None
    return response["body"].decode("utf-8")

def parse_products(html):
    soup = BeautifulSoup(html, "html.parser")
    products = []
    for item in soup.select(".sh-dgr__grid-result"):
        title_el = item.select_one("h3.tAxDx")
        price_el = item.select_one("span.a8Pemb.OFFNJ")
        seller_el = item.select_one(".aULzUe.IuHnof")
        rating_el = item.select_one(".Rsc7Yb")
        link_el = item.select_one("a.Lq5OHe")
        products.append({
            "title": title_el.get_text(strip=True) if title_el else None,
            "price": price_el.get_text(strip=True) if price_el else None,
            "seller": seller_el.get_text(strip=True) if seller_el else None,
            "rating": rating_el.get_text(strip=True) if rating_el else None,
            "link": "https://www.google.com" + link_el["href"] if link_el else None,
        })
    return products

def scrape_multiple_pages(base_url, pages=3):
    all_products = []
    for page in range(pages):
        paginated_url = f"{base_url}&start={page * 20}"
        html = fetch_html(paginated_url)
        if html:
            all_products.extend(parse_products(html))
        time.sleep(3)
    return all_products

def save_json(data, filename="products.json"):
    with open(filename, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=4)

def save_csv(data, filename="products.csv"):
    if not data:
        return
    with open(filename, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=data[0].keys())
        writer.writeheader()
        writer.writerows(data)

def main():
    base_url = "https://www.google.com/search?q=louis+vuitton+bags&tbm=shop&num=20"
    products = scrape_multiple_pages(base_url, pages=3)
    if products:
        save_json(products)
        save_csv(products)
        print(f"Saved {len(products)} products to products.json and products.csv")

if __name__ == "__main__":
    main()

使用 python shopping.py 运行完整脚本。它遍历 "louis vuitton bags" 购物结果的三个页面,为每个商品提取一条记录,并将所有内容写入 products.jsonproducts.csv。要追踪不同商品,更改 base_url 中的 q 值;其余流水线会处理返回的任何内容。按计划运行它,对比各次运行中价格列的变化,你就拥有了价格监控系统的核心。

输出示例

你将得到一个整洁的商品记录列表,每条包含标题、价格、卖家、评分和链接,可直接写入 JSON、CSV 或数据库。

json
[
    {
        "title": "Louis Vuitton Mini Pochette Accessoires Monogram",
        "price": "$760.00",
        "seller": "louisvuitton.com",
        "rating": "4.5",
        "link": "https://www.google.com/shopping/product/11460745201866483383"
    },
    {
        "title": "Louis Vuitton Onthego Empreinte PM Black",
        "price": "$3,568.00",
        "seller": "StockX",
        "rating": null,
        "link": "https://www.google.com/shopping/product/7199001631589324220"
    }
]

CSV 镜像以每个商品一行的形式存放相同数据,列为 title,price,seller,rating,link,带有标题行,可在任何电子表格工具中直接打开。缺失的字段在 CSV 中显示为空单元格,在 JSON 中显示为 null,如上例中第二条列表的评分所示。

规模化时保持不被封锁

即使 IP 信任问题已由平台处理,Google 仍会监控爬虫形态的流量,购物平台也不例外。以下几个习惯能让运行保持健康。

  • 控制请求节奏。在紧密循环中密集抓取结果页面是最快触发挑战的方式。页面之间的 time.sleep 是有原因存在的,保留它,并让你的查询保持多样化而不是对单个词语全速翻页。
  • 依赖 IP 轮换。住宅 IP 池将请求分散到众多真实用户地址,使任何一个都不会触发限制。Crawling API 为你处理这些;如果你自建方案,这是需要重点投入的部分。
  • 指定正确的国家。价格和卖家因地区而异。将 country 选项设置为你监控的市场,以获得本地购物者所见的数据。
  • 当字段为空时重新检查。Google 定期更改其标记。如果某个字段停止解析,在开发者工具中打开实时页面并更新选择器。

更广泛的操作手册请参阅如何不被封锁地抓取网站。如果你正在构建周期性价格追踪任务,我们关于网络爬取用于价格情报的指南介绍了如何将原始列表转化为可用信号,而更广泛的电商网络爬取指南将同样方法扩展到其他商店。要抓取标准 Google 搜索结果而非购物标签,请参阅如何抓取 Google 搜索页面

抓取 Google 购物合法吗?

抓取 Google 购物是否被允许,取决于 Google 的服务条款、你所在的司法管辖区以及你对数据的使用方式。Google 的条款对自动化访问设有限制,因此无论你的工具多么谨慎,抓取行为都可能违反这些条款。这里的任何代码都不改变这一事实,它只是使技术部分可行。请阅读 Google 的条款和 robots.txt,并将两者视为你收集范围的边界。

以下几条值得坚守。只收集公开列表数据:任何人在购物结果页面上无需账号即可看到的商品标题、价格、卖家、评分和链接。将你的请求量控制在不会给 Google 服务器造成压力的范围内,控制爬取节奏而不是全速运行,并将国家设置为你实际需要的市场。不要批量转载 Google 的商品图片或受版权保护的媒体,远离任何需要登录的内容或任何个人数据,本教程均未涉及这些。

如果你需要以公开抓取无法合理支撑的规模或形式获取购物数据,Google 提供了官方路径。Shopping 的 Content API 以及商家和广告 API 是以程序化方式管理和读取商品数据的经授权方式,当项目超出适度、公开、节奏合理的收集范围时,官方协议才是正确路径。更聪明的爬虫无法替代数据协议。

回顾

核心要点

  • Google 购物使用 JavaScript 渲染且对地区敏感。普通请求会收到同意声明页或错误的货币,因此你需要来自正确国家可信住宅 IP 的已渲染获取。
  • Crawling API 通过真实 IP 进行获取。发送附带指定国家的购物 URL,它在服务器端轮换住宅 IP 并渲染网格,返回渲染完毕的 HTML 供你解析。
  • BeautifulSoup 完成提取工作。选择每个 .sh-dgr__grid-result,然后从中读取标题、价格、卖家、评分和链接,并预期生成的类名会发生变化。
  • 用 start 偏移量分页。以 20 为步长增加 start 以深入结果,并在页面之间加入 sleep 控制节奏。
  • 导出为 JSON 和 CSV。JSON 为代码保留结构;CSV 可在电子表格中打开,这正是大多数价格监控工作流使用的格式。坚守公开数据,遵守 Google 的服务条款和 robots.txt。

常见问题

为什么普通请求在 Google 购物上会失败或返回错误页面?

Google 使用 JavaScript 渲染购物网格的大部分内容,并根据请求 IP 和地区定制返回内容,因此来自境外数据中心地址的请求可能收到同意声明页、错误货币或部分内容,而不是你在浏览器中看到的列表。它还会标记不像真实浏览器的流量。通过 Crawling API 进行获取(使用轮换住宅 IP 并渲染页面)会让请求看起来像普通购物者,从而得到真实结果。

我可以从 Google 购物中提取哪些字段?

本教程从每张商品卡片中提取五个字段:标题、价格、卖家或零售商、有时显示的评分,以及商品购物页面的链接。你可以通过添加更多选择器扩展解析器以获取图片或运费文字。只收集公开列表数据,远离任何需要登录的内容。

如何在 Google 购物上处理分页?

使用 start 查询参数,它是结果中的偏移量:start=0 是第一页,start=20 是第二页,start=40 是第三页,以步长 20 递增。用偏移量构建每个页面 URL,通过 Crawling API 获取,用同一函数解析,并在请求之间暂停几秒以控制爬取节奏而不是猛烈抓取。

我可以将此用于价格监控吗?

可以,这正是主要的使用场景。按计划运行脚本,将结果导出为 CSV,对比各次运行中价格列的变化,以发现你追踪商品的价格上涨、下降和新卖家。指定 country 选项使价格与你关心的市场保持可比性。我们关于网络爬取用于价格情报的指南深入介绍了如何将原始数据转化为可用信号。

我的选择器没有返回任何内容。是什么变了?

几乎可以肯定是 Google 的标记发生了变化。tAxDxa8Pemb 这样的类名是自动生成的,会在 Google 重新部署前端时变化,所以上个月有效的选择器现在可能失效。在浏览器开发者工具中重新检查实时购物页面并更新选择器。定期维护选择器是任何生产爬虫的正常操作。

Google 购物有官方 API 吗?

有。Google 提供了 Shopping 的 Content API,以及让开发者以程序化方式管理商品列表、广告活动和效果数据的商家 API 和广告 API。如果你的项目需要大规模或以经授权形式获取购物数据,这些官方接口才是正确路径,而非公开抓取。

开始构建

大规模爬取任何站点,无需与基础设施对抗。

Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。

自助开通 · 无需销售通话 · 提供企业级爬取量