跳至内容
免费开始
返回博客

搜索结果页面(SERP)抓取 API 指南:Google、Bing、DuckDuckGo、Yandex

Suciu Dan最后更新于 7 min read
搜索结果页面(SERP)抓取 API 指南:Google、Bing、DuckDuckGo、Yandex
简而言之:要使用 SERP Scraping API,请向 https://serpapi.webscrapingapi.com/v2 发送 GET 请求,附上 api_key、engine 和查询词(大多数引擎用 q,Yandex 用 text)。Google 和 Bing 返回已解析的 JSON,而 DuckDuckGo 和 Yandex 返回结果页的 HTML,需要由您的代码提取,因此请围绕这一差异规划解析和测试。本指南还介绍本地化、异步任务、错误和定价。

你需要获取特定国家/地区、特定语言和特定设备上的一组关键词的搜索结果,以便将这些数据存储在代码中并进行周环比比较。如果自行开发搜索抓取工具,每次结果页面发生变化时,你都不得不应对屏蔽机制并重写解析器,因此本指南改用 WebScrapingAPI 的 SERP API 来实现。 完成本指南后,您将能够:通过环境变量传入密钥发送首次请求;选择合适的搜索引擎;锁定要监测的市场;采用防御性解析方式处理 Google 和 Bing 的 JSON 数据;自行提取 DuckDuckGo 和 Yandex 的 HTML 内容;异步执行 Google 搜索;根据文档中规定的错误处理机制进行重试;并选择合适的套餐。

快速入门

  • 端点: https://serpapi.webscrapingapi.com/v2 (GET)
  • 必填: api_key, engine,以及查询内容(q 对于大多数搜索引擎, text Yandex 则需)
  • 输出:Google 和 Bing 返回已解析的 JSON。DuckDuckGo 和 Yandex 返回封装在 JSON 外壳中的结果页面 HTML
  • 免费试用:100 次 Google 搜索

什么是 SERP 抓取 API,它会返回什么?

WebScrapingAPI 的 SERP API 是一个服务器端接口,它接受搜索查询、搜索引擎和市场背景作为参数,然后将结果页面作为数据返回。Google 和 Bing 返回的是已解析的 JSON 集合。DuckDuckGo 和 Yandex 返回的则是一个 JSON 封装体,其 search_results 字段中包含搜索结果页面的 HTML 内容,供您的代码进行提取。

在编写任何代码之前,理解这种数据拆分方式至关重要。对于 Google 和 Bing,您需要读取命名集合,例如 organic[] 和 related[]。对于 DuckDuckGo 和 Yandex,您将收到原始页面,并自行决定从中提取哪些内容。您的数据存储模型、测试方案以及维护工作,都将取决于该搜索引擎属于上述哪一类。

该 API 涵盖四大搜索引擎家族:Google、Bing、DuckDuckGo 和 Yandex。除了标准搜索外,Google 还拥有七个专用搜索引擎:异步搜索、地图、地图评论、酒店、航班、趋势和反向图像搜索。 各团队利用这些功能进行排名和搜索结果页面(SERP)功能监控、品牌及发布商曝光情况监测、付费广告位显示情况分析,以及跨主题和发布商的市场调研。

  • WebScrapingAPI 负责:请求验证和搜索引擎执行、搜索访问基础设施、按文档规范整理的解析结果集合、HTML 搜索引擎的 JSON 封装交付,以及受支持端点行为的维护。
  • 您的团队负责:查询组、市场、语言和设备,以及任务调度、API 级重试、从 HTML 封装中提取数据、数据存储、排名计算、警报,以及数据使用和保留政策。

对于分析而言,还有一项重要边界需注意:该 API 并未定义通用的排名。每个结果仅描述某个市场、某台设备、某个时刻下的单个查询,因此请确保您存储的每一行数据都附带该上下文信息。想与其他基于请求时间的 API 进行比较吗?请查看 SERP API 与其他 Web 访问 API 的对比情况。

如何发送您的第一个 SERP API 请求?

创建一个账户,在仪表盘中订阅 SERP API,并将您的 API 密钥复制到环境变量中。然后向 https://serpapi.webscrapingapi.com/v2 ,并使用 api_key, engine=google,并将 q 将 设置为您的关键词。让您的 HTTP 库负责参数编码,并将密钥保存在服务器端。

  1. 注册一个 WebScrapingAPI 账户。
  2. 在控制台中订阅 SERP API。试用版包含 100 次 Google 搜索。
  3. 将密钥保存在您的终端或密钥管理器中: export WSA_API_KEY=...
  4. 使用以下代码片段之一发送请求。

该请求对所有搜索引擎的结构都相同。只有 engine、查询字段和上下文参数会发生变化,因此一旦此调用成功,其他所有搜索引擎只需调整参数即可。

curl。 --data-urlencode 标志会编码每个值,而 --max-time 120 并设置了显式的客户端超时:

# Google, parsed JSON, US English, mobile
EP="https://serpapi.webscrapingapi.com/v2"
curl --get --fail-with-body --max-time 120 "$EP" \
  --data-urlencode "api_key=$WSA_API_KEY" \
  --data-urlencode "engine=google" \
  --data-urlencode "q=running shoes" \
  --data-urlencode "gl=us" --data-urlencode "hl=en" --data-urlencode "device=mobile"

Python。这个小型客户端从环境中读取密钥,将每个值传递给 params 参数,以便 Requests 自动进行编码,根据错误处理协议进行重试,并记录请求头信息。下文的 Bing 部分和错误处理部分将复用该客户端。

"""Minimal WebScrapingAPI SERP API client: env-var key, jittered retries, usage headers.

Usage:
  export WSA_API_KEY=...                       # never hardcode the key
  python wsa_serp.py google "running shoes" gl=us hl=en device=mobile
  python wsa_serp.py bing "coffee roasters" mkt=en-US cc=us lang=en
  python wsa_serp.py yandex "pizza" lr=213 p=0
  python wsa_serp.py google "running shoes" gl=us --dry-run   # print the request, no call
"""
import os
import random
import sys
import time

import requests

ENDPOINT = "https://serpapi.webscrapingapi.com/v2"
# Most engines take their main input in q. These don't (see the engine table).
QUERY_FIELD = {"yandex": "text", "google_maps_reviews": "fid", "google_reverse_image": "url"}
RETRYABLE = {429, 500, 502, 503}   # back off with jitter, then retry
FINAL = {400, 401}                 # fix the key, plan, or parameters; never retry


def build_params(engine, query, **extra):
    params = {"engine": engine, QUERY_FIELD.get(engine, "q"): query}
    params.update({k: str(v) for k, v in extra.items()})
    return params


def search(engine, query, api_key, max_attempts=4, **extra):
    params = {"api_key": api_key, **build_params(engine, query, **extra)}
    for attempt in range(1, max_attempts + 1):
        resp = requests.get(ENDPOINT, params=params, timeout=120)
        usage = {k: v for k, v in resp.headers.items() if k.lower().startswith("searchdata-")}
        if resp.ok:
            return resp.json(), usage
        if resp.status_code in FINAL:
            # 401: missing, invalid, or unentitled key. 400: "Parameter '<name>' not allowed." etc.
            raise SystemExit(f"{resp.status_code}: {resp.text[:300]}")
        retry_422 = resp.status_code == 422 and attempt == 1   # target-side failure: one retry only
        if resp.status_code in RETRYABLE or retry_422:
            time.sleep(random.uniform(0, min(2 ** attempt, 30)))   # full jitter
            continue
        raise SystemExit(f"Unexpected {resp.status_code}: {resp.text[:300]}")
    raise SystemExit(f"Gave up after {max_attempts} attempts")


def organic(data):
    """Product pages document organic[]; some docs pages show organic_results[]. Accept both."""
    return data.get("organic") or data.get("organic_results") or []


def main():
    args = [a for a in sys.argv[1:] if a != "--dry-run"]
    if len(args) < 2:
        sys.exit(__doc__)
    engine, query = args[0], args[1]
    extra = dict(a.split("=", 1) for a in args[2:])

    if "--dry-run" in sys.argv:
        prepared = requests.Request(
            "GET", ENDPOINT, params={"api_key": "REDACTED", **build_params(engine, query, **extra)}
        ).prepare()
        print(prepared.url)
        return

    api_key = os.environ.get("WSA_API_KEY")
    if not api_key:
        sys.exit("Set WSA_API_KEY first (dashboard -> SERP API).")

    data, usage = search(engine, query, api_key, **extra)
    print("usage headers:", usage)
    if "search_results" in data:          # DuckDuckGo and Yandex: SERP HTML inside a JSON envelope
        print("search_parameters:", data.get("search_parameters"))
        print("html chars:", len(data["search_results"]))
    else:                                  # Google and Bing: parsed JSON
        for row in organic(data)[:10]:
            print(row.get("rank") or row.get("position"), row.get("title"), row.get("link"))


if __name__ == "__main__":
    main()

运行 python wsa_serp.py google "running shoes" gl=us hl=en device=mobile --dry-run 。它会打印精确的请求 URL,其中键被替换为 REDACTED,以便您在消耗一次搜索配额前检查参数。

Node.js(18 或更高版本)。URLSearchParams负责处理编码,而 AbortSignal.timeout 设置了相同的 120 秒限制:

// Build and (optionally) send a SERP API request in Node 18+.
// URLSearchParams handles encoding; the key comes from the environment.
const ENDPOINT = "https://serpapi.webscrapingapi.com/v2";
const QUERY_FIELD = { yandex: "text", google_maps_reviews: "fid", google_reverse_image: "url" };

export function buildSerpUrl(apiKey, engine, query, extra = {}) {
  const url = new URL(ENDPOINT);
  url.searchParams.set("api_key", apiKey);
  url.searchParams.set("engine", engine);
  url.searchParams.set(QUERY_FIELD[engine] ?? "q", query);
  for (const [k, v] of Object.entries(extra)) url.searchParams.set(k, String(v));
  return url;
}

export async function search(engine, query, extra = {}) {
  const apiKey = process.env.WSA_API_KEY;
  if (!apiKey) throw new Error("WSA_API_KEY is required");
  const res = await fetch(buildSerpUrl(apiKey, engine, query, extra), {
    signal: AbortSignal.timeout(120_000),
  });
  if (!res.ok) throw new Error(`SERP API ${res.status}: ${(await res.text()).slice(0, 300)}`);
  const data = await res.json();
  return { data, organic: data.organic ?? data.organic_results ?? [] };
}

// Dry run: show the encoded URL and prove the query survives the round trip.
if (process.argv[1]?.endsWith("build_serp_request.mjs")) {
  const q = 'site:example.com "running shoes" & socks?';
  const u = buildSerpUrl("REDACTED", "google", q, { gl: "us", hl: "en", device: "mobile" });
  console.log(u.toString());
  console.log("round trip ok:", new URL(u).searchParams.get("q") === q);
  const y = buildSerpUrl("REDACTED", "yandex", "pizza", { lr: 213, p: 0 });
  console.log(y.toString());
}

干跑示例说明了编码为何重要。查询 site:example.com "running shoes" & socks? 变为 q=site%3Aexample.com+%22running+shoes%22+%26+socks%3F ,解码后保持不变(round trip ok: true)。如果你手动将该查询拼接到 URL 中,仅 & 将导致你的 q 值,并开始一个名为 socks?。该 /v2 端点会按引擎对参数进行验证,并拒绝其不识别的名称,因此手动构建的 URL 会以难以预料的方式失败。请让库在每次请求时都应用百分比编码。

请将密钥视为密码。将其保存在服务器端的环境变量或密钥管理器中,切勿将其放入前端代码、公开仓库、日志或截图中。任何掌握该密钥的人都能消耗您的搜索配额。

文档:访问 API

可以查询哪些搜索引擎和 Google 引擎?

当前的 API 参考文档列出了十一种搜索引擎: google, google_async, google_maps, google_maps_reviews, google_flights, google_hotels, google_trends, google_reverse_image, bing, duckduckgo,以及 yandex。每个引擎都有其特定的必填输入项和允许的参数。请仅将上述引擎视为受支持的对象,即使旧版文档中描述了其他 Google 引擎。

引擎

必填输入

返回结果

定价

google

q

已解析的 JSON(general, input, navigation[], organic[],可选模块)

Google 计划

google_async

q

response_id,随后是最终的搜索结果页面(SERP)有效载荷

Google 计划

google_maps

type=search + q,或 type=place + data

解析后的地点搜索结果

谷歌计划

google_maps_reviews

fid

已解析的评论

谷歌计划

google_hotels

type=search + q,或 type=place + pid;入住和退房日期

overview, organic[]

谷歌行程

google_flights

q (编码后的Google Flights令牌)

search_parameters, search_results[]

谷歌行程

google_trends

q

widgets[] (TIMESERIES, GEO_MAP)

Google行程

google_reverse_image

url

general, images[], exact_matches[]

谷歌行程

bing

q

解析后的 JSON (general, organic[], related[])

引用

duckduckgo

q

JSON 封装: search_parameters + search_results (HTML)

引用

yandex

text

JSON 封装: search_parameters + search_results (HTML)

引用

这三款搜索引擎不以 q。Yandex将查询作为 text,Google 地图评论接受地点的 fid,而 Google 反向图像搜索则接受一张图片 url。上文提到的 Python 和 Node 客户端已为您映射了这些字段。Google 地图和 Google 酒店还需 type=search 或 type=place,这会改变另一个必填字段。

“Google 套餐”指的是定价部分中提到的已发布的月度 Google 套餐。Bing、DuckDuckGo 和 Yandex 则按报价计价。

您可能会找到其他 Google 引擎的文档页面,例如自动完成、活动、财经、招聘、本地服务、地图照片、Play、产品和学术搜索。这些不在支持列表中,因此请勿基于它们构建数据管道。

文档:SearchData 和 SERP API 参考指南

如何根据国家/地区、语言和设备对 Google 搜索结果进行本地化?

将 gl 为两位字母的国家代码, hl 设置为界面语言,并将 device 设置为 desktop, mobile,或 tablet。添加 domain 用于特定的 Google 主机,并 uule 当城市级上下文重要时,添加编码位置。请将这些值与每个结果一同存储,因为同一查询在不同市场中的排名可能不同。

谷歌官方表示,搜索结果取决于您的搜索位置,因此脱离市场背景的排名数据仅是半真半假。以下是在 google.ca 上进行的加拿大英语桌面搜索“history”,通过“首次请求”部分中的 Python 客户端实现:

python wsa_serp.py google history gl=ca hl=en domain=google.ca device=desktop

若需指定特定城市(例如安大略省伦敦市),请添加其 uule 值作为额外的 uule=... 参数。请仅使用搜索引擎文档中明确支持的参数。那些按日期排序或按时间段过滤的旧示例不适用于 /v2 Google 搜索,且该端点会拒绝不允许的参数。

结果类型和分页方式也会影响返回的内容:

  • 显示项: tbm 切换结果类型: isch 对于图片, vid 对于视频, nws 针对新闻,以及 shop 针对购物内容。对于 Google 职位搜索结果,请使用 ibp=htl;jobs.
  • 分页: start 设置要跳过的结果数量,以及 num 设置每页显示的结果数量。

这里最实用的做法是“市场设计”。在收集任何数据之前,先选择一个小而固定的查询×国家×语言×设备组合网格,并在每次运行之间保持不变。如果某一周的运行使用 gl=us&device=mobile ,而下一周使用 gl=us&device=desktop,所谓的“排名下滑”可能仅仅是设备变更所致。每个响应仅代表某个市场的单次观测结果,而非放之四海皆准的真理,因此比较仅在同一网格单元内才有意义。

请求字段和响应块的完整列表可在 Google 搜索 API 控制页面上找到。

引擎

位置 / 市场

语言

设备

分页 / 时间

筛选条件和界面

Google

gl (国家), domain (Google主机), uule (编码位置)

hl

device = 桌面端、移动端、平板端

start (跳过), num (每页)

tbm = isch, vid, nws, shop; ibp=htl;jobs

必应

mkt (例如,en-US), cc, location, lat, lon

lang

—

first (偏移量), count

safesearch = 关闭、中等、严格

DuckDuckGo

kl (例如,en_US)

kad (界面)

device

df = d, w, m, y

kp = 1, -1, -2

Yandex

lr (地区 ID,例如 213), yandex_domain

lang

device

p (从0开始计数), within = 77(24 小时),1(2 周)

—

文档:Google 搜索 API 参数

如何读取并存储已解析的 Google 和 Bing 搜索结果?

读取包含排名结果的 organic 数组,每个结果包含标题、链接、显示链接、描述和排名。将其他模块(如相关搜索、导航、广告或本地结果)视为可选内容。将每行数据与查询词、市场、设备和捕获时间一同存储,以便后续排名比较具有可比性。

Google 的响应包含几个顶级块。 general 描述请求信息(搜索引擎、语言、位置、移动设备标记和搜索类型), input 包含原始搜索 URL, navigation[] 列出该查询在 Google 上的其他展示位置,以及 organic[] 包含排名结果。Bing 的响应包含 general, organic[],以及 related[].

其余内容均为条件性显示。广告、本地结果、购物、媒体和相关搜索仅在结果页面显示时才会出现,而 Google 展示的搜索结果页面(SERP)功能会随查询内容、位置和设备而变化。因此,请在代码中区分两种情况:请求失败(HTTP 错误)和请求成功但缺少某个模块的情况。 第二点是一个常规现象,并非错误。

请采用防御性编程来编写解析器。某些文档页面将排序列表显示为 organic_results[] ,并使用 position 而非 organic[] , rank,因此请参考其中任意一种。该脚本将响应结果展平为按排名排列的行。输入文件是一个带标签的示例,而非实时输出:

"""Flatten a Google or Bing SERP API response into rank rows.

Organic results may arrive as organic[] (product pages) or organic_results[] (some docs pages),
and modules such as related[] or navigation[] are optional, so read them defensively.
"""
import json
import sys
from datetime import datetime, timezone


def rank_rows(data, query, market):
    rows = []
    captured = datetime.now(timezone.utc).isoformat(timespec="seconds")
    for item in data.get("organic") or data.get("organic_results") or []:
        rows.append({
            "captured_at": captured,
            "engine": data.get("general", {}).get("search_engine"),
            "query": query,
            "market": market,
            "rank": item.get("rank") or item.get("position"),
            "title": item.get("title"),
            "link": item.get("link"),
        })
    return rows


if __name__ == "__main__":
    data = json.load(open(sys.argv[1] if len(sys.argv) > 1 else "sample_google.json"))
    for row in rank_rows(data, "running shoes", "us/en/mobile"):
        print(row)
    print("optional modules present:", [k for k in ("navigation", "related", "related_questions") if k in data])

在样本文件上,它会为每个自然搜索结果打印一行,并列出其中包含的可选模块:

{'captured_at': '2026-10-29T12:03:56+00:00', 'engine': 'google', 'query': 'running shoes', 'market': 'us/en/mobile', 'rank': 1, 'title': 'Example result one', 'link': 'https://example.com/a'}
{'captured_at': '2026-10-29T12:03:56+00:00', 'engine': 'google', 'query': 'running shoes', 'market': 'us/en/mobile', 'rank': 2, 'title': 'Example result two', 'link': 'https://example.org/b'}
optional modules present: ['navigation']

每行排名数据应至少包含 captured_at:搜索引擎、查询词、市场、设备、排名、标题和链接。借助这些字段,您可以比较不同运行结果、绘制排名走势图,并在排名下降时触发警报。这就是长期监控排名和搜索结果页面(SERP)特征的核心所在。 如果您的存储链接包含 Google 重定向包装符,请在按 URL 去重之前,阅读关于结果 URL 中 Google /goto 重定向链接的相关说明。

如何使用 SERP API 搜索 Bing?

设置 engine=bing 并传入查询内容 q中。通过 mkt,例如 en-US,并 cc 用于国家, lang 用于指定语言。添加 location,或 lat 以及 lon,以指定精确的来源, safesearch 用于筛选,以及 first 配合 count 可分页浏览结果。Bing的响应以解析后的JSON格式返回。

针对美国市场的 curl 版本,查询前 10 条结果:

curl --get --fail-with-body --max-time 120 "https://serpapi.webscrapingapi.com/v2" \
  --data-urlencode "api_key=$WSA_API_KEY" \
  --data-urlencode "engine=bing" \
  --data-urlencode "q=coffee roasters" \
  --data-urlencode "mkt=en-US" --data-urlencode "cc=us" --data-urlencode "lang=en" \
  --data-urlencode "first=0" --data-urlencode "count=10"

使用“首次请求”部分中的 Python 客户端,相同的调用只需一行代码:

python wsa_serp.py bing "coffee roasters" mkt=en-US cc=us lang=en first=0 count=10

Bing 返回 general, organic[],以及 related[]。广告和图片是可选模块,因此同样适用以下防御性规则:阅读 organic[],将其余内容视为存在或不存在,并将每行数据与其对应的市场一同存储。 safesearch 取 off, moderate,或 strict.

保留 mkt, cc,并 lang 在不同运行之间保持一致,就像你使用 Google 的 gl 和 hl一样。Bing 采用报价定价,100 次搜索的试用版涵盖 Google。请求字段和响应块详见 Bing 搜索 API 页面。

文档:Bing 基础 API 请求

如何处理 DuckDuckGo 和 Yandex 的 HTML 搜索结果?

这两个搜索引擎都会返回包含 search_parameters,用于描述请求,以及 search_results,该字符串包含搜索结果页面(SERP)的源HTML。WebScrapingAPI不会解析该HTML,因此您的应用程序需自行提取所需字段。请将选择器绑定到实际捕获结果,为其版本化,存储原始封装数据,并在选择器不再匹配时发出警报。


Google、Bing

DuckDuckGo、Yandex

响应

JSON 元数据 + 已解析的集合

JSON 元数据 + 源 SERP HTML,格式为 search_results

解析由

WebScrapingAPI,相关文档详见

您的应用程序

您的代码执行

以防御性方式读取集合;存储行

选择和版本选择器;提取;在数据漂移时发出警报

存储

行数据 + 请求上下文

原始封装数据 + 提取的行 + 选择器版本

DuckDuckGo 请求字段: q 针对查询, kl 用于地区和语言(例如 en_US), kad 用于界面语言, kp 用于安全搜索(1 开启、 -1 中等, -2 关闭), df 关于时间(d, w, m, y)以及 device.

Yandex 请求字段: text 用于查询, lr 用于地区 ID(例如, 213), lang, device, yandex_domain, p 用于页面(从零开始计数,因此 p=0 即第一页),以及 within 时间(77 过去24小时内, 1 表示过去两周)。

# DuckDuckGo, SERP HTML in a JSON envelope
curl --get --fail-with-body --max-time 120 "https://serpapi.webscrapingapi.com/v2" \
  --data-urlencode "api_key=$WSA_API_KEY" \
  --data-urlencode "engine=duckduckgo" \
  --data-urlencode "q=privacy research" \
  --data-urlencode "kl=en_US" --data-urlencode "df=w"

# Yandex, SERP HTML in a JSON envelope (query parameter: text)
curl --get --fail-with-body --max-time 120 "https://serpapi.webscrapingapi.com/v2" \
  --data-urlencode "api_key=$WSA_API_KEY" \
  --data-urlencode "engine=yandex" \
  --data-urlencode "text=pizza" \
  --data-urlencode "lr=213" --data-urlencode "p=0"

这两个引擎的提取模式相同:验证信封,解析 HTML,筛选结果链接,移除引擎自身的导航链接,为每一行添加选择器版本标签,若无匹配项则明确报错,并保留原始 HTML 以便日后重新处理。本脚本使用 Beautiful Soup 配合 CSS 选择器:

"""Pull result links out of a DuckDuckGo or Yandex SERP API envelope.

These engines return {"search_parameters": {...}, "search_results": "<html>...</html>"}.
Your application owns extraction: pin the selector after inspecting a real capture,
version it, and alert when it stops matching. The selectors below fit the synthetic
fixtures only.
"""
import json
import sys
from urllib.parse import urlparse

from bs4 import BeautifulSoup

SELECTORS = {"duckduckgo": "div.result a.result-link", "yandex": "li.serp-item a.link"}
ENGINE_HOSTS = ("duckduckgo.com", "yandex.com", "yandex.ru")


def extract(envelope, selector_version="fixture-v1"):
    params = envelope.get("search_parameters", {})
    engine = params.get("search_engine")
    html = envelope.get("search_results")
    if not isinstance(html, str) or "<html" not in html.lower():
        raise ValueError("search_results is missing or is not HTML; store the raw envelope and alert")
    soup = BeautifulSoup(html, "html.parser")
    rows = []
    for pos, a in enumerate(soup.select(SELECTORS[engine]), start=1):
        host = urlparse(a.get("href", "")).hostname or ""
        if host.endswith(ENGINE_HOSTS):
            continue  # skip the engine's own navigation links
        rows.append({"engine": engine, "query": params.get("query"), "position": pos,
                     "title": a.get_text(strip=True), "link": a["href"],
                     "selector_version": selector_version})
    if not rows:
        raise ValueError(f"selector matched nothing for {engine}; markup may have changed")
    return rows


if __name__ == "__main__":
    for path in sys.argv[1:] or ["sample_ddg_envelope.json", "sample_yandex_envelope.json"]:
        for row in extract(json.load(open(path))):
            print(row)

SELECTORS 仅适用于合成样本文件。真实的DuckDuckGo和Yandex标记结构不同,且会发生变化。在生产环境中运行此脚本前,请保存一个真实的封装结构,检查其HTML,确定您自己的选择器,并在 selector_version 。当标记发生变化时, ValueError 会触发异常,而非让您的处理管道默默存储零结果;而且由于您保留了原始 HTML,在修复选择器后,您可以重新提取受影响的运行结果。

特定于搜索引擎的字段位于 DuckDuckGo 搜索 API 和 Yandex 搜索 API 页面上。

文档:DuckDuckGo API 参数 · Yandex API 参数

何时应使用 Google 地图、趋势、酒店、航班或反向图像搜索?

当答案不在常规网页结果中时,请使用 Google 的专用搜索引擎。Google 地图用于查找地点,地图评论通过 fid,“趋势”提供随时间变化的兴趣趋势图表,“酒店”和“航班”返回旅行结果,“反向图片搜索”则通过 URL 匹配图片。每个引擎都有其特定的输入要求和参数。

它们均采用与标准搜索相同的端点、密钥和编码规则。仅 engine 值和字段有所不同:

  • google_maps:发送 type=search 替换为 q,或 type=place 配合 data. ll=@lat,lon,zoom 设置地图视图,而 place_type 筛选范围限定为酒店或度假租赁房源。文档中未提及分页功能。
  • google_maps_reviews:发送该地点的 fid,该字段取自 knowledge.fid 字段中获取。使用 sort (qualityScore, newestFirst, ratingHigh, ratingLow) 进行排序,并使用 start 和 num.
  • google_trends:发送 q 使用 geo, date (today 12-m, now 1-H,或一个 YYYY-MM-DD YYYY-MM-DD 范围), cat,以及 gprop (images, news, froogle, youtube)。响应中包含 widgets[],例如 TIMESERIES 和 GEO_MAP。
  • google_hotels:发送 type=search 使用 q,或 type=place 配合 pid,外加 check_in_date 以及 check_out_date。入住日期、货币和免费取消筛选条件均为可选。
  • google_flights: q 是一个经过编码的 Google Flights 查询令牌,并非纯文本。添加 curr, hl,以及 gl 用于指定货币和市场。
  • google_reverse_image:发送图片中的 url,并使用 lens (products, visual_matches, exact_matches, homework) 来选择匹配类型。

如果您正在权衡反向图片搜索与 tbm=isch 图片搜索结果之间难以抉择,请参阅《Google图片搜索API对比》。

文档:Google 搜索引擎

如何异步执行 Google 搜索?

使用 engine=google_async。API 会将其加入队列并立即返回一个 response_id 。将该ID与任务关联存储,随后轮询同一 /v2 端点,并 snapshot_id 并设置为该 ID, type=serp ,持续轮询,直到收到已完成的 SERP 有效载荷。请设定总等待时间上限。

Async 适用于大型批次以及无法保持 120 秒请求处于打开状态的工作进程。它支持常规的 Google 字段以及 device 以及 response=json。该模式仅适用于 Google:Bing、DuckDuckGo 和 Yandex 均不支持异步引擎,因此请勿围绕它们设计共享的异步队列。

"""Submit a google_async job, then poll for the finished SERP payload.

Usage:
  export WSA_API_KEY=...
  python google_async.py "events in new york city"
  python google_async.py "events in new york city" --dry-run
"""
import os
import random
import sys
import time

import requests

ENDPOINT = "https://serpapi.webscrapingapi.com/v2"


def submit(api_key, query, **extra):
    r = requests.get(ENDPOINT, params={"api_key": api_key, "engine": "google_async", "q": query, **extra}, timeout=60)
    r.raise_for_status()
    return r.json()["response_id"]          # store this id with your job record


def poll(api_key, response_id, max_wait=600):
    deadline = time.monotonic() + max_wait
    while time.monotonic() < deadline:
        r = requests.get(ENDPOINT, params={"api_key": api_key, "snapshot_id": response_id, "type": "serp"}, timeout=60)
        if r.ok:
            body = r.json()
            if body.get("status") in (None, "finished"):
                return body                  # persist it; don't re-poll old ids later
        elif r.status_code not in (429, 500, 502, 503):
            raise SystemExit(f"{r.status_code}: {r.text[:300]}")
        time.sleep(random.uniform(10, 30))   # docs: poll every 10 to 30 seconds
    raise SystemExit(f"No result for {response_id} after {max_wait} s")


if __name__ == "__main__":
    query = sys.argv[1] if len(sys.argv) > 1 else "events in new york city"
    if "--dry-run" in sys.argv:
        for p in ({"api_key": "REDACTED", "engine": "google_async", "q": query},
                  {"api_key": "REDACTED", "snapshot_id": "<response_id>", "type": "serp"}):
            print(requests.Request("GET", ENDPOINT, params=p).prepare().url)
        sys.exit(0)
    key = os.environ.get("WSA_API_KEY") or sys.exit("Set WSA_API_KEY first.")
    rid = submit(key, query)
    print("response_id:", rid)
    print(str(poll(key, rid))[:500])

每个结果一经收到即刻持久化,切勿稍后返回重新轮询旧的ID。该循环设有硬性截止时间,并将任何意外状态视为停止信号,因此卡住的任务无法无限期占用一个工作者。

文档:异步 API

您的代码应如何处理 SERP API 错误和重试?

不要重试 401 或验证错误。请修正键、计划或参数。 对于 429、500、502 和 503 错误,应采用指数退避和抖动策略重试几次,并在遇到 429 时放缓重试频率。422 错误表示搜索引擎返回了错误、超时或挑战,因此仅当失败看起来是暂时的,才重试一次。

状态

含义

操作

401

无密钥、密钥无效,或密钥无权访问 SERP API

请修正密钥或套餐。请勿重试。

验证错误

所选引擎的参数未知、缺失或超出范围

请修正请求。请勿重试。

422 (error_type: "website")

搜索引擎返回了无效的请求体、挑战、超时或错误状态

若问题看似暂时性,则采用退避策略重试一次;随后记录日志并跳过

429

并发请求过多

降低速率,采用抖动策略进行退避,并重试

500、503

处理程序发生意外错误或身份验证后端出现短暂故障

采用退避策略重试几次

502

异步提交收到无效响应

采用退避策略重试提交

身份验证错误很容易识别。没有密钥的请求会返回 401 状态码,并显示“访问被拒绝。未提供密钥 api_key "的提示;而密钥错误或无权限时,则返回 401 状态码,并附带 Authentication failure: Invalid API key…。无论哪种情况,重试只会导致失败重演。

验证消息会直接指出问题所在: Parameter '<name>' not allowed., Parameter '<name>' is required., Parameter '<name>' must be in range …,以及 Parameter '<a>' cannot be used with parameter '<b>'. 这就是为什么从旧示例中复制的参数在 /v2:每个引擎仅接受其文档中规定的字段。

429 Too Many Requests 表示您一次发送的请求过多。请降低并发量,并在重试前进行退避。 对于 500、502 和 503 错误,采用指数退避和完全抖动进行几次重试就足够了,而随机的延迟可以防止一大批工作者同步重试。Python 客户端中的 search() Python 客户端中的 `retry_function` 实现了这一完整策略:遇到 401 状态码时停止;对可重试的状态码采用带抖动延迟的重试;对 422 状态码重试一次;尝试四次后放弃。

为追踪成本,每次调用时记录两个响应头: Searchdata-Call-Credit-Usage (本次调用消耗的积分) 和 Searchdata-Current-Credit-Usage (当前账户使用量)。客户端将它们作为 usage的形式返回。记录状态码、头部字段和请求上下文,但绝不记录键值。

在指标中将 HTTP 失败与空模块区分开来。如果 200 响应中没有 related[] block 的 200 响应表示搜索成功,不应触发重试。

文档:错误与标头

SERP API 的费用是多少?

Google 的套餐按每月搜索量划分:入门版 28 美元可进行 10,000 次搜索,增长版 130 美元可进行 50,000 次搜索,企业版 550 美元可进行 250,000 次搜索,专业版 1,600 美元可进行 1,000,000 次搜索。 试用版包含 100 次 Google 搜索,费用为 0 美元。截至 2026 年 10 月,Bing、DuckDuckGo 和 Yandex 的定价需根据您的工作负载进行报价。

套餐

每月价格

Google 搜索

每1,000次有效

试用

$0

100

—

入门版

28美元

10,000

2.80美元

成长

130美元

50,000

2.60美元

商业

550美元

250,000

2.20美元

专业版

1,600美元

1,000,000

1.60美元

必应、DuckDuckGo、Yandex

报价

基于工作负载

—

价格以美元计,截至2026年10月。所有套餐(包括试用版)均包含Google产品数据、全球位置、搜索结果页面(SERP)解析、批量处理、所有设备支持、JSON、CSV和HTML输出以及图片搜索功能。各套餐的区别在于每月搜索配额。

要确定合适的套餐,请将市场网格中的各项乘积相加:

每月搜索量 = 关键词 × 市场 × 设备 × 结果类型 × 每月刷新次数

然后为测试以及后续添加的关键词预留余量。例如,500 个关键词 × 2 个国家/地区 × 2 种设备 × 每周 4 次刷新 = 每月 8,000 次搜索,这完全符合“入门版”的配额,且仍有余量。 若将同一项目改为每日运行,搜索量将增至约 60,000 次,此时需升级至“商务”套餐。刷新频率会使其他所有参数的数值成倍增加,因此请根据实际基于数据做出决策的频率来设定刷新频率。

在确定具体使用量之前,请利用试用期验证您的代表性查询、市场、设备和输出字段。如果您还在权衡其他供应商,请比较各提供商的 SERP API 定价情况。

SERP API 无法实现哪些功能?何时应选用其他工具?

SERP API 返回的是搜索结果页面,而非每个链接背后的页面,且不提供统一的排名。DuckDuckGo 和 Yandex 的数据提取需由您的代码完成,异步功能仅限 Google,而 Google AI 模式的答案属于另一款独立产品。 当这些限制对您重要时,请使用 Scraper API、Data API 或托管数据源。

  • 该 API 仅返回搜索结果页面。若需获取链接背后的页面,请使用 Scraper API。了解如何使用 Scraper API。
  • AI 模式是独立的。若需获取 Google AI 模式的答案、引用和品牌提及,请使用 Data API 中的 Google AI 模式数据。
  • 这是按需请求,而非数据源。如果您需要定期接收受管控的记录(而非自行发起调用),请查看搜索结果数据源。
  • 仅支持列出的搜索引擎。请基于参考文档中的十一种搜索引擎进行开发,而非过时的文档页面。
  • HTML 搜索引擎需要维护。当 DuckDuckGo 和 Yandex 的标记发生变化时,您需要自行维护相应的选择器。
  • 异步功能仅限 Google。针对 Bing、DuckDuckGo 和 Yandex,请规划同步 worker。
  • 一个结果即一次观察。排名描述的是单次查询、特定市场、特定设备及特定时刻的状况。结果缺失是否具有商业影响由您自行判断,而非由 API 决定。
  • 使用受限。请按照 WebScrapingAPI 的《服务协议》以及适用于您的要求,收集符合条件的公开结果。

常见问题

什么是 SERP API?

SERP API 是一种网络服务,它会为您执行搜索,并将搜索引擎结果页面作为数据返回。 您需提供查询词、搜索引擎以及国家、语言和设备等上下文信息。WebScrapingAPI的SERP API会返回针对Google和Bing的解析后JSON数据,以及针对DuckDuckGo和Yandex的封装在JSON容器中的HTML数据。

如何使用 SERP API?

将您的 API 密钥存储在环境变量中,然后向 https://serpapi.webscrapingapi.com/v2 发送一个包含 api_key、搜索引擎以及查询内容的 GET 请求。 添加市场控制参数(例如 Google 的 gl 和 hl),从 JSON 响应中读取自然搜索结果,并将每行数据连同其查询、市场、设备和捕获时间一起保存。

如何获取 SERP API 密钥?

注册一个 WebScrapingAPI 账户,然后在仪表盘中订阅 SERP API 以获取您的密钥。试用版包含 100 次 Google 搜索。请将密钥保存在服务器上的环境变量或密钥管理器中,切勿将其放入浏览器代码、公开仓库、日志或截图中。

SERP API 有免费试用吗?

是的。SERP API 试用版费用为 0 美元,包含 100 次 Google 搜索,足以让您在选择套餐前测试关键词、市场、设备和输出结果。 付费版 Google 套餐起价为每月 28 美元,包含 10,000 次搜索。Bing、DuckDuckGo 和 Yandex 的价格将根据您的具体工作负载进行报价。

什么是 SERP 抓取?

SERP 抓取是指收集搜索引擎针对某个查询显示的结果,例如标题、链接、排名位置以及“相关搜索”等功能。若自行操作,则需处理内容被屏蔽和布局变化等问题。而 SERP API 将这些工作交由服务商处理,因此您的代码只需发送请求,即可接收数据形式的搜索结果。

我可以使用 Python 调用 SERP API 吗?

可以。安装 Requests 模块,从环境变量中读取密钥,并在 params 参数中传入 api_key、engine 和 q,以便 Requests 对其进行编码。 将超时时间设置为约 120 秒,对 429 和 5xx 响应采用退避策略进行重试,并从 Google 或 Bing 的 JSON 响应中读取自然搜索结果列表。

SERP API 支持哪些搜索引擎?

它支持 Google、Bing、DuckDuckGo 和 Yandex。Google 还提供用于异步搜索、地图、地图评论、酒店、航班、趋势和反向图像搜索的专用引擎。 Google 和 Bing 返回已解析的 JSON。DuckDuckGo 和 Yandex 则将结果页面的 HTML 封装在 JSON 外壳中返回,需由您的应用程序进行解析。

SERP API 能破解 CAPTCHA 验证码吗?

没有任何 SERP API 页面会做出此类承诺。WebScrapingAPI 负责处理每次请求背后的搜索访问基础设施,当搜索引擎返回验证页面、错误状态或超时时,API 会返回 422 网站错误,而非传递错误数据。如果失败看起来是暂时的,请重试一次,然后进行日志记录。

为什么 DuckDuckGo 和 Yandex 返回的是 HTML 而不是已解析的结果?

对于这两款搜索引擎,SERP API 会将原始结果页面作为 HTML 封装在 JSON 结构中返回,而非解析后的集合。您的应用程序可自行选择要提取的内容。请检查实际响应,为其绑定 CSS 选择器,对选择器进行版本管理,保留原始 HTML,并在布局变更导致选择器不再匹配时发出警报。

如何使用 SERP API 跟踪关键词排名?

选择一组固定的关键词、国家/地区、语言和设备,然后按计划进行查询。保存每个自然搜索结果的排名、标题和链接,并记录捕获时间及市场。仅在同一市场和设备内比较不同运行结果,并将缺失的 SERP 功能视为一种观察结果,而非错误。

从 100 次免费 Google 搜索开始

SERP API 将 Google 和 Bing 的结果以解析后的 JSON 格式返回,而 DuckDuckGo 和 Yandex 的结果页面则以 JSON 封装的 HTML 格式返回,每次请求均可控制市场、语言和设备参数。当您确定了关键词、目标市场和刷新计划后,请对比各款 SERP API 套餐,选择适合您的配额方案。

关于作者

Suciu Dan, 联合创始人 @ WebScrapingAPI

Suciu Dan

联合创始人

Suciu Dan 是 WebScrapingAPI 的联合创始人,他撰写了关于 Python 网页抓取、Ruby 网页抓取以及代理基础设施的实用指南,这些指南专为开发者而设计。

开始构建

准备好扩展您的数据收集规模了吗?

加入2,000多家企业,使用WebScrapingAPI在无需任何基础设施开销的情况下,以企业级规模提取网络数据。