Google People Also AskとAutocompleteのスクレイピングでキーワード調査(Pythonガイド)

Google Autocomplete、People Also Ask、Related Searchesの3つの無料キーワード源をPythonでスクレイピングし、プロキシでブロック回避しながらロングテールキーワードを大量抽出する実践ガイド。

Scrape Google People Also Ask and Autocomplete for Keyword Research (Python Guide)
この記事の内容

Google People Also AskとAutocompleteのスクレイピングでキーワード調査を行う理由

SEOキーワード調査で最もコストがかかるのは、実際の検索意図を含むロングテールキーワードの発見です。有料のキーワードツールは便利ですが、月額$50〜$300のコストがかかり、APIレート制限に縛られます。一方、Google自身が無料で提供している3つのデータ源 — Autocompleteサジェスト、People Also Ask(PAA)、Related Searches — は、実際のユーザー検索行動に直接基づくキーワードデータの「金脈」です。

本記事では、Google People Also AskとAutocompleteのスクレイピングでキーワード調査を行うPython実装を解説します。1つのシードキーワードから数百のロングテール候補を生成するエクスパンダー、PAAの再帰展開、レジデンシャルプロキシによるブロック回避、そしてCSV出力までを完全にカバーします。

3つの無料キーワード源と検索意図のマッピング

Googleは検索体験の中で3つの異なるサジェストメカニズムを提供しており、それぞれが異なる検索意図のレイヤーをカバーします。

データ源エンドポイント / DOM検索意図の種類キーワードの性質
Autocompletesuggestqueries.google.com/complete/search情報検索・トランザクショナル入力途中のクエリ(ロングテール志向)
People Also AskSERP内のPAAアコーディオン質問型(情報検索)「〜とは」「〜方法」「〜違い」等
Related SearchesSERP下部横展開・類義同義語・関連トピック

Autocompleteサジェストエンドポイント

Google Chromeのアドレスバー裏で使われているAutocomplete APIは、client=chromeパラメータでJSONレスポンスを返します。エンドポイントは https://suggestqueries.google.com/complete/search?client=chrome&q=KEYWORD で、レスポンスは配列の配列形式で返ります。これは最も軽量に取得できるキーワード源で、1リクエストあたり通常10件前後のサジェストが得られます。

People Also Askの構造

PAAはSERP内にアコーディオン形式で表示される質問リストで、初期状態では3〜4件が表示されています。各質問をクリック(展開)すると回答と共に追加の質問が動的に挿入されるため、再帰的に展開することで数十件の質問キーワードを収集できます。この動的レンダリングがPlaywright等のブラウザ自動化を必要する理由です。

SERP下部のRelated Searchesは、シードキーワードの類義語や関連トピックを提供します。AutocompleteやPAAと組み合わせることで、キーワードクラスター全体を俯瞰できます。

なぜプロキシが必要なのか — 技術的背景

Autocomplete APIとPAAスクレイピングには2つの大きな障壁があります。

1. IPベースのレート制限: Googleは同一IPからの高頻度リクエストを検出し、429ステータスやCAPTCHAチャレンジで応答します。実測では、データセンターIPからAutocompleteを毎秒5リクエスト以上送ると数分以内に制限がかかります。

2. ロケールバイアス: Autocompleteのサジェスト結果はリクエスト元IPの地理的位置に大きく依存します。例えば「pizza」のサジェストは、US IPからは「pizza hut」「pizza near me」が多く、DE IPからは「pizza lieferung」「pizza bestellen」が上位に来ます。正確な地域キーワードを取得するには、対象国のIPが必要です。

これが、SERPトラッキングやキーワード調査でレジデンシャルプロキシが標準装備となる理由です。Google Search Centralのドキュメントでも、検索結果の地域差について言及されています。

シードキーワードからロングテールを展開するエクスパンダー

1つのキーワードから数百の候補を生成する基本戦略は、a〜zプレフィックス修飾語プレフィックスを組み合わせることです。例えば「proxy」をシードにする場合、「proxy a」「proxy b」...「proxy z」と26回Autocompleteを叩き、さらに「best proxy」「cheap proxy」「free proxy」などの修飾語を前置・後置します。

import itertools
import string

def generate_prefixes(seed: str) -> list[str]:
    prefixes = []
    # a-z の1文字プレフィックス
    for c in string.ascii_lowercase:
        prefixes.append(f"{seed} {c}")
    # 修飾語プレフィックス
    modifiers = [
        "best", "cheap", "free", "top", "how to", "what is",
        "buy", "review", "alternative", "vs", "near me", "2025",
        "for beginners", "guide", "tutorial", "price",
    ]
    for mod in modifiers:
        prefixes.append(f"{mod} {seed}")
        prefixes.append(f"{seed} {mod}")
    # 2文字組み合わせ(上位のみ)
    for c1, c2 in itertools.product("abcdefghi", repeat=2):
        prefixes.append(f"{seed} {c1}{c2}")
    return prefixes

# 使用例
seed = "proxy"
queries = generate_prefixes(seed)
print(f"生成クエリ数: {len(queries)}")  # 26 + 32 + 81 = 139

139個のクエリに対し、それぞれAutocompleteを呼べば1,000件以上のロングテール候補が得られます。重複除去後でも数百件のユニークキーワードが残ります。

ProxyHatプロキシの設定とAutocompleteスクレイピング

ProxyHatのレジデンシャルプロキシを使えば、国・都市レベルのジオターゲティングが可能です。ユーザー名に country-DE-city-berlin のようなフラグを埋め込むことで、ベルリンのIPからAutocompleteを叩き、ドイツ語ローカルのサジェストを取得できます。

import httpx
import json
import time
import random
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# ProxyHat接続設定
PROXYHAT_HTTP = "http://user-country-DE-city-berlin:PASSWORD@gate.proxyhat.com:8080"

AUTOCOMPLETE_URL = "https://suggestqueries.google.com/complete/search"

HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    "Accept-Language": "de-DE,de;q=0.9,en;q=0.8",
}

def fetch_autocomplete(
    query: str,
    client: httpx.Client,
    max_retries: int = 3,
) -> list[str]:
    params = {
        "client": "chrome",
        "q": query,
        "hl": "de",
        "gl": "de",
    }
    for attempt in range(max_retries):
        try:
            resp = client.get(
                AUTOCOMPLETE_URL,
                params=params,
                headers=HEADERS,
                timeout=10.0,
            )
            if resp.status_code == 429:
                wait = 2 ** attempt + random.uniform(0, 1)
                logger.warning(f"429 rate limited, waiting {wait:.1f}s")
                time.sleep(wait)
                continue
            resp.raise_for_status()
            data = resp.json()
            # data[1] がサジェスト文字列の配列
            suggestions = data[1] if len(data) > 1 else []
            return suggestions
        except (httpx.HTTPError, json.JSONDecodeError) as e:
            logger.error(f"Error for '{query}': {e}")
            time.sleep(2 ** attempt)
    return []

# メイン処理
queries = generate_prefixes("proxy")
all_suggestions = set()

with httpx.Client(proxy=PROXYHAT_HTTP) as client:
    for i, q in enumerate(queries):
        suggestions = fetch_autocomplete(q, client)
        all_suggestions.update(suggestions)
        logger.info(f"[{i+1}/{len(queries)}] '{q}' -> {len(suggestions)} suggestions")
        time.sleep(random.uniform(0.5, 1.5))  # 礼儀正しい間隔

print(f"総ユニークサジェスト: {len(all_suggestions)}")

hlglパラメータで言語と国を指定し、Accept-Languageヘッダーとプロキシのジオターゲティングを一致させることで、ローカライズされた正確なサジェストが得られます。

PlaywrightでPAAアコーディオンを再帰展開する

PAAの質問は初期状態で3〜4件しか表示されませんが、各質問をクリックして展開すると、回答と共に新しい質問が動的に追加されます。これを再帰的に繰り返すことで、1つのSERPから数十件の質問キーワードを抽出できます。

from playwright.sync_api import sync_playwright
import time
import json
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

PROXYHAT_SOCKS5 = "socks5://user-country-US:PASSWORD@gate.proxyhat.com:1080"

def scrape_paa(seed_keyword: str, max_depth: int = 5) -> list[dict]:
    results = []
    seen_questions = set()

    with sync_playwright() as p:
        browser = p.chromium.launch(
            headless=True,
            proxy={
                "server": PROXYHAT_SOCKS5,
            },
        )
        context = browser.new_context(
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
            locale="en-US",
            viewport={"width": 1280, "height": 800},
        )
        page = context.new_page()

        url = f"https://www.google.com/search?q={seed_keyword}"
        page.goto(url, wait_until="domcontentloaded", timeout=30000)
        time.sleep(2)

        for depth in range(max_depth):
            # PAAの質問要素を取得
            paa_questions = page.query_selector_all("div[jsname] .w1C3Le")
            if not paa_questions:
                # フォールバック: 別のセレクタを試す
                paa_questions = page.query_selector_all("[jscontroller] .related-question")

            new_count = 0
            for q_elem in paa_questions:
                try:
                    question_text = q_elem.inner_text().strip()
                    if question_text and question_text not in seen_questions:
                        seen_questions.add(question_text)
                        # クリックして展開
                        q_elem.click()
                        time.sleep(1.0)

                        # 展開後に引用元を取得
                        answer_sources = page.query_selector_all("[data-ved] a")
                        sources = []
                        for src in answer_sources[:3]:
                            href = src.get_attribute("href")
                            text = src.inner_text().strip()
                            if href and text:
                                sources.append({"text": text, "url": href})

                        results.append({
                            "question": question_text,
                            "depth": depth,
                            "sources": sources,
                        })
                        new_count += 1
                        logger.info(f"[depth={depth}] {question_text[:60]}")
                except Exception as e:
                    logger.warning(f"要素の処理でエラー: {e}")
                    continue

            if new_count == 0:
                logger.info(f"depth={depth} で新規質問なし、終了")
                break

            time.sleep(random.uniform(1.0, 2.0))

        browser.close()
    return results

# 実行
paa_results = scrape_paa("proxy server", max_depth=5)
print(f"取得PAA質問数: {len(paa_results)}")
for r in paa_results[:5]:
    print(f"  Q: {r['question']}")

このスクリプトは各PAA質問をクリック展開し、回答の引用元URLも同時に収集します。引用元はコンテンツ作成時の参考リソースとしても有用です。セレクタはGoogleが頻繁に変更するため、定期的なメンテナンスが必要です。

非同期でAutocompleteとPAAを並行収集

大量のキーワードを効率的に収集するには、httpx.AsyncClientasyncioで並行リクエストを行います。ただし、同時接続数は制御しないとGoogleのレート制限に即座にヒットします。

import asyncio
import httpx
import random
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

PROXYHAT_HTTP = "http://user-country-US:PASSWORD@gate.proxyhat.com:8080"

AUTOCOMPLETE_URL = "https://suggestqueries.google.com/complete/search"

HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    "Accept-Language": "en-US,en;q=0.9",
}

async def fetch_one(
    query: str,
    client: httpx.AsyncClient,
    semaphore: asyncio.Semaphore,
) -> list[str]:
    async with semaphore:
        await asyncio.sleep(random.uniform(0.3, 0.8))
        params = {"client": "chrome", "q": query}
        try:
            resp = await client.get(
                AUTOCOMPLETE_URL,
                params=params,
                headers=HEADERS,
                timeout=10.0,
            )
            if resp.status_code == 429:
                logger.warning(f"429 for '{query}'")
                return []
            data = resp.json()
            return data[1] if len(data) > 1 else []
        except Exception as e:
            logger.error(f"Error for '{query}': {e}")
            return []

async def batch_autocomplete(queries: list[str], max_concurrent: int = 5) -> dict[str, list[str]]:
    semaphore = asyncio.Semaphore(max_concurrent)
    results = {}

    async with httpx.AsyncClient(proxy=PROXYHAT_HTTP) as client:
        tasks = [fetch_one(q, client, semaphore) for q in queries]
        responses = await asyncio.gather(*tasks, return_exceptions=True)
        for q, res in zip(queries, responses):
            if isinstance(res, list):
                results[q] = res
    return results

# 実行
queries = generate_prefixes("vpn")[:50]  # 最初の50件
results = asyncio.run(batch_autocomplete(queries, max_concurrent=5))
total = sum(len(v) for v in results.values())
print(f"収集サジェスト総数: {total}")

セマフォで同時接続を5に制限し、各リクエスト間にランダムな遅延を入れることで、Googleのレート制限を回避しながら効率的に収集します。50クエリを処理するのに約15〜20秒かかります。

重複除去、意図クラスタリング、CSV出力

収集したキーワードはそのままでは重複やノイズが多いので、正規化してクラスタリングします。

import csv
import re
from collections import defaultdict

def normalize_keyword(kw: str) -> str:
    kw = kw.lower().strip()
    kw = re.sub(r"\s+", " ", kw)  # 連続スペースを1つに
    return kw

def classify_intent(kw: str) -> str:
    """簡易的な検索意図分類"""
    informational_patterns = [
        r"what is", r"how to", r"why", r"when", r"guide",
        r"tutorial", r"meaning", r"definition", r"vs", r"difference",
        r"とは", r"方法", r「理由",
    ]
    transactional_patterns = [
        r"buy", r"cheap", r"price", r"discount", r"deal",
        r"best", r"review", r"alternative", r"comparison",
        r"購入", r「価格", r「安い",
    ]
    navigational_patterns = [
        r"login", r"sign up", r"official", r"download",
        r"ログイン", r「登録」,
    ]
    for p in informational_patterns:
        if re.search(p, kw):
            return "informational"
    for p in transactional_patterns:
        if re.search(p, kw):
            return "transactional"
    for p in navigational_patterns:
        if re.search(p, kw):
            return "navigational"
    return "other"

def dedupe_and_cluster(suggestions: list[str], paa_questions: list[str]) -> list[dict]:
    all_kw = set()
    for s in suggestions:
        all_kw.add(normalize_keyword(s))
    for q in paa_questions:
        all_kw.add(normalize_keyword(q))

    clustered = defaultdict(list)
    for kw in sorted(all_kw):
        intent = classify_intent(kw)
        clustered[intent].append(kw)

    rows = []
    for intent, keywords in clustered.items():
        for kw in keywords:
            rows.append({
                "keyword": kw,
                "intent": intent,
                "word_count": len(kw.split()),
                "char_count": len(kw),
            })
    return rows

# 実行
all_suggestions = list(all_suggestions)  # 前のセクションで収集したもの
paa_questions = [r["question"] for r in paa_results]

rows = dedupe_and_cluster(all_suggestions, paa_questions)

with open("keywords.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["keyword", "intent", "word_count", "char_count"])
    writer.writeheader()
    writer.writerows(rows)

print(f"CSV出力完了: {len(rows)} 行")
for intent, kws in defaultdict(list, {r["intent"]: [] for r in rows}).items():
    count = sum(1 for r in rows if r["intent"] == intent)
    print(f"  {intent}: {count} 件")

出力されたCSVは、コンテンツ計画やFAQページ構築の基盤としてそのまま使えます。PAAの質問はFAQセクションに直接マッピングでき、Autocompleteのロングテールはブログ記事のトピック候補になります。

プロキシのローテーション戦略

ProxyHatではユーザー名にセッションIDを指定することで、スティッキーセッション(同一IPを維持)とローテーション(リクエストごとにIP変更)を切り替えられます。

import httpx
import random
import string

def get_rotating_proxy_url(country: str = "US") -> str:
    """リクエストごとに新しいIPを使う(ローテーション)"""
    session_id = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
    return f"http://user-country-{country}-session-{session_id}:PASSWORD@gate.proxyhat.com:8080"

def get_sticky_proxy_url(country: str = "US", city: str = "newyork", session: str = "mytask1") -> str:
    """同一IPを維持する(スティッキーセッション)"""
    return f"http://user-country-{country}-city-{city}-session-{session}:PASSWORD@gate.proxyhat.com:8080"

# ローテーション: Autocompleteの大量収集に最適
for query in queries[:10]:
    proxy_url = get_rotating_proxy_url(country="US")
    with httpx.Client(proxy=proxy_url) as client:
        suggestions = fetch_autocomplete(query, client)
        # 各リクエストが異なるIPから送信される

# スティッキー: PAAのページ遷移で同一IPを維持したい場合
paa_proxy = get_sticky_proxy_url(country="US", city="newyork", session="paa_task_001")
# 同一セッションIDを使い続けることで、Playwrightの全ページ遷移が同じIPになる

Autocompleteのようなステートレスなリクエストにはローテーション、PAAのようにページ遷移でセッションを維持したい場合にはスティッキーセッションを使い分けます。利用可能なロケーションは多数の国・都市に対応しています。

よくあるミスとエッジケース

  • セレクタの陳腐化: GoogleはDOM構造を頻繁に変更します。PAAセレクタは月に1〜2回の確認が必要です。複数のフォールバックセレクタを用意しましょう。
  • Accept-Languageの不整合: プロキシIPがドイツなのにAccept-Languageがen-USだと、Googleが混乱して不正確なサジェストを返すことがあります。プロキシの地理とヘッダーを一致させてください。
  • 高すぎる同時接続: セマフォなしで100リクエストを同時送信すると、数秒で429の嵐になります。5〜10同時接続が現実的な上限です。
  • PAAが表示されないキーワード: すべてのキーワードにPAAが表示されるわけではありません。ブランド名や非常にニッチなキーワードではPAAセクション自体が存在しない場合があります。
  • Autocompleteの空レスポンス: 特定のクエリに対して空配列が返ることがあります。これはGoogle側のフィルタリングによるもので、エラーではありません。

倫理的配慮とベストプラクティス

AutocompleteとPAAのデータは公開されており、ブラウザで誰でも閲覧できる情報です。ただし、プログラムでの大量収集には以下の配慮が必要です。

  • 適切なレート制限: 1IPあたり毎秒1〜2リクエスト以下を推奨します。GoogleのAPIガイドラインでも同様の推奨が示されています。
  • robots.txtの確認: suggestqueries.google.com/robots.txtを確認し、スクレイピングポリシーを遵守してください。
  • 公式APIの優先: 大規模な本番運用では、Google Custom Search JSON APIやKeyword Planner APIの使用を検討してください。スクレイピングは補完的アプローチとして使います。
  • データの利用目的: 収集したキーワードは自社のコンテンツ戦略やSEO調査に使用し、競合への攻撃的スクレイピングやスパム目的には使わないでください。

詳細なプロキシ設定についてはProxyHat公式ドキュメントを参照してください。料金についてはプラン一覧で確認できます。

Key Takeaways

  • Googleの3つの無料キーワード源 — Autocomplete、PAA、Related Searches — は、検索意図の異なるレイヤーをカバーする強力なキーワード調査データです。
  • a〜zプレフィックスと修飾語プレフィックスを組み合わせることで、1つのシードから数百のロングテールキーワードを生成できます。
  • PAAはアコーディオン展開で再帰的に質問が追加されるため、Playwright等のブラウザ自動化が必要です。
  • レジデンシャルプロキシで国・都市をジオターゲティングすることで、ローカライズされた正確なサジェストを取得できます。
  • リクエスト間隔を0.5〜1.5秒に保ち、セマフォで同時接続を5〜10に制限することで、429ブロックを回避できます。
  • 収集したキーワードは意図クラスタリングしてCSVに出力し、コンテンツ計画とFAQ構築に直接活用できます。

FAQ

Google People Also AskとAutocompleteのスクレイピングとは何ですか?

Google検索結果に表示される「People Also Ask(PAA)」セクションの質問と、検索窓のAutocompleteサジェストをプログラムで抽出する手法です。これらは実際のユーザー検索行動に基づくキーワードデータであり、SEOキーワード調査の無料データ源として非常に有用です。PythonのHTTPクライアントやブラウザ自動化ツールを組み合わせることで、1つのシードキーワードから数百のロングテール候補を生成できます。

プロキシユーザーにとってこのスクレイピングが重要なのはなぜですか?

GoogleのAutocompleteエンドポイントやPAAは、同一IPからの高頻度リクエストに対してレート制限やCAPTCHAを課します。また、サジェスト結果はリクエスト元IPの地理的ロケーションに依存するため、ローカライズされた正確なキーワードを取得するには、対象国・都市のIPを持つレジデンシャルプロキシが不可欠です。プロキシを使うことで、ブロックを回避しつつ地域ごとの検索意図を正確に収集できます。

このスクレイピングに最適なプロキシタイプは何ですか?

レジデンシャルプロキシが最適です。GoogleはデータセンターIPを容易に検出し制限をかけるため、実際のISPに属するレジデンシャルIPを使用することで成功率が大幅に向上します。また、国・都市レベルのジオターゲティング機能を持つプロキシを使えば、日本、ドイツ、アメリカなど対象地域のローカルサジェストを取得できます。モバイルプロキシもモバイル検索意図の収集に有効です。

スクレイピング時のブロックを回避するにはどうすればよいですか?

リクエスト間に適切な遅延を入れ、レジデンシャルプロキシでIPをローテーションし、ユーザーエージェントやAccept-Languageヘッダーを対象ロケールに合わせることが基本です。1IPあたりのリクエスト頻度を低く保ち、エラー時は指数バックオフで再試行します。大規模収集時は公式APIの使用を優先し、スクレイピングは補完的に使うのが安全なアプローチです。

よくある質問

Google People Also AskとAutocompleteのスクレイピングとは何ですか?

Google検索結果に表示される「People Also Ask(PAA)」セクションの質問と、検索窓のAutocompleteサジェストをプログラムで抽出する手法です。これらは実際のユーザー検索行動に基づくキーワードデータであり、SEOキーワード調査の無料データ源として非常に有用です。PythonのHTTPクライアントやブラウザ自動化ツールを組み合わせることで、1つのシードキーワードから数百のロングテール候補を生成できます。

プロキシユーザーにとってこのスクレイピングが重要なのはなぜですか?

GoogleのAutocompleteエンドポイントやPAAは、同一IPからの高頻度リクエストに対してレート制限やCAPTCHAを課します。また、サジェスト結果はリクエスト元IPの地理的ロケーションに依存するため、ローカライズされた正確なキーワードを取得するには、対象国・都市のIPを持つレジデンシャルプロキシが不可欠です。プロキシを使うことで、ブロックを回避しつつ地域ごとの検索意図を正確に収集できます。

このスクレイピングに最適なプロキシタイプは何ですか?

レジデンシャルプロキシが最適です。GoogleはデータセンターIPを容易に検出し制限をかけるため、実際のISPに属するレジデンシャルIPを使用することで成功率が大幅に向上します。また、国・都市レベルのジオターゲティング機能を持つプロキシを使えば、日本、ドイツ、アメリカなど対象地域のローカルサジェストを取得できます。モバイルプロキシもモバイル検索意図の収集に有効です。

スクレイピング時のブロックを回避するにはどうすればよいですか?

リクエスト間に適切な遅延を入れ、レジデンシャルプロキシでIPをローテーションし、ユーザーエージェントやAccept-Languageヘッダーを対象ロケールに合わせることが基本です。1IPあたりのリクエスト頻度を低く保ち、エラー時は指数バックオフで再試行します。大規模収集時は公式APIの使用を優先し、スクレイピングは補完的に使うのが安全なアプローチです。

始める準備はできましたか?

148か国以上の住宅用・ISP・モバイルプロキシ。無料アカウントを作成。

無料アカウントを作成
← ブログに戻る