In this Article
抓取谷歌购物是指从谷歌购物结果中大规模采集结构化商品数据,例如标题、价格、卖家、评分和配送信息。由于这些结果受到严格保护,且会随地区变化,要可靠地完成这项工作,就需要轮换IP并正确进行地理定位,而不能只在单一连接上运行一个抓取器。
本指南以代码为主线,依次介绍地理定位请求、通过国家定向代理路由请求、解析谷歌刻意混淆的商品卡片、分页、遇到速率限制时的重试与退避、处理同意对话框的无头浏览器备用方案、可导出CSV的清晰数据模型、价格历史追踪器以及监控计划。同时也会如实说明反自动化机制的现实、自建抓取器与付费SERP API之间的取舍,以及相关法律边界。
DataImpulse是一家注重合规与道德采购的代理服务商,在195个国家提供超过9000万个住宅、移动和数据中心IP地址。它采用按量付费模式,每GB起价1美元,流量永不过期,被广泛用于网页抓取、广告验证、价格监控、市场调研和多账号管理。
关键事实
- 本地化定价: 要准确抓取谷歌购物数据,必须设置gl和hl参数,并通过目标国家的IP路由请求,因为价格、卖家和库存情况因地区而异。
- 最佳代理类型: 轮换住宅代理使用真实消费者IP,更有机会通过检测。
- 价格: 每GB起价1美元,按需付费,流量永不过期,无需订阅。
- 覆盖范围: 覆盖195个国家、超过9000万个道德来源的IP。
- 可靠性: 成功率99.51%,在G2上获得5分中的4.8分评分。
- 协议与定位: 支持HTTP、HTTPS和SOCKS5,包含国家定位功能。

可以从谷歌购物抓取哪些数据?
谷歌购物展示的商品列表包含多个结构化字段,对定价和市场调研很有用。每条结果通常带有一组一致的属性,可以提取并归一化为数据行。
- 商品详情: 标题、描述摘要、品牌、型号和商品图片URL。
- 价格: 标价、货币,有时还包括不同卖家之间的价格区间。
- 卖家: 商家名称,以及商品详情页中销售同款商品的其他卖家列表。
- 评分和评论: 综合星级评分和评论数量(如有)。
- 库存和配送: 库存状态和因地区而异的配送说明。
每当谷歌更新其页面布局时,具体字段就会发生变化,因此任何解析器都需要容忍缺失值和变化的标记结构。应将每个字段都视为可选项,并在提取后验证类型,而不是假设评分或卖家信息始终存在。由于同一商品在不同国家可能显示不同价格,从一开始就应围绕(商品、国家、时间戳)这一键设计数据结构,而不是事后再加入地理维度。
如何向谷歌购物发送地理定位请求?
要发送地理定位请求,可携带购物垂直搜索标志和地区参数访问谷歌搜索端点,并模拟真实购物者会发送的IP和参数信号。最重要的两个参数是 gl (国家,例如 gl=de 表示德国),以及 hl (界面语言,例如 hl=de)。第三个有用的参数是类似 location 的位置术语,可将其加入查询上下文,不过 gl 加上匹配的出口IP通常已能满足大多数需求。
仅设置参数是不够的,因为谷歌还会读取请求的IP来决定显示哪些价格和卖家。下面这个最简请求发送真实的请求头和地区参数组合。它刻意不使用代理,以便你在添加IP层之前先看到基础调用。
import requests
def build_params(query, country="us", lang="en", start=0):
return {
"q": query,
"tbm": "shop", # shopping vertical
"gl": country, # country bias, e.g. de, gb, fr
"hl": lang, # interface language
"num": 40, # results per page (soft cap)
"start": start, # pagination offset
}
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/124.0 Safari/537.36"
),
"Accept-Language": "en-US,en;q=0.9",
}
resp = requests.get(
"https://www.google.com/search",
params=build_params("wireless headphones", country="us", lang="en"),
headers=HEADERS,
timeout=30,
)
print(resp.status_code, len(resp.text))
保持 Accept-Language 请求头与 hl 一致。用英语请求头请求德语价格页面是一种不匹配,这既显得不自然,还可能返回错误的货币。当你扩大规模时,这个基础函数就成为决定地区设置的唯一位置,从而保持所有后续调用的一致性。
如何通过国家定向代理路由请求?
通过国家定向代理路由请求的方式是,将 requests 指向单一网关,并将目标国家编码在代理用户名中。使用DataImpulse时网关是 gw.dataimpulse.com:823,在用户名末尾添加 __cr.us或__cr.de 即可选择出口国家。国家定位包含在基础价格内,而州、城市、邮编和ASN定位则是付费附加项。如果你对代理身份验证还不熟悉,我们关于 代理身份验证指南详细介绍了用户名和密码的机制。
import requests
USER = "your_login"
PASS = "your_password"
GATEWAY = "gw.dataimpulse.com:823"
def proxy_for(country):
# __cr. pins the exit IP to that country
cred = f"{USER}__cr.{country}:{PASS}"
url = f"http://{cred}@{GATEWAY}"
return {"http": url, "https": url}
resp = requests.get(
"https://www.google.com/search",
params=build_params("wireless headphones", country="de", lang="de"),
headers=HEADERS,
proxies=proxy_for("de"),
timeout=30,
)
print(resp.status_code, len(resp.text))
关键原则是同时切换代理国家与 gl/hl 参数。要构建跨市场价格比较,需遍历国家列表,并让出口IP与地区设置保持同步,使每个响应都反映单一、一致的购物者所在位置。
MARKETS = [
{"country": "us", "lang": "en"},
{"country": "de", "lang": "de"},
{"country": "gb", "lang": "en"},
{"country": "fr", "lang": "fr"},
]
def fetch_market(query, market):
c, lang = market["country"], market["lang"]
return requests.get(
"https://www.google.com/search",
params=build_params(query, country=c, lang=lang),
headers={**HEADERS, "Accept-Language": f"{lang};q=0.9"},
proxies=proxy_for(c),
timeout=30,
)
for m in MARKETS:
r = fetch_market("wireless headphones", m)
print(m["country"], r.status_code, len(r.text))
DataImpulse提供的是IP层,而不是托管抓取服务,因此抓取器需要由你自行维护。这里通常适合使用轮换住宅代理,因为它们来自真实消费者设备。数据中心代理价格更低,但更容易被识别;面对最难访问的目标时,则可使用带有运营商级IP的移动代理。
如何解析谷歌购物的商品卡片?
解析商品卡片时应使用容错的HTML库,并依赖稳定的文本模式,而不是脆弱的深层选择器,因为谷歌的类名是混淆的且经常变化。你今天从浏览器开发者工具复制的选择器可能几周内就会失效,因此应把解析器设计成能优雅降级,而不是在页面布局变化时崩溃。
一种稳健的做法是读取每个候选卡片,从标签结构中提取标题和图片,并用支持货币识别的正则表达式提取价格。下面的选择器只是贴近实际的示例,并非保证有效;应预期需要更新它们,并将正则表达式作为可靠的后备方案。
from bs4 import BeautifulSoup
import re
# Currency-aware price matcher: symbol optional, thousands and decimals tolerated.
PRICE_RE = re.compile(r"(?:[$€£]|USD|EUR|GBP)\s?\d[\d.,]*")
def parse_cards(html):
soup = BeautifulSoup(html, "html.parser")
rows = []
# These class names ROTATE; treat them as hints, keep the fallbacks.
candidates = soup.select("div.sh-dgr__content, div.sh-dlr__list-result, div[data-docid]")
if not candidates:
candidates = soup.find_all("div") # last-resort sweep
for card in candidates:
text = card.get_text(" ", strip=True)
price_match = PRICE_RE.search(text)
if not price_match or len(text) > 400:
continue
img = card.find("img")
link = card.find("a", href=True)
rows.append({
"title": _first_text(card, ["h3", "h4"]),
"price_raw": price_match.group(0),
"image": img.get("src") if img else None,
"url": link["href"] if link else None,
})
return rows
def _first_text(card, tags):
for t in tags:
el = card.find(t)
if el and el.get_text(strip=True):
return el.get_text(strip=True)
return None
CURRENCY = {"$": "USD", "€": "EUR", "£": "GBP"}
def normalize_price(raw):
if not raw:
return None, None
symbol = next((s for s in CURRENCY if s in raw), None)
code = CURRENCY.get(symbol, "USD")
digits = re.sub(r"[^\d.,]", "", raw).replace(",", "")
try:
return float(digits), code
except ValueError:
return None, code
由于混淆标记在这里是常态,一些团队转而使用渲染后的DOM。关于静态HTML解析与渲染页面解析之间更广泛的取舍,请参阅我们的指南 抓取动态网页。
当谷歌返回429时,如何分页和退避?
分页时需推进 start 偏移量,步长应与每页大小一致;应对速率限制时,则应,将HTTP 429(以及空响应或验证码响应)视为需要等待并通过新IP重试的信号。谷歌会激进地限制重复的自动化请求,因此分页和退避应放在同一个循环中处理。
持续翻页,直到某次请求不再返回任何卡片,或达到页数上限。通过重新读取代理字典在各页之间轮换出口IP,并通过检查响应正文中的同意或验证码标记来检测软封锁,而不是仅依赖状态码。
BLOCK_MARKERS = ("unusual traffic", "/sorry/", "consent.google", "captcha")
def is_blocked(resp):
if resp.status_code in (429, 503):
return True
body = resp.text.lower()
return any(m in body for m in BLOCK_MARKERS)
def paginate(query, market, max_pages=5, page_size=40):
all_rows = []
for page in range(max_pages):
params = build_params(query, market["country"], market["lang"],
start=page * page_size)
resp = get_with_retry(
"https://www.google.com/search",
params=params, proxies=proxy_for(market["country"]),
)
if resp is None:
break
rows = parse_cards(resp.text)
if not rows:
break # no more results
all_rows.extend(rows)
return all_rows
下面的重试辅助函数使用带抖动的指数退避,将重试分散开来,避免一批被封锁的工作进程在同一瞬间同时重试。每次尝试都会重新获取代理字典,因此轮换池会在下一次尝试时给你一个新的IP。
import time, random
def get_with_retry(url, params, proxies, max_attempts=4):
for attempt in range(max_attempts):
try:
resp = requests.get(url, params=params, headers=HEADERS,
proxies=proxies, timeout=30)
except requests.RequestException:
resp = None
if resp is not None and not is_blocked(resp):
return resp
# exponential backoff: 2, 4, 8 seconds, plus jitter
wait = (2 ** (attempt + 1)) + random.uniform(0, 1.5)
time.sleep(wait)
return None # exhausted; caller decides what to do
即使重试成功,也要保持请求速率合理。在各页之间设置礼貌的基础延迟,再结合轮换,对长期稳定性的帮助远胜于疯狂请求并依赖重试。 网页抓取最佳实践 相关的说明中更详细地介绍了请求节奏和请求头卫生。
如何使用无头浏览器渲染被封锁的页面?
当普通的HTTP请求不断遇到同意墙或JavaScript渲染的内容时,就回退到无头浏览器,让它像真实客户端一样执行页面,并在读取DOM之前关闭同意对话框。Playwright很适合这项工作,因为它支持按上下文设置代理,并具有可靠的等待机制。
下面这个脚本通过一个国家定向的DataImpulse代理启动Chromium,处理欧洲地区通常会显示的cookie或同意对话框,等待内容加载完成,并返回渲染后的HTML,以便你将其输入到同一个 parse_cards 函数中处理。
from playwright.sync_api import sync_playwright
def render_shopping(query, country="de", lang="de"):
cred_user = f"{USER}__cr.{country}"
params = f"?q={query.replace(' ', '+')}&tbm=shop&gl={country}&hl={lang}"
url = "https://www.google.com/search" + params
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
proxy={
"server": "http://gw.dataimpulse.com:823",
"username": cred_user,
"password": PASS,
},
locale=f"{lang}-{country.upper()}",
user_agent=HEADERS["User-Agent"],
)
page = context.new_page()
page.goto(url, wait_until="domcontentloaded", timeout=45000)
_dismiss_consent(page)
page.wait_for_timeout(2000)
html = page.content()
browser.close()
return html
def _dismiss_consent(page):
# Consent dialogs vary by locale; try a few common accept buttons.
for label in ("Accept all", "Alle akzeptieren", "Tout accepter",
"I agree", "Accept"):
try:
btn = page.get_by_role("button", name=label)
if btn.count() > 0:
btn.first.click(timeout=3000)
page.wait_for_timeout(1000)
return
except Exception:
continue
无头浏览器在CPU和内存上的开销比原始请求更大,因此应把它当作针对确实需要渲染的页面的备用方案,而不是默认路径。许多团队为了速度直接运行HTTP请求,只有在 is_blocked is_blocked不断触发时才升级到Playwright。如果你的技术栈是JavaScript而非Python,同样的模式也适用于Puppeteer;请参阅 使用JavaScript进行网页抓取.
如何组织和导出抓取到的数据?
将数据围绕一条始终包含市场和采集时间戳的稳定记录来组织,然后导出为CSV,这样输出结果便于比对、加载到电子表格或推送到数据仓库中。使用数据类可以获得类型提示,并对一行数据有一个清晰的定义。
from dataclasses import dataclass, asdict, field
from datetime import datetime, timezone
import csv
@dataclass
class Offer:
query: str
country: str
title: str | None
price: float | None
currency: str | None
seller: str | None = None
url: str | None = None
captured_at: str = field(
default_factory=lambda: datetime.now(timezone.utc).isoformat()
)
def to_offers(rows, query, country):
offers = []
for r in rows:
price, currency = normalize_price(r.get("price_raw"))
offers.append(Offer(
query=query, country=country, title=r.get("title"),
price=price, currency=currency, url=r.get("url"),
))
return offers
写入CSV就变成了导出dataclass字段的简单操作。追加模式加上表头保护,可以让定时任务随时间不断扩充同一个文件,而无需重写它。
import os
def export_csv(offers, path="google_shopping.csv"):
if not offers:
return
fieldnames = list(asdict(offers[0]).keys())
write_header = not os.path.exists(path)
with open(path, "a", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
if write_header:
writer.writeheader()
for o in offers:
writer.writerow(asdict(o))
在每一行都保留国家和时间戳信息,日后就能回答诸如今天哪个市场某个SKU最便宜、或者本周价格如何变动之类的问题。这种地理与时间组合键正是下一节价格历史追踪器的基础。
如何随时间跟踪谷歌购物的价格历史?
要跟踪价格历史,可将每次观测结果按商品、国家和日期作为键,通过upsert写入小型数据库,这样重新运行抓取器只会更新当天那一行,而不会产生重复记录,同时保留之前各天的数据。SQLite足以应对数百万行数据,且无需服务器,使监控任务保持自包含。
下面的表结构使用了一个复合唯一键和一个 ON CONFLICT upsert 操作。同一天内运行两次抓取器会覆盖该(商品、国家、日期)组合的最新价格;而跨多天运行则会构建出之后可以绘制图表的时间序列。
import sqlite3
DDL = """
CREATE TABLE IF NOT EXISTS price_history (
product TEXT NOT NULL,
country TEXT NOT NULL,
day TEXT NOT NULL,
price REAL,
currency TEXT,
seller TEXT,
captured_at TEXT,
PRIMARY KEY (product, country, day)
);
"""
def init_db(path="prices.db"):
conn = sqlite3.connect(path)
conn.execute(DDL)
conn.commit()
return conn
def upsert_offers(conn, offers):
sql = """
INSERT INTO price_history
(product, country, day, price, currency, seller, captured_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(product, country, day) DO UPDATE SET
price=excluded.price,
currency=excluded.currency,
seller=excluded.seller,
captured_at=excluded.captured_at;
"""
for o in offers:
day = o.captured_at[:10] # YYYY-MM-DD
conn.execute(sql, (o.title, o.country, day, o.price,
o.currency, o.seller, o.captured_at))
conn.commit()
def price_trend(conn, product, country, days=14):
cur = conn.execute(
"""SELECT day, price, currency FROM price_history
WHERE product = ? AND country = ?
ORDER BY day DESC LIMIT ?""",
(product, country, days),
)
return cur.fetchall()
实际操作中,跨天匹配标题是最困难的部分,因为谷歌的标题在不同抓取之间会有细微差异。对于小型监控列表,建议在upsert之前,将原始标题映射到你自己掌控的规范商品ID,而不是把抓取到的标题当作稳定的键来信任。
如何安排持续的价格监控?
可将获取、解析和存储这几个步骤封装为一个入口函数,并用cron以固定节奏运行它来安排监控,这样价格就能自动刷新而无需手动运行。对于价格追踪来说,每天运行一次是明智的默认设置;更频繁的运行会同时增加成本和被封锁的风险,而对大多数商品目录而言并不会带来多少额外信号。
# run_monitor.py
def run_once(queries):
conn = init_db()
for query in queries:
for market in MARKETS:
rows = paginate(query, market, max_pages=3)
offers = to_offers(rows, query, market["country"])
export_csv(offers)
upsert_offers(conn, offers)
print(f"{query} / {market['country']}: {len(offers)} offers")
conn.close()
if __name__ == "__main__":
WATCHLIST = ["wireless headphones", "mechanical keyboard"]
run_once(WATCHLIST)
然后用cron注册它。下面这条条目每天06:15运行监控任务,并记录输出以便调试,这一点很重要,因为否则计划任务型抓取器会悄无声息地失败。
# crontab -e
# minute hour day month weekday command
15 6 * * * cd /opt/shopping && /usr/bin/python3 run_monitor.py >> monitor.log 2>&1
在任务中加入轻微的抖动(开始时随机休眠几分钟),避免每天都在完全相同的时刻发起请求。按前文所述轮换出口国家并调节请求节奏,同时留意日志中封锁率是否上升,这是谷歌已经调整防御策略或你的模式变得过于规律的最早信号。
应选择哪种采集方式:自建、隐藏端点、SERP API还是无头浏览器?
选择依据在于你愿意自行承担多少脆弱性和工程投入,相对于你愿意为每次请求支付多少费用。没有唯一正确的答案;坦率地说,这是控制权和成本与维护负担和反机器人风险之间的权衡。
反机器人现实: 谷歌通过速率限制、验证码、同意墙、轮换的混淆标记以及行为信号积极防御这些结果。任何自建抓取器都会周期性地失效,需要持续维护。代理和退避可以减少封锁,但无法彻底消除封锁。任何承诺提供永久不会被封锁的谷歌抓取器的人都是在过度承诺。
| 方式 | 成本 | 脆弱性 | 控制力 |
|---|---|---|---|
| DIY HTML解析 | 低(仅代理) | 高 | 完全 |
| 隐藏JSON端点 | 低(仅代理) | 非常高 | 完全 |
| SERP API | 高(按请求计费) | 低 | 有限 |
| 无头浏览器 | 中等(算力+代理) | 中等 | 完全 |
SERP API与自建方案对比: 第三方SERP API会以JSON形式返回已解析的购物结果,并替你承担解析和解封工作,但按请求计费,存在供应商锁定,且你对实际获取的内容控制较少。使用代理自建抓取器的单次请求成本低得多,控制力也更强,但当谷歌发生变化时,解析器、重试机制和维护工作都需要由你负责。需要明确的是,DataImpulse在这些自建方案中提供的是底层代理服务,不是SERP API,也不是托管抓取服务。如果你希望完全免除解析工作,SERP API是更合适的选择;如果你想降低成本、保有控制力,并能维护抓取器,那么使用轮换代理自建抓取器在经济性上更优。隐藏JSON端点的成本可能更低,但也最脆弱,因为谷歌随时可能更改或移除它们。
抓取谷歌购物数据合法且合乎道德吗?
抓取公开可见的商品数据通常被认为风险低于访问私有或受限内容,但这并非完全没有规则约束,本文也不构成法律建议。谷歌购物上的价格和列表是公开的,但你如何采集和使用这些数据仍然很重要,具体案例应向合格律师确认。
- 服务条款: 自动化访问可能与谷歌的服务条款发生冲突,这是与版权或计算机访问法律不同的一个合同问题。
- 个人数据: 商品列表通常不属于个人数据,但应避免收集评论者姓名或其他身份标识信息,并在适用的情况下遵守GDPR。
- 速率与负载: 保持请求量合理,以免影响你所查询的服务的正常运行。
- 数据使用: 大规模重复使用受版权保护的图片或描述,其风险要高于将价格作为事实来分析。
在数据采集方面,道德实践首先取决于所使用IP的来源。DataImpulse的IP来自自愿加入并获得报酬的用户,符合GDPR要求,并提供数据处理协议,从而支撑起一条站得住脚的合规链条。关于同意、来源和负责任实践的更全面介绍,请参阅我们的指南 道德网页抓取。将合规来源的IP与礼貌的请求节奏以及狭窄、真实的数据用途结合起来,就能同时把法律风险和声誉风险控制在较低水平。

常见问题
抓取谷歌购物需要住宅代理吗?
住宅代理是最可靠的选择,因为它们使用能够融入正常流量的真实消费者IP。数据中心代理在轻量级请求下可以使用,但更容易被检测和封锁。
如何从谷歌购物获取本地化价格?
将gl和hl参数设置为目标国家和语言,并使用国家语法通过该国家的代理IP路由请求,例如德国对应以__cr.de结尾的用户名。谷歌会同时读取参数和请求IP来决定显示哪些价格。
DataImpulse是谷歌购物抓取API吗?
不是。DataImpulse提供的是代理层(住宅、移动和数据中心IP),而不是托管抓取或SERP API。你需要在其IP之上运行自己的抓取器或第三方工具。
为什么我的谷歌购物抓取器一直被封锁?
谷歌会限制来自单一IP的重复自动化请求,并显示验证码和同意墙。轮换住宅代理、使用贴近真实浏览器的请求头、对429响应采用指数退避,以及保持合理的请求速率,都能显著减少封锁,不过没有任何方法能完全消除封锁。
我应该使用SERP API还是自己构建抓取器?
SERP API免除了解析和反封锁的工作,但每次请求的成本更高,控制力也更弱,适合希望零维护的团队。基于轮换代理的自建抓取器成本低得多,并提供完全的控制权,但需要随着谷歌的变化自行维护解析器和重试逻辑。
什么情况下DataImpulse不适合?
如果你需要静态ISP代理、完全托管的抓取API,或需要访问银行和政府网站,DataImpulse并不适合。它专注于提供轮换住宅、移动和数据中心代理,用于采集公开数据和访问内容。
相关指南
使用可靠的代理开始抓取谷歌购物数据
如果你准备在多个市场采集谷歌购物数据,DataImpulse可提供轮换和固定的住宅、移动及数据中心IP,包含国家定位功能,每GB起价1美元。 创建账户 并让你的第一个抓取器通过一个干净的IP池进行路由。
