Los ingenieros SEO saben que "pizza near me" devuelve resultados distintos en Manhattan que en Brooklyn. Sin embargo, muchos pipelines de rank tracking siguen scrapeando desde una única ubicación IP y asumen que el targeting a nivel de país es suficiente. No lo es. Para construir datasets SERP localizados precisos necesitas aplicar mejores prácticas para scraping SERP geo-segmentado: combinar parámetros URL precisos (uule, gl, hl) con proxies residenciales que coincidan con la geografía objetivo a nivel de ciudad. Esta guía recorre el stack completo, desde la construcción del parámetro uule hasta la rotación de IPs residenciales por locale, con ejemplos ejecutables en Python y curl.
Mejores prácticas para scraping SERP geo-segmentado: por qué la ubicación importa
Google personaliza los resultados de búsqueda según múltiples señales: la ubicación física del usuario (derivada de su IP), la configuración de búsqueda (gl, hl, google_domain), el historial de búsquedas y el tipo de dispositivo. Para SEO local, el targeting a nivel de país es insuficiente porque el local pack (el bloque de Google Maps con 3 negocios) se basa en un radio de aproximadamente 20 km alrededor del buscador. "Plumber" en Miami y "plumber" en Seattle devuelven conjuntos de resultados completamente distintos.
Además, Google verifica la coherencia entre la geografía de la IP y los parámetros de localización. Si envías gl=us con una IP alemana, Google puede degradar los resultados o mostrar un CAPTCHA. Por eso, el scraping SERP geo-segmentado requiere que cada parámetro de URL coincida con la geografía del proxy. Consulta nuestra guía de SERP tracking para más contexto sobre rank tracking localizado.
Parámetros core de localización: gl, hl, google_domain, lr y uule
Google acepta varios parámetros de URL para controlar la localización de los resultados:
- gl: país del buscador (ej:
us,de,es). Equivale al parámetro documentado en la API de Google Custom Search. - hl: idioma de la interfaz (ej:
en,es,de). - google_domain: dominio de Google (ej:
google.com,google.de,google.es). Debe ser coherente congl. - lr: restringe el idioma de los resultados (ej:
lang_en,lang_es). Diferente dehl:hlcontrola la interfaz;lrfiltra los resultados por idioma. - uule: codifica una ubicación precisa (ciudad o distrito). Es un parámetro interno no documentado oficialmente, pero ampliamente usado por herramientas SEO.
- pws=0: desactiva la personalización, devolviendo un baseline no personalizado. Imprescindible para datos reproducibles.
Para scraping SERP local por ciudad, uule es el parámetro más potente: permite fijar una ciudad exacta independientemente del país. Combinado con gl y hl coherentes, obtienes resultados que replican lo que vería un usuario real en esa ciudad.
Construcción del valor uule
El formato del parámetro uule es:
w+CAIQ + carácter de longitud + base64 del nombre canónico del lugar
El carácter de longitud se calcula como chr(64 + len(base64_string)). Por ejemplo, para "New York,New York,United States":
- Base64:
TmV3IFlvcmssTmV3IFlvcmssVW5pdGVkIFN0YXRlcw==(44 caracteres) - Carácter de longitud:
chr(64 + 44) = 'l' - uule:
w+CAIQlTmV3IFlvcmssTmV3IFlvcmssVW5pdGVkIFN0YXRlcw==
El primer ejemplo muestra una llamada básica con curl:
curl -s "https://www.google.com/search?q=pizza&gl=us&hl=en&pws=0&uule=w+CAIQlTmV3IFlvcmssTmV3IFlvcmssVW5pdGVkIFN0YXRlcw==" \
--proxy "http://user-country-US-city-newyork:pass@gate.proxyhat.com:8080"
La geografía de la IP debe coincidir con tus parámetros
Google cruzaliza la geografía derivada de la IP con los parámetros gl/uule. Si hay una discrepancia significativa —por ejemplo, una IP de datacenter en Frankfurt con gl=us y uule apuntando a Nueva York— Google puede devolver resultados menos precisos, mostrar un intersticial de CAPTCHA o bloquear la solicitud con un error 429. La solución es enrutar cada solicitud a través de un proxy residencial cuya ubicación coincida con el mercado objetivo. ProxyHat permite especificar país y ciudad directamente en el nombre de usuario, garantizando que la IP de salida esté en la geografía correcta. Consulta las ubicaciones disponibles para verificar cobertura.
| Tipo de proxy | Precisión geográfica | Riesgo de detección | Coste aprox. | Ideal para |
|---|---|---|---|---|
| Datacenter | País (por IP) | Alto | $0.5–2/GB | Consultas masivas sin geo |
| Residencial | Ciudad/Distrito | Bajo | $3–15/GB | Scraping SERP local por ciudad |
| Móvil | Ciudad/Distrito | Muy bajo | $10–30/GB | Verificación de anuncios, SERP móvil |
Implementación en Python: uule + gl/hl con rotación de IPs residenciales
El siguiente ejemplo construye el parámetro uule, configura gl/hl coherentes y enruta la solicitud a través de un proxy residencial de ProxyHat con targeting de ciudad. Primero, la versión con proxy crudo (sin flags de geo), y luego la versión con targeting geo en el nombre de usuario —el equivalente al SDK de ProxyHat.
Versión con proxy crudo (sin geo-targeting)
import requests
import base64
def build_uule(canonical_name: str) -> str:
"""Construye el parámetro uule de Google desde un nombre canónico de lugar."""
b64 = base64.b64encode(canonical_name.encode("utf-8")).decode("ascii")
length_key = chr(64 + len(b64))
return f"w+CAIQ{length_key}{b64}"
# Proxy crudo: sin flags de país/ciudad en el nombre de usuario
raw_proxy = "http://user:pass@gate.proxyhat.com:8080"
params = {
"q": "best pizza",
"gl": "us",
"hl": "en",
"pws": "0",
"uule": build_uule("New York,New York,United States"),
}
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/124.0.0.0 Safari/537.36",
"Accept-Language": "en-US,en;q=0.9",
}
resp = requests.get(
"https://www.google.com/search",
params=params,
headers=headers,
proxies={"http": raw_proxy, "https": raw_proxy},
timeout=15,
)
print(f"Status: {resp.status_code}, Length: {len(resp.text)}")
Versión con ProxyHat geo-targeting (flags en el username)
import requests
import base64
def build_uule(canonical_name: str) -> str:
b64 = base64.b64encode(canonical_name.encode("utf-8")).decode("ascii")
length_key = chr(64 + len(b64))
return f"w+CAIQ{length_key}{b64}"
# ProxyHat con geo-targeting: país + ciudad en el nombre de usuario
proxy = "http://user-country-US-city-newyork:pass@gate.proxyhat.com:8080"
params = {
"q": "best pizza",
"gl": "us",
"hl": "en",
"pws": "0",
"uule": build_uule("New York,New York,United States"),
}
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/124.0.0.0 Safari/537.36",
"Accept-Language": "en-US,en;q=0.9",
}
resp = requests.get(
"https://www.google.com/search",
params=params,
headers=headers,
proxies={"http": proxy, "https": proxy},
timeout=15,
)
print(f"Status: {resp.status_code}, Length: {len(resp.text)}")
La diferencia clave: en la segunda versión, ProxyHat asigna una IP residencial ubicada en Nueva York, alineada con gl=us y el uule de Nueva York. Esto reduce drásticamente el riesgo de CAPTCHAs y resultados desviados. Consulta la documentación de ProxyHat para más detalles sobre flags de geo-targeting.
Con curl_cffi e impersonación de Chrome
Para mayor realismo, usa curl_cffi con impersonate="chrome", que replica la firma TLS de Chrome —incluyendo el orden de cipher suites y extensiones—:
from curl_cffi import requests as cffi_requests
import base64
def build_uule(canonical_name: str) -> str:
b64 = base64.b64encode(canonical_name.encode("utf-8")).decode("ascii")
length_key = chr(64 + len(b64))
return f"w+CAIQ{length_key}{b64}"
proxy = "http://user-country-US-city-newyork:pass@gate.proxyhat.com:8080"
params = {
"q": "best pizza",
"gl": "us",
"hl": "en",
"pws": "0",
"uule": build_uule("New York,New York,United States"),
}
resp = cffi_requests.get(
"https://www.google.com/search",
params=params,
impersonate="chrome",
proxies={"http": proxy, "https": proxy},
timeout=15,
)
print(f"Status: {resp.status_code}")
Parsing de resultados orgánicos y local pack con selectolax
Una vez que tienes el HTML, necesitas extraer los resultados orgánicos y el local pack. selectolax es un parser rápido basado en C, ideal para procesar grandes volúmenes de SERPs. Más información sobre pipelines de scraping en nuestro caso de uso de web scraping.
from selectolax.parser import HTMLParser
def parse_serp(html: str) -> dict:
"""Extrae resultados orgánicos y local pack del HTML de Google."""
tree = HTMLParser(html)
results = []
# Resultados orgánicos
for node in tree.css("div.g"):
title_node = node.css_first("h3")
link_node = node.css_first("a")
if title_node and link_node:
results.append({
"type": "organic",
"title": title_node.text(strip=True),
"url": link_node.attributes.get("href", ""),
})
# Local pack (Google Maps)
for node in tree.css("div[data-cid]"):
name_node = node.css_first("div.dbg0p")
if name_node:
results.append({
"type": "local_pack",
"name": name_node.text(strip=True),
})
return {"results": results, "count": len(results)}
Nota: los selectores CSS de Google cambian con frecuencia. Debes monitorizar y actualizar los selectores cuando Google modifique su HTML. Una práctica recomendada es guardar el HTML crudo junto con los datos parseados para debugging.
Sesiones sticky para SERPs multipágina
Cuando necesitas recoger múltiples páginas de resultados (page 1, 2, 3...), es crucial mantener la misma IP durante toda la sesión. Si la IP cambia entre páginas, Google puede devolver resultados inconsistentes o mostrar un CAPTCHA. ProxyHat soporta sesiones sticky mediante el flag session- en el nombre de usuario:
from curl_cffi import requests as cffi_requests
import base64
import time
def build_uule(canonical_name: str) -> str:
b64 = base64.b64encode(canonical_name.encode("utf-8")).decode("ascii")
length_key = chr(64 + len(b64))
return f"w+CAIQ{length_key}{b64}"
session_id = "nyc-pizza-001"
# Sesión sticky: la misma IP se mantiene durante toda la sesión
proxy = f"http://user-country-US-city-newyork-session-{session_id}:pass@gate.proxyhat.com:8080"
uule = build_uule("New York,New York,United States")
all_pages = []
for page in range(3):
start = page * 10
params = {
"q": "best pizza",
"gl": "us",
"hl": "en",
"pws": "0",
"uule": uule,
"start": str(start),
}
resp = cffi_requests.get(
"https://www.google.com/search",
params=params,
impersonate="chrome",
proxies={"http": proxy, "https": proxy},
timeout=15,
)
if resp.status_code == 429:
print(f"429 en página {page + 1}, esperando 30s")
time.sleep(30)
continue
all_pages.append(resp.text)
print(f"Página {page + 1}: {resp.status_code}, {len(resp.text)} bytes")
time.sleep(3) # Rate limiting respetuoso
print(f"Total páginas recogidas: {len(all_pages)}")
Estrategias de backoff para CAPTCHA y errores 429
Incluso con proxies residenciales y TLS fingerprinting, Google puede devolver 429 o CAPTCHAs. Necesitas un sistema de reintentos con backoff exponencial. El RFC 7231 define el código 429 como "Too Many Requests" y recomienda respetar la cabecera Retry-After:
import time
import logging
from curl_cffi import requests as cffi_requests
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("serp-scraper")
MAX_RETRIES = 5
INITIAL_BACKOFF = 2 # segundos
def fetch_with_retry(url, params, proxy, impersonate="chrome"):
"""Fetch con backoff exponencial y manejo de 429."""
backoff = INITIAL_BACKOFF
for attempt in range(MAX_RETRIES):
try:
resp = cffi_requests.get(
url,
params=params,
impersonate=impersonate,
proxies={"http": proxy, "https": proxy},
timeout=15,
)
if resp.status_code == 200:
return resp
elif resp.status_code == 429:
retry_after = int(resp.headers.get("Retry-After", backoff))
logger.warning(f"429 en intento {attempt + 1}, esperando {retry_after}s")
time.sleep(retry_after)
elif resp.status_code >= 500:
logger.warning(f"{resp.status_code} en intento {attempt + 1}")
time.sleep(backoff)
backoff *= 2
else:
logger.error(f"Status inesperado: {resp.status_code}")
return resp
except Exception as e:
logger.error(f"Error de red: {e}")
time.sleep(backoff)
backoff *= 2
raise RuntimeError(f"Falló tras {MAX_RETRIES} intentos")
Pools de proxy por país
Para datasets SERP multi-país, mantén pools de proxy separados por mercado. Cada pool debe usar el flag de país correspondiente en ProxyHat, y los parámetros gl/hl/google_domain deben ser coherentes:
PROXY_POOLS = {
"US": {
"proxy": "http://user-country-US-city-newyork:pass@gate.proxyhat.com:8080",
"gl": "us", "hl": "en", "domain": "google.com",
"uule": build_uule("New York,New York,United States"),
},
"DE": {
"proxy": "http://user-country-DE-city-berlin:pass@gate.proxyhat.com:8080",
"gl": "de", "hl": "de", "domain": "google.de",
"uule": build_uule("Berlin,Berlin,Germany"),
},
"ES": {
"proxy": "http://user-country-ES-city-madrid:pass@gate.proxyhat.com:8080",
"gl": "es", "hl": "es", "domain": "google.es",
"uule": build_uule("Madrid,Madrid,Spain"),
},
}
def scrape_by_country(query: str, country: str):
cfg = PROXY_POOLS[country]
params = {
"q": query,
"gl": cfg["gl"],
"hl": cfg["hl"],
"pws": "0",
"uule": cfg["uule"],
}
resp = fetch_with_retry(
f"https://www.{cfg['domain']}/search",
params,
cfg["proxy"],
)
return parse_serp(resp.text)
Consideraciones legales y éticas
El scraping de resultados de Google plantea cuestiones legales. Debes revisar los términos del servicio de Google y cumplir con normativas aplicables como el GDPR y la CCPA. Más contexto en Wikipedia: Search engine results page. Recomendaciones:
- Respeta
robots.txty los límites de tasa razonables (3–5 segundos entre solicitudes). - No almacenes datos personales innecesarios.
- Considera usar la Google Custom Search API para volúmenes moderados (100 consultas/día gratis).
- Para datasets a gran escala, consulta con asesoría legal sobre el uso de datos públicos.
Conclusiones clave
El scraping SERP geo-segmentado efectivo requiere tres capas alineadas: parámetros URL (uule + gl + hl), geografía de IP coincidente (proxy residencial por ciudad), y fingerprinting TLS realista (curl_cffi impersonate). Si cualquiera de las tres falla, Google puede degradar o bloquear tus solicitudes.
- uule es el parámetro más potente para fijar una ciudad exacta; combínalo siempre con gl y hl coherentes.
- pws=0 elimina la personalización y da un baseline reproducible.
- La IP debe coincidir con el uule: usa proxies residenciales con targeting de ciudad en ProxyHat (
-country-US-city-newyork). - Sesiones sticky (
-session-abc123) mantienen la misma IP para SERPs multipágina. - Backoff exponencial con Retry-After para 429; máximo 5 reintentos.
- Selectores CSS cambian: guarda HTML crudo para debugging y actualiza parsers con frecuencia.
¿Listo para construir tu pipeline de scraping SERP geo-segmentado? Explora las ubicaciones de proxy disponibles, revisa nuestro caso de uso de SERP tracking, o consulta la documentación de precios de ProxyHat.






