Pythonでレジデンシャルプロキシを使ったGoogleランクトラッカーの構築:はじめに
SEOエンジニアとPython開発者にとって、キーワードごとの検索順位を日々追跡することは不可欠です。しかし、Googleは2025年9月にnum=100パラメータを廃止し、TLS/JA3-JA4フィンガープリンティングとIPレピュテーションスコアリングを強化しました。本記事では、Pythonでレジデンシャルプロキシを使ったGoogleランクトラッカーの構築を、実行可能なコードとプロダクション運用のベストプラクティスとともに解説します。
対象読者は、SERPスクレイピングの自動化、順位変動のモニタリング、そしてプロキシの選定に悩むSEOエンジニアとPython開発者です。ProxyHatのレジデンシャルプロキシを使えば、都市レベルのジオターゲティングとスティッキーセッションで、安定したSERPデータの収集が可能になります。
なぜこの問題が存在するのか:技術的背景
Google検索のスクレイピングは年々難しくなっています。主な障壁は以下の3点です。
- num=100パラメータの廃止:2025年9月以降、1回のリクエストで100件の検索結果を取得できなくなりました。現在は
start=0,10,20...でページネーションする必要があります。 - TLS/JA3-JA4フィンガープリンティング:GoogleはTLSハンドシェイクのフィンガープリントを検査し、Pythonの
requestsライブラリなどが発するデフォルトのフィンガープリントをブロックします。詳細はMDNのUser-Agentドキュメントも参照してください。 - IPレピュテーションスコアリング:データセンターIPは高確率でCAPTCHAやブロックの対象になります。レジデンシャルIPであれば、通常のユーザーと同等の信頼スコアを得られます。
これらの対策により、単純なrequests.get()ではGoogleのSERPを安定的に取得できなくなりました。本記事では、curl_cffiのブラウザ偽装機能とProxyHatのレジデンシャルプロキシを組み合わせた解決策を提示します。SERPの定義についてはWikipediaのSearch Engine Results Pageを参照してください。
データモデルと日次SERPスナップショット
順位トラッキングの核心は「同じキーワード、同じ条件で、毎日スナップショットを撮る」ことです。1回だけのチェックでは、日々の順位変動(ボラティリティ)を追跡できません。以下のデータモデルを使います。
- keyword:追跡対象の検索キーワード
- target_domain:順位を測定したいドメイン(例:
example.com) - country:検索のジオロケーション(例:
US、JP) - device:
desktopまたはmobile - position:検索結果における順位(1〜100)
- captured_at:スナップショット取得日時(UTC)
SQLiteを使えば、外部DBサーバーなしで履歴管理ができます。以下のスキーマは、日次スナップショットの保存に最適です。
import sqlite3
from datetime import datetime, timezone
DB_PATH = "rank_tracker.db"
def init_db(db_path: str = DB_PATH) -> sqlite3.Connection:
conn = sqlite3.connect(db_path)
conn.execute("""
CREATE TABLE IF NOT EXISTS rank_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
keyword TEXT NOT NULL,
target_domain TEXT NOT NULL,
country TEXT NOT NULL DEFAULT 'US',
device TEXT NOT NULL DEFAULT 'desktop',
position INTEGER,
page INTEGER,
result_url TEXT,
captured_at TEXT NOT NULL,
UNIQUE(keyword, target_domain, country, device, captured_at)
)
""")
conn.execute("""
CREATE INDEX IF NOT EXISTS idx_keyword_domain
ON rank_snapshots(keyword, target_domain, captured_at DESC)
""")
conn.commit()
return conn
このスキーマでは、UNIQUE制約により同一キーワード・同一ドメイン・同日の重複保存を防ぎます。日次で実行すれば、7日間・30日間・90日間の順位推移を簡単にクエリできます。
SERPの取得:num=100廃止後のページネーション
2025年9月以降、Googleはnum=100パラメータを無効化しました。そのため、上位100位を取得するにはstart=0,10,20,30...90で10回リクエストを送る必要があります。ただし、実用上は上位30位(3ページ分)で十分なケースが多いです。
各リクエストには以下のパラメータを含めます。
q:検索キーワードgl:ジオロケーション(例:us、jp)hl:言語(例:en、ja)start:結果の開始位置(0, 10, 20...)num:1ページあたりの件数(10固定)
まずはcurlでProxyHatプロキシ経由のリクエストを試してみましょう。
curl -x "http://user-country-US-city-chicago-session-kw1:pass@gate.proxyhat.com:8080" \
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" \
"https://www.google.com/search?q=best+running+shoes&gl=us&hl=en&start=0&num=10"
user-country-US-city-chicago-session-kw1というユーザー名により、シカゴのレジデンシャルIPが割り当てられ、session-kw1でスティッキーセッションが維持されます。これにより、ページネーション中に同じIPを使い続け、一貫した検索結果が得られます。
レジデンシャルプロキシが必須な理由
GoogleのIPレピュテーションシステムは、データセンターIPを即座に識別し、CAPTCHAやブロックをトリガーします。レジデンシャルプロキシはISPから割り当てられた本物のIPアドレスを使用するため、通常ユーザーと同じ信頼スコアを持ちます。
以下の比較表は、プロキシタイプごとの特徴をまとめたものです。
| プロキシタイプ | IPレピュテーション | ブロック率 | ジオターゲティング | コスト | SERPスクレイピング適性 |
|---|---|---|---|---|---|
| データセンター | 低 | 高(50%以上) | 国レベルのみ | 低 | 不適 |
| レジデンシャル | 高 | 低(5%未満) | 都市レベル | 中 | 最適 |
| モバイル | 最高 | 極低 | 国レベル | 高 | 適(モバイルSERP用) |
データセンターIPでGoogleをスクレイピングすると、50%以上の確率でブロックされます。一方、レジデンシャルプロキシであればブロック率は5%未満に抑えられます。詳細なプロキシロケーションについてはProxyHatのロケーション一覧を参照してください。
実装例:curl_cffi + ProxyHatでSERPを取得
curl_cffiは、libcurlのTLSフィンガープリントをブラウザと同一にするライブラリです。impersonate='chrome'を指定すれば、Chromeと同じJA3/JA4フィンガープリントを生成できます。これがGoogleのTLS検査を回避する鍵となります。
from curl_cffi import requests as cffi_requests
PROXY_BASE = "http://user-country-{country}-city-{city}-session-{sid}:pass@gate.proxyhat.com:8080"
def fetch_serp(keyword: str, country: str = "US", city: str = "chicago",
start: int = 0, session_id: str = "default") -> str:
proxy_url = PROXY_BASE.format(
country=country, city=city, sid=session_id
)
url = "https://www.google.com/search"
params = {
"q": keyword,
"gl": country.lower(),
"hl": "en",
"start": str(start),
"num": "10",
}
headers = {
"Accept": "text/html,application/xhtml+xml",
"Accept-Language": "en-US,en;q=0.9",
}
resp = cffi_requests.get(
url, params=params, headers=headers,
proxies={"http": proxy_url, "https": proxy_url},
impersonate="chrome",
timeout=30,
)
resp.raise_for_status()
return resp.text
次に、取得したHTMLからオーガニック検索結果をパースします。GoogleのSERPには広告、ナレッジパネル、画像パックなどのフィーチャーが含まれますが、ランクトラッキングではオーガニック結果のみを抽出する必要があります。
from bs4 import BeautifulSoup
import re
def parse_organic_results(html: str) -> list:
soup = BeautifulSoup(html, "html.parser")
results = []
# Googleのオーガニック結果は div.g コンテナ内にある
for div in soup.select("div.g"):
link = div.select_one("a[href]")
if not link:
continue
href = link.get("href", "")
if not href.startswith("http"):
continue
title_el = div.select_one("h3")
title = title_el.get_text(strip=True) if title_el else ""
results.append({"url": href, "title": title})
# フォールバック: 正規表現ベースの抽出
if not results:
pattern = r'<a href="/url\?q=([^&]+)&'
for match in re.finditer(pattern, html):
results.append({"url": match.group(1), "title": ""})
return results
def find_rank(results: list, target_domain: str) -> int:
target = target_domain.lower().replace("www.", "")
for i, r in enumerate(results, 1):
url = r["url"].lower()
if target in url:
return i
return None
div.gセレクタはGoogleのオーガニック結果の標準コンテナですが、Googleは頻繁にマークアップを変更します。正規表現のフォールバックを用意することで、セレクタが壊れた場合でもデータの継続性を確保できます。
プロダクション運用:リトライ、CAPTCHA検出、並行処理
本番環境では、ネットワークエラー、CAPTCHA、レート制限への対応が不可欠です。以下の実装は、指数バックオフによるリトライ、CAPTCHA検出、セッション再生成を統合した完全なランクトラッカーです。
import time
import logging
from datetime import datetime, timezone
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("rank_tracker")
MAX_RETRIES = 3
BASE_DELAY = 2 # 秒
CAPTCHA_INDICATORS = ["unusual traffic", "captcha", "detected"]
def is_captcha(html: str) -> bool:
lower = html.lower()
return any(ind in lower for ind in CAPTCHA_INDICATORS)
def fetch_with_retry(keyword: str, country: str, city: str,
session_id: str, start: int = 0) -> str:
delay = BASE_DELAY
sid = session_id
for attempt in range(1, MAX_RETRIES + 1):
try:
html = fetch_serp(keyword, country, city, start, sid)
if is_captcha(html):
logger.warning(f"CAPTCHA detected for '{keyword}' (attempt {attempt})")
sid = f"{session_id}-retry{attempt}"
time.sleep(delay)
delay *= 2
continue
return html
except Exception as e:
logger.error(f"Fetch failed for '{keyword}': {e} (attempt {attempt})")
time.sleep(delay)
delay *= 2
raise RuntimeError(f"Max retries exceeded for keyword: {keyword}")
def track_keyword(conn, keyword: str, target_domain: str,
country: str = "US", city: str = "chicago") -> int:
session_id = f"kw-{keyword.replace(' ', '-')[:20]}"
all_results = []
for start in [0, 10, 20]: # 上位30位
html = fetch_with_retry(keyword, country, city, session_id, start)
results = parse_organic_results(html)
all_results.extend(results)
time.sleep(2) # ページ間のポライト遅延
rank = find_rank(all_results, target_domain)
now = datetime.now(timezone.utc).isoformat()
conn.execute(
"INSERT OR REPLACE INTO rank_snapshots "
"(keyword, target_domain, country, device, position, captured_at) "
"VALUES (?, ?, ?, 'desktop', ?, ?)",
(keyword, target_domain, country, rank, now)
)
conn.commit()
logger.info(f"'{keyword}' -> position: {rank}")
return rank
複数キーワードの並行処理にはThreadPoolExecutorを使用します。ただし、同時接続数を5〜10に制限し、Googleへの負荷を抑えることが重要です。
import csv
from concurrent.futures import ThreadPoolExecutor, as_completed
KEYWORDS = [
("best running shoes", "nike.com"),
("cloud hosting providers", "digitalocean.com"),
("python web framework", "fastapi.com"),
]
def run_batch(conn, keywords: list, max_workers: int = 5):
with ThreadPoolExecutor(max_workers=max_workers) as pool:
futures = {
pool.submit(track_keyword, conn, kw, domain): (kw, domain)
for kw, domain in keywords
}
for future in as_completed(futures):
kw, domain = futures[future]
try:
future.result()
except Exception as e:
logger.error(f"Failed to track '{kw}': {e}")
def export_csv(conn, output_path: str = "rank_history.csv"):
rows = conn.execute(
"SELECT keyword, target_domain, country, position, captured_at "
"FROM rank_snapshots ORDER BY captured_at DESC"
).fetchall()
with open(output_path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["keyword", "target_domain", "country", "position", "captured_at"])
writer.writerows(rows)
logger.info(f"Exported {len(rows)} rows to {output_path}")
if __name__ == "__main__":
conn = init_db()
run_batch(conn, KEYWORDS)
export_csv(conn)
conn.close()
プロダクション運用のポイント
- 指数バックオフ:リトライごとに待機時間を2倍にする(2秒→4秒→8秒)。最大3回まで。
- CAPTCHA検出:レスポンスHTMLに「unusual traffic」や「captcha」が含まれる場合は、セッションIDを変更して新しいIPを取得。
- 並行処理の制限:
max_workers=5で同時接続を制限。10を超えるとGoogleのレート制限に引っかかるリスクが高まります。 - スティッキーセッション:キーワードごとに固定セッションIDを使用し、ページネーション中は同じIPを維持。これにより、一貫した検索結果が得られます。
- 順位ボラティリティの平滑化:7日移動平均を算出することで、日々のノイズを除去し、トレンドを可視化できます。
倫理と制限
GoogleのSERPスクレイピングは、Googleの利用規約と競合する可能性があります。以下のガイドラインを守ることが重要です。
- 自分のドメインの順位を追跡:自社サイトのSEOモニタリングは、正当なユースケースです。
- 公開情報の収集:検索結果は公開情報ですが、スクレイピングの頻度とボリュームには配慮が必要です。
- レート制限の尊重:1秒間に1リクエスト以下を推奨。ページ間に2秒以上の遅延を入れる。
- 低ボリュームでは公式APIを検討:GoogleはSearch Console APIを提供しており、低ボリュームの順位チェックには公式手段が適しています。
- robots.txtの確認:対象サイトのrobots.txtを確認し、スクレイピングが許可されているかを確認する。
ProxyHatの利用にあたっては、料金プランで適切なプロキシプールサイズを選択し、公式ドキュメントで最新の接続仕様を確認してください。SERPトラッキングのユースケース詳細はSERPトラッキングページを、Webスクレイピング全般はWebスクレイピングページを参照してください。
Key Takeaways
Pythonでレジデンシャルプロキシを使ったGoogleランクトラッカーの構築において押さえるべきポイント:
- 日次スナップショットが順位ボラティリティ追跡の基盤。1回のチェックではなく、継続的なデータ蓄積が重要。
- num=100廃止後は
start=0,10,20...のページネーションが必須。上位30位で十分なケースが多い。 - curl_cffiの
impersonate='chrome'でTLS/JA3-JA4フィンガープリントを偽装し、Googleのブロックを回避。 - レジデンシャルプロキシで都市レベルのジオターゲティングとスティッキーセッションを実現。ProxyHatでは
user-country-US-city-chicago-session-kw1形式で指定。 - プロダクション運用では指数バックオフ、CAPTCHA検出、並行処理制限を必ず実装する。
- 倫理的配慮:低ボリュームでは公式APIを検討し、スクレイピング時はレート制限を尊重する。
これらの要素を組み合わせれば、99%以上の成功率で安定したGoogleランクトラッカーを構築できます。まずは数キーワードから始め、スケールに合わせてプロキシプールと並行処理を調整してください。


