Google 的"People Also Ask"(PAA)模块出现在大量搜索结果中,位于自然结果的正上方或中间,以一组可展开的问答对形式呈现。对于做内容研究或 SEO 的人来说,这是结果页面上最直接的用户意图信号之一:用户使用的确切措辞、他们追问的后续问题,以及竞争对手尚未覆盖的空白。点击展开一个问题,Google 会在其下方加载更多相关问题,因此一个查询词就能演变出一棵完整的意图树。

本指南将向你展示如何以可靠、可重复的方式用 Python 抓取 Google People Also Ask。你将构建一个小而可运行的爬虫,通过 Crawling API 获取已渲染的 Google SERP,解析 PAA 问题和答案,展开嵌套条目以捕获大多数爬虫遗漏的深层内容,并将干净的问答对导出为 JSON。整个教程的范围严格限定在任何人无需账号即可看到的公开搜索结果数据,结尾处的合法性章节并非套话,请在将其指向真实流量之前认真阅读。

你将构建什么

一个 Python 脚本,接受一个公开的 Google 搜索词,通过 Crawling API 获取已渲染的 HTML,并为页面上每一个 PAA 条目(包括展开第一层后揭示的问题)提取结构化记录。我们将以一个示例查询词作为贯穿全文的例子,并从每个 PAA 条目中提取以下字段:

  • 问题(Question),PAA 问题文本,与 Google 的措辞完全一致,这正是扩展关键词和话题覆盖面的来源。
  • 答案(Answer),条目展开后 Google 显示的摘要答案,适用于精选摘要研究。
  • 来源 URL(Source URL),Google 引用作为答案来源的页面,支持竞争分析。
  • 子条目(Children),展开该条目后揭示的嵌套问题,捕获扩展树的更深层级。

为什么普通请求无法获取 PAA

如果你用脚本向 Google 搜索 URL 发起普通 HTTP 请求,PAA 部分通常不会出现。有两个因素对你不利。第一,PAA 框由 JavaScript 在页面初始化后渲染,并在每次点击问题时再次更新,因此普通请求返回的原始 HTML 要么完全没有这个框,要么只有一个空壳。第二,Google 会监控自动化流量:看起来不像真实浏览器的请求会被发起挑战、显示同意或验证页面,或在到达结果之前就被拦截。

因此,一个可用的 PAA 爬虫需要在一次请求中同时满足两点:平台视为真实访客的 IP,以及能真正渲染页面并执行脚本的浏览器。你可以自行用无头浏览器加轮换住宅代理池来搭建,但维护这套系统是大部分的工作量。Crawling API 将两者融合进一次调用:你发送搜索 URL,它从可信的住宅 IP 获取页面,在真实浏览器中渲染,等待动态内容加载完成,然后把处理好的 HTML 返回给你解析。

渲染才是关键

PAA 内容在页面初始化后才加载,并随交互而变化,所以未经渲染的请求会返回不完整或空白的模块。渲染页面,给脚本稍加等待时间,PAA 部分就会出现在 HTML 中。Crawling API 在服务端完成渲染和 IP 轮换,你可以先用最多 20,000 次免费请求开始,无需信用卡。

前提条件

在编写任何代码之前,你需要准备几样东西,每样都不费时。

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

Python 3.8 或更高版本。python --version 确认你的版本。如果尚未安装,从 python.org 或 Anaconda 等发行版安装。

Crawlbase 账户和 JavaScript token。注册后打开控制台,从账户文档页面复制你的 token。Google SERP 需要渲染,因此请使用你的 JavaScript token(也称为启用浏览器的密钥),而非普通 token。最多 20,000 次免费请求:注册赠送 1,000 次,完成引导步骤即可获得更多。请像对待密码一样保管 token:它用于验证你的请求,因此不要将其提交到版本控制中。

设置项目

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

bash
python --version

python -m venv paa_env
source paa_env/bin/activate

pip install requests beautifulsoup4

Windows 用户请用 paa_env\Scripts\activate 替换 source 那行来激活环境。两个依赖各司其职:requests 向 Crawling API 发送 HTTP 调用,beautifulsoup4 解析返回的 HTML,让你通过 CSS 选择器提取各个字段。

第一步:通过 Crawling API 获取已渲染的 SERP

首先获取已渲染的 HTML。编写一个小型 crawl() 函数,将你的 Google 搜索 URL 发送到 Crawling API,附带你的 JavaScript token,请求渲染并等待,检查底层页面是否以 200 状态返回,然后返回 HTML 正文。URL 中的 glhl 参数设置国家和语言,page_wait 为 PAA 脚本执行完成留出时间,之后才捕获 HTML。

python
import json
import requests
from urllib.parse import urlencode

JS_TOKEN = "YOUR_CRAWLBASE_TOKEN"  # use your JavaScript token
API_ENDPOINT = "https://api.crawlbase.com/"

def build_serp_url(query, gl="us", hl="en"):
    base = "https://www.google.com/search?"
    return base + urlencode({"q": query, "gl": gl, "hl": hl})

def crawl(url, page_wait=2000):
    params = {
        "token": JS_TOKEN,
        "url": url,
        "page_wait": page_wait,
    }
    response = requests.get(API_ENDPOINT, params=params, timeout=90)
    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 = build_serp_url("how to scrape google", gl="us", hl="en")
    html = crawl(url)
    print(html[:500])

API 返回一个 JSON 信封,因此你用 json.loads 加载响应并读取两个字段:original_status 是 Google 本身返回的状态码,body 是已渲染的页面 HTML。对 original_status 进行守卫判断,意味着同意门或封锁会以异常形式出现,而不是将无效内容喂给解析器。约 2000 毫秒的 page_wait 通常足够 PAA 模块加载完成;90 秒的请求超时给渲染留有余裕,不至于让调用永远挂起。运行 python crawling.py,你应当能在前 500 个字符中看到真实的 SERP 标记,在编写任何选择器之前,这能确认抓取和渲染已正常工作。如果 PAA 部分看起来内容不足,首先尝试增大 page_wait

Crawlbase Google Scraper

那个 page_wait 能发挥作用,是因为请求首先以真实浏览器在可信 IP 上到达了 Google。Crawling API 从轮换的住宅地址获取 SERP,在真实的无头浏览器中渲染页面,等待 PAA 脚本稳定后才将处理好的 HTML 交给你,省去了自行运营无头浏览器集群和住宅代理池的麻烦。先在免费层用一个公开搜索 URL 测试。

第二步:解析 PAA 问题和答案

拿到已渲染的 HTML 后,将其加载到 BeautifulSoup 中并提取每个 PAA 条目。Google 没有为 PAA 模块提供一个稳定的单一 class 名称,因此可靠的方式是分层回退选择器:每个 PAA 条目都带有一个 data-q 属性存放问题,可见答案位于其旁边的可展开块中。下面的解析器从 data-q 读取问题,从已渲染文本读取答案,从条目中的第一个外部锚点读取引用来源。

python
from bs4 import BeautifulSoup

# Layered fallbacks: Google rotates these class names, so try
# the stable data-q attribute first, then known container classes.
PAA_ITEM_SELECTORS = [
    "div[data-q]",
    "div.related-question-pair",
    "div[jsname='Cpkphb']",
]

def find_paa_items(soup):
    for selector in PAA_ITEM_SELECTORS:
        items = soup.select(selector)
        if items:
            return items
    return []

def parse_paa(html):
    soup = BeautifulSoup(html, "html.parser")
    questions = []

    for item in find_paa_items(soup):
        question = item.get("data-q")
        if not question:
            heading = item.select_one("div[role='heading'], span")
            question = heading.get_text(strip=True) if heading else None
        if not question:
            continue

        answer_el = item.select_one("div[data-attrid], div.wDYxhc, span.hgKElc")
        answer = answer_el.get_text(" ", strip=True) if answer_el else None

        link = item.select_one("a[href^='http']")
        source_url = link["href"] if link else None

        questions.append({
            "question": question,
            "answer": answer,
            "source_url": source_url,
            "children": [],
        })

    return questions

记录结构完全遵循设计规范:question 扩展关键词覆盖面,answer 辅助精选摘要研究,source_url 支持竞争分析,children 留给下一步捕获的嵌套展开内容。优先从稳定的 data-q 属性读取问题,再回退到标题文本,正是这种方式让解析器在 Google 重排 class 名称后仍能正常工作。if not question: continue 守卫语句会跳过空壳,确保只有真实的 PAA 条目进入输出。

选择器会漂移

Google 会轮换其 SERP 标记中的混淆 class 名称,今天有效的选择器下个月可能返回空结果。这正是解析器优先尝试稳定的 data-q 属性而非任何 class 名称的原因,并维护一份回退列表。当每个 PAA 字段都返回空时,在浏览器开发者工具中重新检查实时结果页面并更新列表。记录每次运行时触发了哪个选择器,这样匹配数量突然下降就容易发现。

第三步:展开嵌套 PAA 条目

到目前为止,你只提取了初始的 PAA 问题集。这固然有用,但并不完整:真正的价值在扩展树的更深处。当用户点击一个 PAA 问题时,Google 会动态加载二到四个更多相关问题于其下方,而每一个都可能触发进一步的展开。要捕获这些内容,你需要告诉 Crawling API 在捕获 HTML 之前模拟点击,使用 css_click_selector 参数让额外的问题加载到 DOM 中,再进行解析。

python
def crawl_expanded(url, page_wait=3000):
    # css_click_selector clicks each PAA question so Google loads
    # the nested questions before the HTML is captured.
    params = {
        "token": JS_TOKEN,
        "url": url,
        "page_wait": page_wait,
        "css_click_selector": "div[data-q]",
    }
    response = requests.get(API_ENDPOINT, params=params, timeout=90)
    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 scrape_with_expansions(query, gl="us", hl="en"):
    url = build_serp_url(query, gl, hl)

    # First pass: the visible PAA questions.
    base_items = parse_paa(crawl(url))
    seen = {item["question"] for item in base_items}

    # Second pass: click to load the nested questions, then diff.
    expanded_items = parse_paa(crawl_expanded(url))
    for item in expanded_items:
        if item["question"] not in seen:
            base_items[0]["children"].append(item)
            seen.add(item["question"])

    return base_items

流程是:用查询词和地理参数构建 SERP URL,先获取一次可见的 PAA 集合,然后将 css_click_selector 设置为 PAA 条目选择器再获取一次,让 API 点击每个问题,将新问题加载到 DOM 中。解析两轮数据并只保留尚未出现的问题,就能获得更深的层级,同时不产生重复。实际操作中,一个查询词在经过几轮展开后,可以从三四个可见问题扩展到十二到二十个。这一步从实现角度来说是可选的,但这里才是大部分遗漏价值所在。

第四步:整合代码并导出问答对

现在将构建、获取、展开和解析串联成一个可运行的脚本,将结构化的 PAA 数据写入 JSON。设置 ensure_ascii=False 可以让非 ASCII 字符在文件中保持可读,而不是被转义为 \u 序列,一旦你用其他语言运行查询时,这一点就很重要。

python
import sys

def main():
    query = sys.argv[1] if len(sys.argv) > 1 else "how to scrape google"
    country = sys.argv[2] if len(sys.argv) > 2 else "us"

    paa = scrape_with_expansions(query, gl=country)

    outfile = f"paa_{country}.json"
    with open(outfile, "w", encoding="utf-8") as f:
        json.dump(paa, f, ensure_ascii=False, indent=2)

    total = len(paa) + sum(len(q["children"]) for q in paa)
    print(f"Saved {total} PAA questions to {outfile}")

if __name__ == "__main__":
    main()

python main.py "content gap analysis" uk 运行完整脚本。它为该查询词在所选国家构建 Google SERP URL,获取已渲染的 HTML,展开 PAA 条目,并将问答对写入 paa_uk.json。只需这几个函数就足够了:换一个查询词或国家代码,解析器就能处理返回的内容。如果结果看起来不完整,在做其他调整之前先增大 page_wait,因为渲染缓慢是 PAA 列表过短的最常见原因。

输出结果示例

你会得到一个干净的问题对象列表,每个对象包含答案、引用来源,以及展开时捕获的嵌套问题,可直接写入 JSON、填充内容简报,或加载到数据库中进行聚类分析。

json
[
  {
    "question": "Is it legal to scrape Google?",
    "answer": "Scraping public search results is generally permitted, but it can conflict with Google's terms of service.",
    "source_url": "https://example.com/is-scraping-google-legal",
    "children": [
      {
        "question": "Can Google detect scraping?",
        "answer": "Yes, Google uses rate limits and behavioral signals to flag automated traffic.",
        "source_url": "https://example.com/google-bot-detection",
        "children": []
      }
    ]
  },
  {
    "question": "What is the best tool to scrape Google?",
    "answer": "A rendering API that handles proxies and JavaScript is the most reliable approach.",
    "source_url": "https://example.com/google-scraping-tools",
    "children": []
  }
]

每个问题都成为一个节点,每次展开都在其 children 下添加更多节点。从这里开始,导出为 CSV 用于电子表格,或将树结构展平为内容简报,只需几行代码。由于每条记录都带有来源 URL,你还可以按 Google 引用的域名对问题进行分组,看看谁已经拥有这些答案。

跨国家比较 PAA

PAA 结果并不通用:它们因地域和语言而异,因为 Google 会根据搜索者所在市场进行个性化定制。要进行比较,用不同的 gl 值运行相同的查询词,然后对比结果。

python
import time

query = "best running shoes"
markets = ["us", "uk", "de"]

by_market = {}
for gl in markets:
    items = scrape_with_expansions(query, gl=gl)
    by_market[gl] = {q["question"] for q in items}
    time.sleep(3)

# Questions unique to the UK market.
uk_only = by_market["uk"] - by_market["us"]
print(f"UK-only PAA questions: {len(uk_only)}")

比较各市场的独特问题、重叠话题和答案差异,在拓展新地区或本地化内容时尤为有用。请求之间的 time.sleep 可以控制节奏,避免连续密集发请求。如果需要扩展到远超几个查询词的规模,异步 Crawler 允许你批量推送 URL,并通过 webhook 接收结果,无需等待每次调用返回。

保持不被封锁

即使渲染和可信 IP 已为你处理好,Google 仍会监控爬虫形态的流量。以下几个习惯有助于保持运行健康。

  • 控制请求节奏。在紧密循环中密集请求 SERP 是最快被发起挑战的方式。分散请求,变换查询词,而不是全速翻同一个词的分页。
  • 依赖轮换。将请求分散到众多真实用户 IP 上,避免单个地址触发速率限制。Crawling API 替你处理这个问题;如果你自行搭建,这是最重要的环节。
  • 关注状态和数量。运行开始返回挑战,或 PAA 数量降至零,说明当前速率、IP 层级或选择器需要关注。将此视为信号加以处理,而非忽略。
  • 字段为空时重新检查。Google 会定期重排其标记结构。如果 PAA 解析停止返回条目,在开发者工具中打开实时页面并更新选择器列表。

更全面的操作指南请参阅如何不被封锁地抓取网站。如果你想了解 PAA 以外所有 SERP 功能的完整视图,我们的 Google 搜索页面抓取指南涵盖了自然结果、广告和知识面板,而 Python 抓取 Google 搜索结果实战指南则是本文的配套操作教程。拿到 PAA 数据后,提取和分析 Google SEO 数据以及利用抓取数据提升 SEO将告诉你如何进一步应用这些数据。

抓取 Google PAA 是否合法?

抓取 Google 的 People Also Ask 数据是否被允许,取决于 Google 的服务条款、你所在的司法管辖区以及你对数据的使用方式。抓取公开可见的搜索结果处于法律灰色地带:PAA 模块中的问题和答案无需账号即可看到,但 Google 的条款对自动化访问有所限制,因此无论工具多么谨慎,爬虫都可能违反这些条款。此处的代码并不改变这一点,只是让技术层面运作起来。请阅读 Google 的条款和 robots.txt,将两者视为你收集内容的边界。

以下几条值得坚守。仅收集公开 PAA 数据:无需登录即可在结果页面上看到的问题、答案和引用来源 URL。将请求量控制在不给 Google 服务器造成负担的范围内,节奏抓取而非全速运行。不要收集个人数据,不要大量转载受版权保护的答案文本,不要触碰任何需要登录的内容。引用来源 URL 指向他人的页面,请按照该页面自身的许可协议对待其内容,而非视之为你可以转发的内容。

如果存在官方途径,请优先选择。Google 提供了 Programmable Search 和其他官方 API 用于获得授权的搜索数据访问,在生产规模的需求下,官方数据协议才是正确路径,而不是更聪明的爬虫。本指南故意将范围限定在公开 PAA 页面,因为这是让工作保持可辩护的边界:任何人都能看到的问答对,用于研究和内容规划,仅此而已。

回顾

核心要点

  • PAA 需要渲染。该模块在页面初始化后通过 JavaScript 加载,普通请求返回空白或缺失的部分;渲染页面并给予短暂等待时间。
  • Crawling API 在真实 IP 后方渲染。发送带有 JavaScript token 和 page_wait 的搜索 URL,它会轮换住宅 IP、运行真实浏览器并返回处理好的 HTML。
  • 用分层回退解析。优先从稳定的 data-q 属性读取问题,再回退到 class 名称,因为 Google 会轮换其混淆标记。
  • 用 css_click_selector 展开树结构。点击可见问题让 Google 加载嵌套问题,然后对比两轮数据,从三四个问题扩展到十二到二十个。
  • 仅限公开数据。遵守 Google 的服务条款和 robots.txt,控制请求节奏,大规模使用时优先选择官方 API,不要触碰登录功能或个人数据。

常见问题

什么是 People Also Ask 模块?

PAA 模块是 Google SERP 中的一个功能,显示一组与搜索词相关的可展开问答对。它出现在大量搜索结果中,每次点击都会动态加载两到四个更多相关问题,正是这种机制使其成为 SEO 和内容研究的丰富用户意图数据来源。

为什么普通请求会遗漏 PAA 部分?

PAA 模块由 JavaScript 在初始 HTML 加载后渲染,并随交互而再次更新,因此普通 HTTP 请求返回的是空壳或完全没有该模块。通过带有 JavaScript token 的 Crawling API 获取页面,会在真实浏览器中渲染页面并等待脚本执行,从而确保你解析的 HTML 中存在 PAA 内容。

如何捕获嵌套的 PAA 问题?

使用 css_click_selector 参数让 API 在捕获 HTML 之前点击每个可见的 PAA 问题,促使 Google 将相关问题加载到 DOM 中。解析展开后的 HTML 并与第一轮数据对比,将新问题收集到每个条目的 children 下。三层展开通常每个查询词可获得十二到二十个问题。

能用 Python 抓取 Google PAA 吗?

可以。使用 requests 和 BeautifulSoup,你可以获取已渲染的 SERP,并从每个 PAA 条目中提取问题、答案、来源 URL 和嵌套子条目。Crawling API 是让你的请求以可信 IP 带渲染到达 Google 的桥梁。更广泛的入门内容请参阅我们的Python 网页抓取指南

为什么 PAA 因国家而异?

Google 根据搜索者所在国家和语言对 PAA 结果进行个性化定制,因此相同查询词在美国和英国往往返回不同的问题,因为两个市场的用户行为、语言习惯和可用内容各不相同。用不同的 gl 值运行相同查询词并对比问题集,可以看出差异,这在进行内容本地化时很有价值。

我的选择器返回空结果。发生了什么?

几乎可以肯定是 Google 的标记发生了变化。Google 会轮换其 SERP 中的混淆 class 名称,上个月有效的选择器可能已经失效,解析器会无声地返回空列表。优先从稳定的 data-q 属性读取问题,维护一份回退选择器列表,记录每次运行触发了哪个选择器,并在数量下降时重新检查实时页面的开发者工具。

开始构建

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

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

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