ジオターゲティングSERPスクレイピングのベストプラクティスとは何か
ジオターゲティングSERPスクレイピングのベストプラクティスを理解する前に、なぜ同じ検索キーワードでも地域によって結果が変わるのかを知る必要があります。GoogleはユーザーのIPアドレス、過去の検索履歴、位置情報に基づいて検索結果をパーソナライズします。「pizza」というクエリは、東京とニューヨークとロンドンで全く異なる結果を返します。SEOエンジニアがローカルランキングデータセットを構築する場合、国レベルのターゲティングだけでは不十分です — ニューヨークとロサンゼルスでも結果が異なります。
本記事では、uule、gl、hl、google_domain、lr などのローカライズパラメータの仕組みと、それをProxyHatのレジデンシャルプロキシでIP地理と一致させる実装方法を、実行可能なPythonコードとともに解説します。
なぜ国レベルターゲティングだけでは不十分なのか
Googleは検索結果を決定する際、ユーザーの地理的位置を最も重要なシグナルの一つとして扱います。Googleのローカル検索ドキュメントでも明記されている通り、ローカル検索結果はユーザーの物理的位置に強く依存します。国レベルでgl=USを指定しても、GoogleはIPアドレスからより細かい地域を推定し、その地域に基づいたローカルパック(地図付き3件表示)を返します。
たとえば、gl=USで「dentist」を検索した場合、リクエスト元IPがシカゴならシカゴの歯科医院が、マイアミならマイアミの歯科医院がローカルパックに表示されます。都市レベルのSERPデータセットを構築するには、uuleパラメータで正確な都市を指定し、同時にその都市に一致するレジデンシャルIPでリクエストを送る必要があります。
Googleローカライズパラメータの技術詳細
Google検索URLには以下のローカライズパラメータを指定できます。
| パラメータ | 役割 | 例 |
|---|---|---|
| gl | 結果の国(Google Country) | gl=US, gl=DE, gl=JP |
| hl | インターフェース言語 | hl=en, hl=ja, hl=de |
| google_domain | Googleドメイン | google.com, google.co.jp, google.de |
| lr | 言語で絞り込む(Language Restrict) | lr=lang_ja, lr=lang_en |
| uule | 正確な地理位置を指定(エンコード済み) | uule=w+CAIQICINQ2hpY2Fnbw |
| pws | パーソナライズを無効化 | pws=0 |
uuleパラメータの構築方法
uuleはGoogleが内部で使用する位置エンコード形式で、以下の3要素から構成されます。
- 固定プレフィックス:
w+CAIQICI(常に同じ値) - 長さキー: Base64エンコード後の文字列長に対応する1文字
- Base64エンコードされた正規地名: 例えば「Chicago, IL, United States」
uuleの構造についてはGoogle SearchのWikipedia記事や多数のSEO技術ブログで文書化されています。以下のPythonコードでuuleを動的に生成できます。
パラメータとIP地理の一致 — ProxyHatの活用
最も重要なベストプラクティスは「uule/gl/hlパラメータがIPアドレスの地理と一致していること」です。Googleはパラメータだけでなくリクエスト元IPもクロスチェックします。uule=Chicagoを指定してもIPがドイツから来ていれば、Googleは不自然なリクエストと判断し、CAPTCHAや429エラーを返す可能性が高くなります。
ProxyHatのレジデンシャルプロキシを使えば、ユーザー名に国と都市を指定してIP地理を制御できます。ゲートウェイは gate.proxyhat.com、HTTPポートは 8080、SOCKS5ポートは 1080 です。
実装例1:uule生成 + gl/hl設定 + レジデンシャルIPローテーション
以下は、curl_cffiを使ってChromeのTLSフィンガープリントを偽装しつつ、都市別にuuleを構築し、ProxyHatのレジデンシャルプロキシでローテーションする例です。
import base64
from curl_cffi import requests as cffi_requests
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
# uule構築関数
def build_uule(place_name: str) -> str:
"""place_name例: 'Chicago, IL, United States'"""
encoded = base64.b64encode(place_name.encode("utf-8")).decode("utf-8")
# 長さキー: エンコード後の文字列長に1を足した値をASCII文字として扱う
length_key = chr(len(encoded) + 63)
prefix = "w+CAIQICI"
return prefix + length_key + encoded
# 都市設定
targets = [
{"city": "newyork", "place": "New York, NY, United States", "gl": "US", "hl": "en", "domain": "google.com"},
{"city": "chicago", "place": "Chicago, IL, United States", "gl": "US", "hl": "en", "domain": "google.com"},
{"city": "tokyo", "place": "Tokyo, Tokyo, Japan", "gl": "JP", "hl": "ja", "domain": "google.co.jp"},
]
PROXYHAT_USER = "your_username"
PROXYHAT_PASS = "your_password"
for t in targets:
uule = build_uule(t["place"])
url = (
f"https://www.{t['domain']}/search"
f"?q=dentist&gl={t['gl']}&hl={t['hl']}&uule={uule}&pws=0&num=100"
)
# 都市に一致するレジデンシャルIPを指定
proxy_user = f"user-country-{t['gl']}-city-{t['city']}:{PROXYHAT_PASS}"
proxy_url = f"http://{proxy_user}@gate.proxyhat.com:8080"
try:
resp = cffi_requests.get(
url,
proxies={"http": proxy_url, "https": proxy_url},
impersonate="chrome",
timeout=30,
)
logger.info(f"{t['city']}: status={resp.status_code}, len={len(resp.text)}")
# レスポンスを保存
with open(f"serp_{t['city']}.html", "w", encoding="utf-8") as f:
f.write(resp.text)
except Exception as e:
logger.error(f"{t['city']} failed: {e}")
実装例2:ProxyHat SDKを使った同じ処理
ProxyHat SDK(またはHTTPゲートウェイの同等機能)を使えば、プロキシ管理をより簡潔に記述できます。以下はrawプロキシと並行したSDK利用パターンです。
from curl_cffi import requests as cffi_requests
import logging
logger = logging.getLogger(__name__)
class ProxyHatClient:
"""ProxyHatゲートウェイのラッパー"""
GATEWAY = "gate.proxyhat.com"
HTTP_PORT = 8080
SOCKS5_PORT = 1080
def __init__(self, username: str, password: str):
self.username = username
self.password = password
def build_proxy(self, country: str, city: str = None, session: str = None, socks5: bool = False):
parts = [f"user-country-{country}"]
if city:
parts.append(f"city-{city}")
if session:
parts.append(f"session-{session}")
user_str = "-".join(parts)
scheme = "socks5" if socks5 else "http"
port = self.SOCKS5_PORT if socks5 else self.HTTP_PORT
return f"{scheme}://{user_str}:{self.password}@{self.GATEWAY}:{port}"
client = ProxyHatClient("your_username", "your_password")
# ニューヨークのレジデンシャルIPでSERP取得
proxy = client.build_proxy(country="US", city="newyork")
resp = cffi_requests.get(
"https://www.google.com/search?q=coffee+shop&gl=US&hl=en&pws=0&num=100",
proxies={"http": proxy, "https": proxy},
impersonate="chrome",
timeout=30,
)
logger.info(f"Status: {resp.status_code}, Body length: {len(resp.text)}")
実装例3:selectolaxでオーガニック結果とローカルパックをパース
取得したHTMLからオーガニック結果とローカルパックを抽出する例です。ProxyHat公式ドキュメントも参照してください。
from selectolax.parser import HTMLParser
import json
def parse_serp(html: str) -> dict:
tree = HTMLParser(html)
results = {"organic": [], "local_pack": []}
# オーガニック結果
for node in tree.css("div.g"):
title_tag = node.css_first("h3")
link_tag = node.css_first("a")
if title_tag and link_tag:
results["organic"].append({
"title": title_tag.text(strip=True),
"url": link_tag.attributes.get("href", ""),
})
# ローカルパック(地図付き3件)
for node in tree.css("div.rllt__link"):
name_tag = node.css_first("span")
if name_tag:
results["local_pack"].append({
"name": name_tag.text(strip=True),
"href": node.attributes.get("href", ""),
})
return results
# 使用例
with open("serp_newyork.html", "r", encoding="utf-8") as f:
html = f.read()
data = parse_serp(html)
print(json.dumps(data, indent=2, ensure_ascii=False))
print(f"オーガニック結果数: {len(data['organic'])}")
print(f"ローカルパック数: {len(data['local_pack'])}")
実装例4:スティッキーセッションで複数ページを取得
検索結果の2ページ目以降を取得する場合、同じIPを使い続ける(スティッキーセッション)ことが重要です。ページごとにIPが変わると、Googleが異なるユーザーとみなし、結果の一貫性が失われます。ProxyHatではユーザー名に session-xxx を指定することで同じIPを維持できます。
import base64
from curl_cffi import requests as cffi_requests
import time
import logging
logger = logging.getLogger(__name__)
def build_uule(place_name: str) -> str:
encoded = base64.b64encode(place_name.encode("utf-8")).decode("utf-8")
length_key = chr(len(encoded) + 63)
return "w+CAIQICI" + length_key + encoded
def scrape_multi_page(query: str, city: str, country: str, place: str,
gl: str, hl: str, domain: str, max_pages: int = 3):
uule = build_uule(place)
session_id = f"{city}-{int(time.time())}"
# スティッキーセッション: 同一session IDで同じIPを維持
proxy_user = f"user-country-{country}-city-{city}-session-{session_id}:your_password"
proxy_url = f"http://{proxy_user}@gate.proxyhat.com:8080"
all_results = []
for page in range(max_pages):
start = page * 10
url = (
f"https://www.{domain}/search"
f"?q={query}&gl={gl}&hl={hl}&uule={uule}&pws=0&start={start}&num=10"
)
try:
resp = cffi_requests.get(
url,
proxies={"http": proxy_url, "https": proxy_url},
impersonate="chrome",
timeout=30,
)
if resp.status_code == 200:
logger.info(f"Page {page+1}: OK, length={len(resp.text)}")
all_results.append({"page": page + 1, "html": resp.text})
elif resp.status_code == 429:
logger.warning(f"Page {page+1}: 429 rate limited, backing off")
time.sleep(30 * (page + 1))
continue
else:
logger.warning(f"Page {page+1}: status={resp.status_code}")
time.sleep(3) # 礼儀ある間隔
except Exception as e:
logger.error(f"Page {page+1} failed: {e}")
return all_results
results = scrape_multi_page(
query="plumber",
city="losangeles",
country="US",
place="Los Angeles, CA, United States",
gl="US",
hl="en",
domain="google.com",
max_pages=3,
)
print(f"取得ページ数: {len(results)}")
実装例5:CAPTCHA/429バックオフ + リトライ + 国別プロキシプール
本番環境では、CAPTCHAや429エラーへの対応が不可欠です。指数バックオフとサーキットブレーカーを組み合わせた堅牢なスクレイパーを示します。
import base64
import time
import logging
from curl_cffi import requests as cffi_requests
from functools import wraps
logger = logging.getLogger(__name__)
def build_uule(place_name: str) -> str:
encoded = base64.b64encode(place_name.encode("utf-8")).decode("utf-8")
return "w+CAIQICI" + chr(len(encoded) + 63) + encoded
def retry_with_backoff(max_retries=3, base_delay=5):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_retries):
try:
result = func(*args, **kwargs)
if result is None:
raise Exception("Empty response")
return result
except Exception as e:
delay = base_delay * (2 ** attempt)
logger.warning(f"Attempt {attempt+1} failed: {e}, retry in {delay}s")
if attempt < max_retries - 1:
time.sleep(delay)
logger.error(f"All {max_retries} retries exhausted")
return None
return wrapper
return decorator
# 国別プロキシプール
country_config = {
"US": {"domain": "google.com", "hl": "en", "cities": ["newyork", "chicago", "losangeles"]},
"DE": {"domain": "google.de", "hl": "de", "cities": ["berlin", "munich", "hamburg"]},
"JP": {"domain": "google.co.jp","hl": "ja", "cities": ["tokyo", "osaka", "nagoya"]},
}
place_map = {
"newyork": "New York, NY, United States",
"chicago": "Chicago, IL, United States",
"losangeles": "Los Angeles, CA, United States",
"berlin": "Berlin, Berlin, Germany",
"munich": "Munich, Bavaria, Germany",
"hamburg": "Hamburg, Hamburg, Germany",
"tokyo": "Tokyo, Tokyo, Japan",
"osaka": "Osaka, Osaka, Japan",
"nagoya": "Nagoya, Aichi, Japan",
}
@retry_with_backoff(max_retries=3, base_delay=5)
def scrape_with_pool(query: str, country: str, city: str):
cfg = country_config[country]
place = place_map[city]
uule = build_uule(place)
url = f"https://www.{cfg['domain']}/search?q={query}&gl={country}&hl={cfg['hl']}&uule={uule}&pws=0&num=100"
proxy_user = f"user-country-{country}-city-{city}:your_password"
proxy_url = f"http://{proxy_user}@gate.proxyhat.com:8080"
resp = cffi_requests.get(
url,
proxies={"http": proxy_url, "https": proxy_url},
impersonate="chrome",
timeout=30,
)
if resp.status_code == 429:
logger.warning(f"429 for {city}, {country} — backing off")
time.sleep(60)
raise Exception("429 rate limited")
if resp.status_code == 302 and "google.com/sorry" in resp.headers.get("location", ""):
logger.warning(f"CAPTCHA redirect for {city}, {country}")
time.sleep(120)
raise Exception("CAPTCHA challenge")
if resp.status_code != 200:
raise Exception(f"HTTP {resp.status_code}")
return {"city": city, "country": country, "html": resp.text, "length": len(resp.text)}
# 全都市を巡回
for country, cfg in country_config.items():
for city in cfg["cities"]:
result = scrape_with_pool("restaurant", country, city)
if result:
logger.info(f"{city}, {country}: {result['length']} chars")
time.sleep(2)
よくある間違いとエッジケース
- uuleとIP地理の不一致: uuleで東京を指定しながら米国IPでリクエストを送ると、GoogleがCAPTCHAを返す頻度が50%以上に跳ね上がります。常にパラメータとプロキシ地理を一致させてください。
- pws=0の省略: パーソナライズを無効化しないと、同じIPで連続リクエストした際に過去の検索履歴が結果に影響し、ベースラインが崩れます。常にpws=0を追加してください。
- データセンタープロキシの使用: GoogleはデータセンターIPを自動ボットと判定しやすく、レジデンシャルIPに比べてブロック率が著しく高くなります。SERPスクレイピングにはレジデンシャルプロキシを使用してください。
- hlとlrの混同: hlはインターフェース言語、lrは検索結果の言語フィルタです。日本語のページだけを取得したい場合はlr=lang_jaを追加します。
- numパラメータの上限: num=100はGoogleがサポートする最大値ですが、一部のモバイルエンドポイントでは無視されることがあります。結果数を確認するロジックを入れてください。
ProxyHatの設定と内部リンク
ProxyHatでは、ユーザー名に国・都市・セッションを指定するだけで、ジオターゲティングされたレジデンシャルIPを取得できます。価格についてはプロキシプラン一覧を、対応地域についてはロケーション一覧を参照してください。SERPスクレイピングのユースケース詳細はSERPトラッキングおよびWebスクレイピングページで解説しています。
公開データと利用規約の注意点
Googleの利用規約は自動化されたスクレイピングを明示的に禁止しています。本記事の技術解説は教育目的であり、実際のスクレイピングを行う前に、対象サイトの利用規約(ToS)とrobots.txtを確認し、適用される法律(GDPR、CCPAなど)を遵守してください。商用利用にはGoogle Official APIの使用を推奨します。ProxyHatはツールを提供しますが、利用者の行動に対する法的責任は負いません。
Key Takeaways
- 国レベル(gl)だけでは不十分 — 都市レベルのuule指定と一致するレジデンシャルIPが必須。
- uuleは「w+CAIQICI + 長さキー + Base64地名」の3要素で構成される。
- 常にpws=0でパーソナライズを無効化し、ベースラインを統一する。
- パラメータ(gl/hl/uule)とプロキシIP地理が一致しないとCAPTCHA/429が頻発する。
- 複数ページ取得にはスティッキーセッション(session-xxx)で同一IPを維持する。
- 指数バックオフ + リトライで429/CAPTCHAに対応し、国別プロキシプールで運用する。
FAQ
ジオターゲティングSERPスクレイピングのベストプラクティスとは何ですか?
ジオターゲティングSERPスクレイピングのベストプラクティスとは、Googleのローカライズパラメータ(gl、hl、uule)とリクエスト元IPの地理を一致させ、都市レベルで正確な検索結果を収集する手法です。国レベルの指定だけではローカルパックの地域差を再現できないため、uuleで都市をピン留めし、対応するレジデンシャルプロキシでリクエストを送る必要があります。pws=0でパーソナライズを無効化することも重要です。
なぜジオターゲティングSERPスクレイピングのベストプラクティスがプロキシユーザーにとって重要なのですか?
Googleはリクエスト元IPアドレスとURLパラメータの両方をクロスチェックして検索結果を決定します。uuleでシカゴを指定してもIPがドイツから来ていれば、結果が不正確になるか、CAPTCHAや429エラーが返されます。プロキシユーザーは、パラメータとIP地理を一致させることで初めて、信頼性の高いローカルSERPデータセットを構築できます。これがレジデンシャルプロキシを選ぶ理由です。
ジオターゲティングSERPスクレイピングに最適なプロキシタイプはどれですか?
レジデンシャルプロキシが最適です。データセンタープロキシはGoogleにボットとして検出されやすく、ブロック率が大幅に高くなります。モバイルプロキシも有効ですがコストが高いため、コストパフォーマンスを重視する場合はレジデンシャルプロキシが推奨されます。ProxyHatではユーザー名にcountry-US-city-newyorkのように指定するだけで、対象都市のレジデンシャルIPにルーティングできます。
ジオターゲティングSERPスクレイピングでブロックを回避するにはどうすればよいですか?
まず、uule/gl/hlパラメータとプロキシIPの地理を一致させてください。次に、curl_cffiなどでChromeのTLSフィンガープリントを偽装し、リクエスト間に2〜5秒の間隔を空けます。429やCAPTCHAリダイレクトを検出したら指数バックオフでリトライし、複数ページ取得時はスティッキーセッションで同一IPを維持してください。1IPあたりのリクエスト数を1時間あたり50件以下に抑えるのも有効な手段です。
uuleパラメータはどのように構築しますか?
uuleは「w+CAIQICI」という固定プレフィックス、Base64エンコード後の文字列長に対応する1文字の長さキー、Base64エンコードされた正規地名(例:Chicago, IL, United States)の3要素を結合して構築します。Pythonではbase64.b64encodeで地名をエンコードし、chr(len(encoded) + 63)で長さキーを生成して結合するだけで作成できます。


