La semana pasada intenté recopilar las valoraciones de hoteles y el número de reseñas de unas 200 propiedades en tres ciudades europeas desde TripAdvisor. Mi primer script — un requests.get() básico con cabeceras por defecto — me devolvió un precioso error 403 Forbidden en cada solicitud. Ni un solo byte útil.
TripAdvisor es una de las fuentes de datos públicas más completas del sector turístico: más de , más de 8 millones de fichas de negocio y alrededor de 460 millones de visitantes únicos al mes. Influye en más de en gasto turístico anual. Pero ¿obtener esos datos de forma programática? Ahí es donde todo se complica. TripAdvisor utiliza detección de bots con DataDome, Cloudflare WAF, fingerprinting TLS y desafíos JavaScript: una defensa por capas que bloquea la mayoría de intentos ingenuos antes incluso de empezar. Esta guía es el recurso que me habría gustado tener: una comparación directa de tres enfoques de scraping en Python (más una opción sin código), código completo para cada uno, una sección estructurada de resolución de problemas anti-bot y patrones reutilizables que funcionan para hoteles, restaurantes y atracciones. Tanto si estás empezando con Python como si ya eres desarrollador con experiencia, esto debería ahorrarte muchos 403 innecesarios.
¿No quieres escribir código? Extrae TripAdvisor de la forma más fácil
Quiero ser claro desde el principio: mucha gente que busca “scrape TripAdvisor with Python” en realidad no está casada con la idea de programar. Solo quiere los datos — nombres de hoteles, valoraciones, número de reseñas, precios — en una hoja de cálculo, y rápido. Si ese es tu caso, hay un camino mucho más corto.
es una extensión de Chrome con IA que hemos creado y que puede leer cualquier página de TripAdvisor y sugerir automáticamente las columnas correctas para extraer. El flujo de trabajo de verdad se hace en dos clics:
- Abre una página de resultados de TripAdvisor (por ejemplo, resultados de “Hotels in Paris”).
- Haz clic en “AI Suggest Fields” en la barra lateral de Thunderbit. La IA analiza la página y propone columnas como Nombre del hotel, Valoración, Número de reseñas, Precio y Ubicación.
- Haz clic en “Scrape”. Thunderbit extrae los datos de cada ficha de la página y, si necesitas más resultados, gestiona la paginación automáticamente.
- Exporta a Excel, Google Sheets, Airtable o Notion. Las exportaciones son gratuitas en todos los planes.
Thunderbit funciona con hoteles, restaurantes y atracciones sin cambiar la configuración: la IA se adapta a lo que haya en la página. En resultados paginados, detecta automáticamente los botones de “Siguiente” y el scroll infinito. Y como se ejecuta dentro de tu propio navegador Chrome, hereda tus cookies de sesión y el fingerprint del navegador, lo que le da una ventaja natural frente a la detección de bots.
Puedes probarlo con la : el plan gratis ofrece 6 páginas al mes, suficiente para probar el flujo.
Si necesitas control programático, lógica de parsing personalizada o planeas extraer más de 10.000 páginas, Python es el camino. Sigue leyendo.
¿Por qué extraer TripAdvisor con Python?
Los datos de TripAdvisor tienen un impacto empresarial directo y medible. Un encontró que un aumento de 1 punto en el Global Review Index de un hotel sobre 100 se traduce en un incremento del 0,89% en la tarifa diaria media y del 1,42% en Revenue Per Available Room. Otro mostró que un aumento exógeno de 1 estrella en la valoración de TripAdvisor supone entre 55.000 y 75.000 dólares adicionales de ingresos anuales para un hotel medio. Las reseñas no son solo métricas de vanidad: impulsan ingresos.
Así es como distintos equipos usan los datos de TripAdvisor:
| Caso de uso | Quién se beneficia | Datos necesarios |
|---|---|---|
| Análisis de competidores hoteleros | Cadenas hoteleras, revenue managers | Valoraciones, precios, volumen de reseñas, servicios |
| Investigación de mercado de restaurantes | Grupos de restauración, marcas de alimentación | Tipo de cocina, rangos de precio, sentimiento de reseñas |
| Seguimiento de tendencias de atracciones | Operadores turísticos, oficinas de turismo | Ranking de popularidad, patrones estacionales |
| Análisis de sentimiento | Investigadores, analistas de datos | Texto completo de reseñas, estrellas, fechas |
| Generación de leads | Equipos de ventas, agencias de viajes | Nombres de negocio, datos de contacto, ubicaciones |
¿Por qué Python concretamente? Tres razones. Primero, el ecosistema: BeautifulSoup, Selenium, Playwright, Scrapy, httpx, pandas… Python tiene bibliotecas de scraping y análisis de datos más maduras que cualquier otro lenguaje. Segundo, usan Python, así que hay más apoyo de la comunidad, más respuestas en StackOverflow y más guías actualizadas. Tercero, la ventaja del flujo de trabajo: puedes extraer con BeautifulSoup, limpiar con pandas, hacer análisis de sentimiento con Hugging Face Transformers y crear paneles, todo en un solo lenguaje. Sin cambiar de contexto.
Tres formas de extraer TripAdvisor con Python (comparadas)
Todas las guías comparables eligen un único enfoque y se quedan con él. Eso no ayuda cuando intentas decidir antes de escribir código. Aquí tienes la tabla comparativa que me habría gustado recibir:
| Enfoque | Velocidad | Soporte JS | Resistencia anti-bot | Complejidad | Ideal para |
|---|---|---|---|---|---|
requests + BeautifulSoup | ⚡ Rápido (~120–200 páginas/min sin procesar) | ❌ Ninguno | ⚠️ Baja | Fácil | Páginas estáticas, proyectos pequeños |
| Selenium / navegador sin interfaz | 🐢 Lento (~8–20 páginas/min) | ✅ Completo | ⚠️ Media | Media | Contenido dinámico, clics en “Leer más”, banners de cookies |
| JSON oculto / API GraphQL | ⚡⚡ El más rápido (~200–600 páginas/min sin procesar) | N/A | ✅ Más alta | Difícil | Extracción masiva de reseñas/hoteles |
| Sin código (Thunderbit) | ⚡ Rápido | ✅ Integrado | ✅ Integrado | La más fácil | No programadores, exportaciones rápidas puntuales |
Un par de matices importantes. Esas velocidades son teóricas: los límites de TripAdvisor (~10–15 solicitudes por minuto por IP) reducen el rendimiento real a aproximadamente 10 páginas por minuto por IP, independientemente del enfoque. El método del JSON oculto te da más datos por solicitud, así que necesita menos peticiones totales y se expone menos al rate limiting. Selenium es 5 veces más lento que los enfoques basados en requests en pruebas reales, pero es la única opción cuando necesitas hacer clic en botones o renderizar JavaScript.
El resto de esta guía recorre los tres métodos en Python con código completo. Elige el que encaje con tu caso, o combínalos (yo suelo usar requests+BS4 para páginas de listados y JSON oculto para páginas de detalle).
Configurar tu entorno Python
Antes de empezar, deja listo el entorno. Necesitarás Python 3.10+ (recomiendo 3.12 o 3.13: los principales paquetes ya los soportan sin problemas conocidos).
Instala todo de una vez:
1pip install requests beautifulsoup4 selenium httpx parsel pandas curl-cffi
Notas sobre los paquetes:
requests(2.33.1) — solicitudes HTTP, requiere Python 3.10+beautifulsoup4(4.14.3) — análisis HTMLselenium(4.43.0) — automatización del navegador, requiere Python 3.10+httpx(0.28.1) — cliente HTTP asíncronoparsel(1.11.0) — selectores CSS/XPath (más ligero que BS4)pandas(3.0.2) — exportación de datos, requiere Python 3.11+curl_cffi(0.15.0) — suplantación de fingerprint TLS (clave para saltarse Cloudflare)
ChromeDriver: Si usas Selenium, buenas noticias: desde Selenium 4.6, Selenium Manager descarga y cachea automáticamente el binario correcto de ChromeDriver. No necesitas instalación manual. Resuelve la compatibilidad de versiones de forma dinámica, así que no tendrás que preocuparte por desajustes con Chrome.
Entorno virtual (recomendado):
1python -m venv tripadvisor-scraper
2source tripadvisor-scraper/bin/activate # macOS/Linux
3tripadvisor-scraper\Scripts\activate # Windows
Enfoque 1: extraer TripAdvisor con Requests y BeautifulSoup
Este es el enfoque más sencillo. Funciona muy bien para páginas de listados (resultados de búsqueda de hoteles, listas de restaurantes) donde los datos necesarios están en el HTML estático. Sin navegador, sin renderizado JavaScript, con uso mínimo de recursos.
Entender los patrones de URL de TripAdvisor
Las URL de TripAdvisor siguen patrones bastante predecibles según la categoría:
- Hoteles:
https://www.tripadvisor.com/Hotels-g{locationId}-{Location_Name}-Hotels.html - Restaurantes:
https://www.tripadvisor.com/Restaurants-g{locationId}-{Location_Name}.html - Atracciones:
https://www.tripadvisor.com/Attractions-g{locationId}-Activities-{Location_Name}.html
La paginación usa el parámetro oa (offset anchors), insertado en la URL. Cada página muestra 30 resultados:
- Página 1: URL base (sin parámetro
oa) - Página 2:
Hotels-g187768-oa30-Italy-Hotels.html - Página 3:
Hotels-g187768-oa60-Italy-Hotels.html
En páginas de reseñas, el parámetro de offset es or y avanza de 10 en 10:
- Página 1:
Reviews-or0-Hotel_Name.html - Página 2:
Reviews-or10-Hotel_Name.html
Para obtener reseñas en todos los idiomas, añade ?filterLang=ALL a la URL.
Enviar solicitudes con cabeceras realistas
TripAdvisor comprueba las cabeceras de forma agresiva. Una solicitud con cabeceras por defecto de Python se bloquea al instante. Hay que imitar un navegador Chrome real:
1import requests
2import time
3import random
4session = requests.Session()
5headers = {
6 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
7 "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
8 "Accept-Language": "en-US,en;q=0.9",
9 "Accept-Encoding": "gzip, deflate, br",
10 "Referer": "https://www.tripadvisor.com/",
11 "Sec-Fetch-Dest": "document",
12 "Sec-Fetch-Mode": "navigate",
13 "Sec-Fetch-Site": "none",
14 "Sec-CH-UA": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
15 "Sec-CH-UA-Mobile": "?0",
16 "Sec-CH-UA-Platform": '"Windows"',
17}
18session.headers.update(headers)
19url = "https://www.tripadvisor.com/Hotels-g187147-Paris_Ile_de_France-Hotels.html"
20response = session.get(url)
21print(f"Status: {response.status_code}")
22print(f"Content length: {len(response.text)} characters")
Detalle clave: TripAdvisor valida que las cabeceras User-Agent y Sec-CH-UA sean coherentes. Si afirmas que eres Chrome 135 en User-Agent pero Sec-CH-UA dice Chrome 120, te marcarán. Rota siempre conjuntos completos de cabeceras, no cabeceras sueltas.
Analizar listados con BeautifulSoup
Una vez que consigues una respuesta válida, extrae los datos con BeautifulSoup. TripAdvisor usa atributos data-automation y data-test-attribute, que son más estables que los nombres de clase CSS (que cambian con frecuencia):
1from bs4 import BeautifulSoup
2soup = BeautifulSoup(response.text, "html.parser")
3# Encontrar todas las tarjetas de hoteles
4cards = soup.select('div[data-test-attribute="location-results-card"]')
5hotels = []
6for card in cards:
7 # Nombre del hotel
8 title_el = card.select_one('div[data-automation="hotel-card-title"]')
9 name = title_el.get_text(strip=True) if title_el else None
10 # Enlace a la página de detalle
11 link_el = card.select_one('div[data-automation="hotel-card-title"] a')
12 link = "https://www.tripadvisor.com" + link_el["href"] if link_el else None
13 # Valoración
14 rating_el = card.select_one('[data-automation="bubbleRatingValue"]')
15 rating = rating_el.get_text(strip=True) if rating_el else None
16 # Número de reseñas
17 review_el = card.select_one('[data-automation="bubbleReviewCount"]')
18 review_count = review_el.get_text(strip=True).replace(",", "").split()[0] if review_el else None
19 hotels.append({
20 "name": name,
21 "rating": rating,
22 "review_count": review_count,
23 "url": link,
24 })
25print(f"Encontrados {len(hotels)} hoteles en esta página")
26for h in hotels[:3]:
27 print(h)
Nota sobre los selectores: TripAdvisor usa clases CSS ofuscadas (como FGwzt, yyzcQ) que cambian con cada actualización del sitio. Los atributos data-automation y data-test-target son mucho más estables. Prioriza siempre los atributos de datos frente a las clases.
Gestionar la paginación
Para extraer varias páginas, recorre el parámetro de offset con una pausa prudente entre solicitudes:
1import pandas as pd
2all_hotels = []
3base_url = "https://www.tripadvisor.com/Hotels-g187147-oa{offset}-Paris_Ile_de_France-Hotels.html"
4for page in range(5): # Primeras 5 páginas
5 offset = page * 30
6 url = base_url.format(offset=offset) if page > 0 else "https://www.tripadvisor.com/Hotels-g187147-Paris_Ile_de_France-Hotels.html"
7 response = session.get(url)
8 if response.status_code != 200:
9 print(f"Página {page + 1}: código {response.status_code}, deteniendo.")
10 break
11 soup = BeautifulSoup(response.text, "html.parser")
12 cards = soup.select('div[data-test-attribute="location-results-card"]')
13 for card in cards:
14 title_el = card.select_one('div[data-automation="hotel-card-title"]')
15 name = title_el.get_text(strip=True) if title_el else None
16 rating_el = card.select_one('[data-automation="bubbleRatingValue"]')
17 rating = rating_el.get_text(strip=True) if rating_el else None
18 review_el = card.select_one('[data-automation="bubbleReviewCount"]')
19 review_count = review_el.get_text(strip=True).replace(",", "").split()[0] if review_el else None
20 all_hotels.append({"name": name, "rating": rating, "review_count": review_count})
21 print(f"Página {page + 1}: {len(cards)} hoteles encontrados")
22 time.sleep(random.uniform(3, 7)) # Pausa aleatoria para evitar rate limiting
23df = pd.DataFrame(all_hotels)
24print(f"\nTotal de hoteles extraídos: {len(df)}")
time.sleep(random.uniform(3, 7)) es importante. El umbral de rate limit de TripAdvisor ronda los 10–15 solicitudes por minuto por IP. Ir más rápido dispara CAPTCHAs o errores 429.
Limitaciones de este enfoque
¿Dónde falla? El enfoque requests+BS4 se queda corto cuando:
- TripAdvisor sirve contenido renderizado por JavaScript (algunas páginas de resultados lo requieren)
- El texto de las reseñas está truncado detrás de botones de “Leer más”
- Las medidas anti-bot escalan a desafíos JavaScript o CAPTCHAs
- Necesitas datos que solo aparecen tras renderizado del lado del cliente (precios, disponibilidad)
Para esos casos, necesitas Selenium (Enfoque 2) o el método de JSON oculto (Enfoque 3).
Enfoque 2: extraer TripAdvisor con Selenium (navegador sin interfaz)
Selenium abre un navegador real, así que puede renderizar JavaScript, hacer clic en botones, manejar banners de consentimiento de cookies e interactuar con contenido dinámico. El coste: es aproximadamente y consume entre 300 y 500 MB de RAM por instancia de navegador.
Configurar Selenium con ajustes anti-detección
De serie, Selenium es detectado con facilidad. El fingerprinting de TripAdvisor lo identifica enseguida. Hay que desactivar las señales de automatización:
1from selenium import webdriver
2from selenium.webdriver.chrome.options import Options
3from selenium.webdriver.common.by import By
4from selenium.webdriver.support.ui import WebDriverWait
5from selenium.webdriver.support import expected_conditions as EC
6options = Options()
7options.add_argument("--headless=new") # Nuevo modo headless (Chrome 112+)
8options.add_argument("--disable-blink-features=AutomationControlled")
9options.add_argument("--window-size=1920,1080")
10options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36")
11options.add_experimental_option("excludeSwitches", ["enable-automation"])
12options.add_experimental_option("useAutomationExtension", False)
13driver = webdriver.Chrome(options=options)
14# Quitar la propiedad webdriver de navigator
15driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
16 "source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
17})
¿Es suficiente para TripAdvisor? Para scraping a pequeña escala (menos de 50 páginas), esta configuración con proxies residenciales suele funcionar. Para volúmenes mayores, quizá necesites undetected-chromedriver o nodriver: la protección DataDome de TripAdvisor analiza más de 1.000 señales por solicitud, incluidas fingerprints TLS que Selenium puro no puede falsificar.
Extraer resultados de búsqueda de hoteles con Selenium
1import time
2import random
3url = "https://www.tripadvisor.com/Hotels-g187147-Paris_Ile_de_France-Hotels.html"
4driver.get(url)
5# Esperar a que carguen las tarjetas de hotel
6wait = WebDriverWait(driver, 15)
7wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'div[data-test-attribute="location-results-card"]')))
8# Manejar el popup de cookies (si aparece)
9try:
10 cookie_btn = driver.find_element(By.ID, "onetrust-accept-btn-handler")
11 cookie_btn.click()
12 time.sleep(1)
13except:
14 pass # No apareció el popup de cookies
15# Extraer datos de hoteles
16cards = driver.find_elements(By.CSS_SELECTOR, 'div[data-test-attribute="location-results-card"]')
17hotels = []
18for card in cards:
19 try:
20 name = card.find_element(By.CSS_SELECTOR, 'div[data-automation="hotel-card-title"]').text
21 except:
22 name = None
23 try:
24 rating = card.find_element(By.CSS_SELECTOR, '[data-automation="bubbleRatingValue"]').text
25 except:
26 rating = None
27 try:
28 reviews = card.find_element(By.CSS_SELECTOR, '[data-automation="bubbleReviewCount"]').text
29 except:
30 reviews = None
31 hotels.append({"name": name, "rating": rating, "review_count": reviews})
32print(f"Extraídos {len(hotels)} hoteles")
33for h in hotels[:3]:
34 print(h)
En mi máquina esto tardó unos 8 segundos para una sola página, frente a menos de 1 segundo con requests+BS4. Esa diferencia de 8x se acumula rápido cuando raspas cientos de páginas.
Expandir “Leer más” y extraer reseñas completas
Las páginas de reseñas truncan las opiniones largas detrás de un botón de “Leer más”. Selenium puede hacer clic en él:
1review_url = "https://www.tripadvisor.com/Hotel_Review-g187147-d188726-Reviews-Le_Marais_Hotel-Paris_Ile_de_France.html"
2driver.get(review_url)
3wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'div[data-reviewid]')))
4time.sleep(2)
5# Hacer clic en todos los botones de "Leer más"
6read_more_buttons = driver.find_elements(By.XPATH, '//button//*[contains(text(), "Read more")]/..')
7for btn in read_more_buttons:
8 try:
9 driver.execute_script("arguments[0].click();", btn)
10 time.sleep(0.3)
11 except:
12 pass
13# Extraer reseñas
14review_elements = driver.find_elements(By.CSS_SELECTOR, 'div[data-reviewid]')
15reviews = []
16for rev in review_elements:
17 try:
18 title = rev.find_element(By.CSS_SELECTOR, 'div[data-test-target="review-title"]').text
19 except:
20 title = None
21 try:
22 body = rev.find_element(By.CSS_SELECTOR, 'q.IRsGHoPm span').text
23 except:
24 try:
25 body = rev.find_element(By.CSS_SELECTOR, 'p.partial_entry').text
26 except:
27 body = None
28 try:
29 rating_class = rev.find_element(By.CSS_SELECTOR, 'div[data-test-target="review-rating"] span').get_attribute("class")
30 # La valoración está codificada en una clase como "ui_bubble_rating bubble_50" = 5.0
31 rating_num = [c for c in rating_class.split() if "bubble_" in c][0].replace("bubble_", "")
32 rating = int(rating_num) / 10
33 except:
34 rating = None
35 reviews.append({"title": title, "body": body, "rating": rating})
36print(f"Extraídas {len(reviews)} reseñas")
Añadir rotación de proxies a Selenium
Para scraping sostenido, necesitarás rotación de proxies. Como selenium-wire quedó obsoleto desde enero de 2024, usa el soporte de proxy integrado de Chrome:
1# Con proxy sin autenticación
2proxy = "http://your-proxy-address:port"
3options.add_argument(f"--proxy-server={proxy}")
4# Para proxies con autenticación, usa una extensión de Chrome o el protocolo BiDi de Selenium 4
Para rotar proxies de forma programática, crea una nueva instancia de driver con un proxy distinto para cada lote de solicitudes. No es elegante, pero sí fiable.
Enfoque 3: método del JSON oculto (sin parsear HTML)
La mayoría de guías omiten por completo este enfoque, y es una pena: es el más rápido y limpio de los tres. TripAdvisor incrusta datos estructurados en JSON directamente en sus páginas HTML, dentro de etiquetas <script> como variables JavaScript pageManifest y urqlCache. Extraer ese JSON te da datos más limpios (valoraciones como números, fechas en formato ISO) con menos solicitudes y sin necesidad de renderizar JavaScript.
Encontrar el JSON incrustado en el código fuente
La idea clave: puedes usar un simple requests.get() para obtener la página y luego extraer el JSON del HTML en bruto sin renderizar JavaScript.
1import requests
2import re
3import json
4headers = {
5 "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
6 "Accept-Language": "en-US,en;q=0.9",
7 "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
8 "Referer": "https://www.tripadvisor.com/",
9 "Sec-CH-UA": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
10 "Sec-CH-UA-Mobile": "?0",
11 "Sec-CH-UA-Platform": '"macOS"',
12}
13url = "https://www.tripadvisor.com/Hotel_Review-g188590-d194317-Reviews-NH_City_Centre_Amsterdam.html"
14response = requests.get(url, headers=headers)
15# Extraer el bloque JSON de pageManifest
16match = re.search(r"pageManifest:({.+?})};", response.text)
17if match:
18 page_data = json.loads(match.group(1))
19 print("Encontrados datos en pageManifest")
20 print(f"Claves: {list(page_data.keys())[:10]}")
Cómo encontrar tú mismo el nombre de la variable: abre cualquier página de hotel de TripAdvisor en Chrome, haz clic derecho → Ver código fuente de la página, luego Ctrl+F para buscar pageManifest, urqlCache o aggregateRating. Los datos están ahí, esperando a ser procesados.
Analizar el JSON y extraer datos estructurados
TripAdvisor también incrusta datos schema.org application/ld+json, que son aún más fáciles de extraer:
1from parsel import Selector
2sel = Selector(text=response.text)
3# Extraer datos estructurados JSON-LD
4json_ld_scripts = sel.xpath("//script[@type='application/ld+json']/text()").getall()
5for script in json_ld_scripts:
6 data = json.loads(script)
7 if isinstance(data, dict) and data.get("@type") in ["Hotel", "Restaurant", "TouristAttraction"]:
8 print(f"Nombre: {data.get('name')}")
9 print(f"Valoración: {data.get('aggregateRating', {}).get('ratingValue')}")
10 print(f"Número de reseñas: {data.get('aggregateRating', {}).get('reviewCount')}")
11 print(f"Rango de precios: {data.get('priceRange')}")
12 print(f"Dirección: {data.get('address', {}).get('streetAddress')}")
13 print(f"Coordenadas: {data.get('geo', {}).get('latitude')}, {data.get('geo', {}).get('longitude')}")
14 break
Los datos JSON-LD están incrustados en el HTML estático y NO requieren renderizado JavaScript. Te dan el nombre del establecimiento, la valoración agregada, el número de reseñas, la dirección, las coordenadas, el rango de precios y las URLs de fotos, todo sin analizar una sola etiqueta HTML.
Para datos más ricos (reseñas individuales, desglose de valoraciones, listas de servicios), necesitas el objeto urqlCache:
1# Extraer urqlCache para datos detallados de reseñas
2cache_match = re.search(r'"urqlCache"\s*:\s*({.+?})\s*,\s*"redux"', response.text)
3if cache_match:
4 cache_data = json.loads(cache_match.group(1))
5 # Navegar por la caché para encontrar datos de reseñas
6 for key, value in cache_data.items():
7 if "reviews" in str(value).lower()[:100]:
8 reviews_data = json.loads(value.get("data", "{}")) if isinstance(value, dict) else None
9 if reviews_data:
10 print(f"Encontrada entrada de caché de reseñas: {key[:50]}...")
11 break
Las rutas exactas del JSON cambian de vez en cuando cuando TripAdvisor actualiza su frontend, pero la estructura general — JSON-LD para datos resumen, urqlCache para datos detallados — se ha mantenido estable durante años.
Reverse engineering de la API GraphQL de TripAdvisor (avanzado)
Para extracción a gran escala, los endpoints GraphQL de TripAdvisor devuelven datos estructurados directamente. Es el método más rápido, pero también el que requiere más mantenimiento.
1import httpx
2import random
3import string
4def generate_request_id():
5 """Genera el valor del encabezado X-Requested-By"""
6 random_chars = ''.join(random.choices(string.ascii_letters + string.digits, k=180))
7 return f"TNI1625!{random_chars}"
8# Buscar hoteles en París
9search_payload = [{
10 "variables": {
11 "request": {
12 "query": "hotels in Paris",
13 "limit": 10,
14 "scope": "WORLDWIDE",
15 "locale": "en-US",
16 "scopeGeoId": 1,
17 "searchCenter": None,
18 "types": ["LOCATION", "QUERY_SUGGESTION", "RESCUE_RESULT"],
19 "locationTypes": ["GEO", "AIRPORT", "ACCOMMODATION", "ATTRACTION", "EATERY", "NEIGHBORHOOD"]
20 }
21 },
22 "extensions": {
23 "preRegisteredQueryId": "84b17ed122fbdbd4"
24 }
25}]
26graphql_headers = {
27 "Content-Type": "application/json",
28 "Accept": "*/*",
29 "Accept-Language": "en-US,en;q=0.9",
30 "Origin": "https://www.tripadvisor.com",
31 "Referer": "https://www.tripadvisor.com/Hotels",
32 "X-Requested-By": generate_request_id(),
33 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
34}
35with httpx.Client() as client:
36 response = client.post(
37 "https://www.tripadvisor.com/data/graphql/ids",
38 json=search_payload,
39 headers=graphql_headers
40 )
41 if response.status_code == 200:
42 results = response.json()
43 print(json.dumps(results, indent=2)[:1000])
44 else:
45 print(f"La solicitud GraphQL falló: {response.status_code}")
Para obtener reseñas vía GraphQL:
1review_payload = [{
2 "variables": {
3 "locationId": 194317, # NH City Centre Amsterdam
4 "offset": 0,
5 "limit": 20,
6 "filters": {},
7 "sortType": None,
8 "sortBy": "date",
9 "language": "en",
10 "doMachineTranslation": False,
11 "photosPerReviewLimit": 3
12 },
13 "extensions": {
14 "preRegisteredQueryId": "ef1a9f94012220d3"
15 }
16}]
17with httpx.Client() as client:
18 response = client.post(
19 "https://www.tripadvisor.com/data/graphql/ids",
20 json=review_payload,
21 headers=graphql_headers
22 )
23 if response.status_code == 200:
24 data = response.json()
25 reviews = data[0]["data"]["locations"][0]["reviewListPage"]["reviews"]
26 total = data[0]["data"]["locations"][0]["reviewListPage"]["totalCount"]
27 print(f"Total de reseñas: {total}")
28 for r in reviews[:3]:
29 print(f" [{r['rating']}/5] {r['title']} - {r['createdDate']}")
Advertencia importante: Los valores preRegisteredQueryId (como 84b17ed122fbdbd4 para búsqueda y ef1a9f94012220d3 para reseñas) pueden romperse cuando TripAdvisor vuelve a desplegar. Cuando eso ocurra, tus solicitudes fallarán sin avisar. Tendrás que volver a descubrir los IDs de consulta monitorizando las peticiones de red en las herramientas de desarrollador del navegador.
Por qué este método reduce la necesidad de proxies
La matemática es simple. Con requests+BS4, extraer 100 páginas de detalle de hoteles requiere 100 solicitudes. Con el método del JSON oculto, cada solicitud devuelve todos los datos que necesitas de una sola carga de página: sin solicitudes adicionales para expandir reseñas ni para cargar contenido dinámico. Con GraphQL, una sola llamada a la API puede devolver 20 reseñas a la vez. Menos solicitudes = menos exposición al rate limiting = menos necesidad de rotar proxies. Para proyectos pequeños o medianos (menos de 1.000 páginas), quizá no necesites proxies en absoluto si añades pausas razonables.
Extraer hoteles, restaurantes y atracciones con un único script reutilizable
Cuatro de cada cinco guías comparables solo cubren hoteles. Pero TripAdvisor tiene tres categorías principales de contenido, y los patrones de URL y los campos de datos cambian entre ellas. Así puedes construir una función que maneje las tres.
Campos disponibles por categoría
| Campo | Hoteles | Restaurantes | Atracciones |
|---|---|---|---|
| Nombre | ✅ | ✅ | ✅ |
| Valoración | ✅ | ✅ | ✅ |
| Número de reseñas | ✅ | ✅ | ✅ |
| Precio / rango de precios | ✅ | ✅ | A veces |
| Dirección | ✅ | ✅ | ✅ |
| Tipo de cocina | ❌ | ✅ | ❌ |
| Duración / tipo de tour | ❌ | ❌ | ✅ |
| Servicios | ✅ | ❌ | ❌ |
| Coordenadas | ✅ | ✅ | ✅ |
Crear una función reutilizable scrape_tripadvisor()
1import requests
2from bs4 import BeautifulSoup
3import pandas as pd
4import time
5import random
6import re
7import json
8def scrape_tripadvisor(category, location_id, location_name, num_pages=3):
9 """
10 Extrae listados de TripAdvisor para hoteles, restaurantes o atracciones.
11 Args:
12 category: "hotels", "restaurants" o "attractions"
13 location_id: Geo ID de TripAdvisor (por ejemplo, "187147" para París)
14 location_name: nombre apto para la URL (por ejemplo, "Paris_Ile_de_France")
15 num_pages: número de páginas a extraer
16 """
17 url_patterns = {
18 "hotels": "https://www.tripadvisor.com/Hotels-g{geo}-oa{offset}-{name}-Hotels.html",
19 "restaurants": "https://www.tripadvisor.com/Restaurants-g{geo}-oa{offset}-{name}.html",
20 "attractions": "https://www.tripadvisor.com/Attractions-g{geo}-oa{offset}-Activities-{name}.html",
21 }
22 first_page_patterns = {
23 "hotels": "https://www.tripadvisor.com/Hotels-g{geo}-{name}-Hotels.html",
24 "restaurants": "https://www.tripadvisor.com/Restaurants-g{geo}-{name}.html",
25 "attractions": "https://www.tripadvisor.com/Attractions-g{geo}-Activities-{name}.html",
26 }
27 headers = {
28 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
29 "Accept-Language": "en-US,en;q=0.9",
30 "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
31 "Referer": "https://www.tripadvisor.com/",
32 "Sec-CH-UA": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
33 "Sec-CH-UA-Mobile": "?0",
34 "Sec-CH-UA-Platform": '"Windows"',
35 }
36 session = requests.Session()
37 session.headers.update(headers)
38 all_items = []
39 for page in range(num_pages):
40 offset = page * 30
41 if page == 0:
42 url = first_page_patterns[category].format(geo=location_id, name=location_name)
43 else:
44 url = url_patterns[category].format(geo=location_id, offset=offset, name=location_name)
45 response = session.get(url)
46 if response.status_code != 200:
47 print(f" Página {page + 1}: estado {response.status_code}, deteniendo.")
48 break
49 soup = BeautifulSoup(response.text, "html.parser")
50 cards = soup.select('div[data-test-attribute="location-results-card"]')
51 for card in cards:
52 item = {"category": category}
53 title_el = card.select_one('div[data-automation="hotel-card-title"]') or card.select_one('a[data-automation]')
54 item["name"] = title_el.get_text(strip=True) if title_el else None
55 rating_el = card.select_one('[data-automation="bubbleRatingValue"]')
56 item["rating"] = rating_el.get_text(strip=True) if rating_el else None
57 review_el = card.select_one('[data-automation="bubbleReviewCount"]')
58 item["review_count"] = review_el.get_text(strip=True) if review_el else None
59 all_items.append(item)
60 print(f" Página {page + 1}: {len(cards)} elementos encontrados")
61 time.sleep(random.uniform(3, 7))
62 return pd.DataFrame(all_items)
63# Ejemplos de uso
64print("=== Hoteles en París ===")
65hotels_df = scrape_tripadvisor("hotels", "187147", "Paris_Ile_de_France", num_pages=2)
66print(hotels_df.head())
67print("\n=== Restaurantes en Roma ===")
68restaurants_df = scrape_tripadvisor("restaurants", "187791", "Rome_Lazio", num_pages=2)
69print(restaurants_df.head())
70print("\n=== Atracciones en Barcelona ===")
71attractions_df = scrape_tripadvisor("attractions", "187497", "Barcelona_Catalonia", num_pages=2)
72print(attractions_df.head())
Una sola función, tres categorías, cero duplicación de código. Si TripAdvisor cambia un selector, lo corriges en un único sitio.
Qué hacer cuando TripAdvisor te bloquea (resolución de problemas anti-bot)
Esta es la sección que más necesitaba cuando empecé a extraer datos de TripAdvisor, y también la que ninguna guía comparable ofrece de forma estructurada. TripAdvisor combina DataDome (que analiza al día) y Cloudflare WAF. Aquí tienes una tabla de diagnóstico de los fallos más comunes:
| Síntoma | Causa probable | Solución |
|---|---|---|
| Respuesta HTTP 403 | Cabeceras ausentes o sospechosas; desafío JS de Cloudflare | Configura cabeceras realistas User-Agent, Accept-Language, Referer y Sec-CH-UA. Mantén coherencia entre cabeceras. |
| Página CAPTCHA en lugar de datos | Rate limiting o fingerprinting del navegador | Rota proxies residenciales, añade retrasos aleatorios (2–7 segundos entre solicitudes) |
| HTML vacío o cuerpo en blanco | JavaScript no renderizado por requests | Cambia a Selenium o extrae desde el JSON oculto en el código fuente |
| Reseñas parciales / “Read more” no se expande | Contenido cargado en el evento de clic | Usa .click() con Selenium o extrae desde el bloque JSON incrustado |
| Las reseñas aparecen solo en un idioma | Falta el parámetro de idioma | Añade ?filterLang=ALL a la URL de reseñas |
| Los datos dejan de cargarse tras N páginas | Límite por sesión | Rota sesiones, borra cookies entre lotes |
| HTTP 1020 Access Denied | IP/ASN bloqueado por Cloudflare | Cambia de proxies de centro de datos a proxies residenciales |
| Bucle de desafío (CAPTCHA infinito) | Persistencia de cookies rota | Calienta las sesiones visitando primero la página principal; conserva la cookie jar |
Lógica de reintentos con backoff exponencial
Ninguna de las guías comparables muestra realmente este código. Aquí tienes una función reutilizable de reintento:
1import time
2import random
3import requests
4def fetch_with_retry(session, url, max_retries=4, base_delay=2, max_delay=60):
5 """
6 Recupera una URL con backoff exponencial y jitter.
7 Rota el User-Agent en cada reintento.
8 """
9 user_agents = [
10 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
11 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
12 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
13 "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
14 ]
15 for attempt in range(max_retries):
16 # Rotar User-Agent en cada reintento
17 if attempt > 0:
18 session.headers["User-Agent"] = random.choice(user_agents)
19 try:
20 response = session.get(url, timeout=30)
21 if response.status_code == 200:
22 return response
23 if response.status_code == 429:
24 # Respetar el encabezado Retry-After si existe
25 retry_after = int(response.headers.get("Retry-After", base_delay * (2 ** attempt)))
26 print(f" Límite de velocidad (429). Esperando {retry_after}s...")
27 time.sleep(retry_after)
28 continue
29 if response.status_code in (403, 503):
30 wait = min(base_delay * (2 ** attempt) + random.uniform(0, 1), max_delay)
31 print(f" Obtenido {response.status_code}. Reintento {attempt + 1}/{max_retries} en {wait:.1f}s...")
32 time.sleep(wait)
33 continue
34 # Otros códigos de error: no reintentar
35 print(f" Estado inesperado {response.status_code} para {url}")
36 return response
37 except requests.exceptions.Timeout:
38 wait = min(base_delay * (2 ** attempt) + random.uniform(0, 1), max_delay)
39 print(f" Timeout. Reintento {attempt + 1}/{max_retries} en {wait:.1f}s...")
40 time.sleep(wait)
41 print(f" Agotados los {max_retries} reintentos para {url}")
42 return None
Rotar cabeceras, proxies y sesiones
Para scraping sostenido, mantén un conjunto de cabeceras y rótalas juntas:
1import random
2HEADER_SETS = [
3 {
4 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
5 "Sec-CH-UA": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
6 "Sec-CH-UA-Platform": '"Windows"',
7 },
8 {
9 "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
10 "Sec-CH-UA": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
11 "Sec-CH-UA-Platform": '"macOS"',
12 },
13 {
14 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
15 "Sec-CH-UA": '"Google Chrome";v="134", "Not-A.Brand";v="8", "Chromium";v="134"',
16 "Sec-CH-UA-Platform": '"Windows"',
17 },
18]
19PROXY_LIST = [
20 "http://user:pass@residential-proxy-1:port",
21 "http://user:pass@residential-proxy-2:port",
22 # Añade más proxies residenciales
23]
24def get_rotated_session():
25 """Crea una nueva sesión con cabeceras y proxy rotados."""
26 session = requests.Session()
27 # Elegir un conjunto aleatorio de cabeceras
28 header_set = random.choice(HEADER_SETS)
29 base_headers = {
30 "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
31 "Accept-Language": "en-US,en;q=0.9",
32 "Accept-Encoding": "gzip, deflate, br",
33 "Referer": "https://www.tripadvisor.com/",
34 "Sec-Fetch-Dest": "document",
35 "Sec-Fetch-Mode": "navigate",
36 "Sec-CH-UA-Mobile": "?0",
37 }
38 base_headers.update(header_set)
39 session.headers.update(base_headers)
40 # Elegir un proxy aleatorio
41 if PROXY_LIST:
42 proxy = random.choice(PROXY_LIST)
43 session.proxies = {"http": proxy, "https": proxy}
44 return session
El tipo de proxy importa. Los proxies de centro de datos se bloquean casi de inmediato en TripAdvisor (HTTP 1020 Access Denied). Para scraping sostenido, los proxies residenciales son obligatorios: salen por ISPs de consumo y resultan indistinguibles de usuarios reales. Espera pagar entre 2,50 y 8,40 dólares/GB según el proveedor.
Exportar y guardar los datos extraídos de TripAdvisor
Una vez que tienes los datos, pasarlos a un formato útil es sencillo.
Exportación a CSV (la más común)
1import pandas as pd
2df = pd.DataFrame(all_hotels)
3df.to_csv("tripadvisor_hotels_paris.csv", index=False, encoding="utf-8-sig")
4print(f"Exportadas {len(df)} filas a CSV")
encoding='utf-8-sig' es importante: garantiza que Excel muestre correctamente caracteres no latinos (acentos franceses, caracteres chinos, etc.) al abrir el CSV.
Exportación a JSON (para datos anidados)
Cuando tienes reseñas anidadas bajo hoteles, JSON conserva la jerarquía:
1# Estructura jerárquica
2hotel_data = {
3 "property_id": "d194317",
4 "name": "NH City Centre Amsterdam",
5 "rating": 4.0,
6 "reviews": [
7 {"title": "Great location", "rating": 5, "date": "2025-03-15", "text": "..."},
8 {"title": "Average stay", "rating": 3, "date": "2025-03-10", "text": "..."},
9 ]
10}
11# Para análisis plano, usa json_normalize
12flat_reviews = pd.json_normalize(
13 hotel_data,
14 record_path="reviews",
15 meta=["property_id", "name"]
16)
17flat_reviews.to_csv("reviews_flat.csv", index=False)
Enfoque de dos archivos para datos relacionales
Para conjuntos de datos grandes, suelo usar dos CSV:
hotels.csv— una fila por propiedad (plano)reviews.csv— una fila por reseña, conproperty_idcomo clave foránea
Así es fácil hacer joins en pandas, cargarlo en una base de datos o importarlo en herramientas de BI.
Si no quieres lidiar con la lógica de exportación, Thunderbit te permite a Excel, Google Sheets, Airtable o Notion: todo gratis, sin código. Muy útil cuando necesitas compartir resultados con compañeros no técnicos.
Consejos para un scraping responsable y eficiente de TripAdvisor
Scraping responsable en seis puntos:
- Revisa
robots.txt: elrobots.txtde TripAdvisor bloquea por completo bots de entrenamiento de IA (GPTBot, ClaudeBot, etc.). Los rastreadores estándar se enfrentan a restricciones selectivas por ruta. Revísalo entripadvisor.com/robots.txt. - Añade pausas: entre 3 y 7 segundos por solicitud es un rango seguro. Ir más rápido de 10–15 solicitudes por minuto por IP activa el rate limiting.
- Extrae solo datos públicos. No inicies sesión para acceder a contenido restringido.
- Almacena los datos de forma segura y cumple con GDPR/CCPA si tratas información personal (nombres de usuarios, etc.).
- Considera la API oficial de TripAdvisor si necesitas datos a escala comercial. El ofrece acceso a datos de negocio y hasta 5 reseñas y 5 fotos por ubicación: limitado, pero legal y estable.
- Ten presente el contexto legal: la reforzó las prohibiciones de scraping basadas en los Términos de Servicio en toda la UE. Los Términos de Servicio de TripAdvisor prohíben explícitamente el scraping. Hazlo con responsabilidad y bajo tu propia responsabilidad.
Conclusión
Ese es el panorama completo.
- Requests + BeautifulSoup es la ruta más sencilla. Funciona para páginas estáticas de listados, requiere una configuración mínima y es rápido. Empieza aquí si vas a extraer menos de 100 páginas y no necesitas contenido renderizado por JavaScript.
- Selenium resuelve todo lo que requests no puede: contenido dinámico, botones de “Leer más”, banners de cookies. Es 5 veces más lento y consume más recursos, pero es la única opción cuando necesitas interactuar con la página.
- JSON oculto / GraphQL es el enfoque más limpio y rápido. Te da datos estructurados sin parsear HTML, reduce el número de solicitudes (y por tanto la necesidad de proxies) y devuelve información lista para analizar. Requiere más reverse engineering inicial y mantenimiento ocasional cuando TripAdvisor cambia su estructura de datos.
La función reutilizable scrape_tripadvisor() cubre hoteles, restaurantes y atracciones. No deberías necesitar un segundo tutorial.
Y si en mitad del tutorial decides que programar no es lo tuyo — o simplemente necesitas 50 hoteles en una hoja de cálculo para final del día — la puede hacerlo en dos clics con detección de campos impulsada por IA, paginación automática y exportación gratis a Excel o Google Sheets. No necesitas Python.
Si quieres profundizar más, tenemos más guías de scraping en el y en nuestro .
Preguntas frecuentes
1. ¿Es legal extraer datos de TripAdvisor?
Los Términos de Servicio de TripAdvisor prohíben explícitamente el scraping. Sin embargo, los tribunales han sostenido en general que extraer datos públicos (no protegidos por inicio de sesión) no viola la Computer Fraud and Abuse Act en Estados Unidos. Dicho esto, la sentencia Ryanair del Tribunal de la UE de 2025 reforzó las restricciones basadas en Términos de Servicio en Europa. Extrae solo datos públicos, respeta robots.txt, no vuelvas a publicar contenido con copyright y consulta con un abogado si vas a usar los datos con fines comerciales.
2. ¿Puedo extraer TripAdvisor sin Python?
Sí. Herramientas sin código como pueden extraer TripAdvisor directamente desde el navegador con detección de campos mediante IA y paginación automática. También puedes usar extensiones de navegador, complementos de Google Sheets o APIs comerciales de scraping. Python te da el máximo control y flexibilidad, pero no es la única opción.
3. ¿Cómo evito que me bloqueen al extraer TripAdvisor?
Las tácticas clave: usa cabeceras realistas y coherentes (especialmente User-Agent y Sec-CH-UA), rota proxies residenciales (las IP de centro de datos se bloquean casi al instante), añade retrasos aleatorios de 3–7 segundos entre solicitudes, usa el método del JSON oculto para minimizar el número total de peticiones, implementa reintentos con backoff exponencial y calienta las sesiones visitando primero la página principal antes de entrar en páginas profundas.
4. ¿Qué datos puedo extraer de TripAdvisor?
Hoteles, restaurantes y atracciones, incluyendo nombres, valoraciones, número de reseñas, rangos de precio, direcciones, coordenadas, servicios (hoteles), tipo de cocina (restaurantes), duración de tours (atracciones) y texto completo de las reseñas con valoraciones individuales y fechas. Los enfoques de JSON oculto y GraphQL devuelven los datos más ricos por solicitud.
5. ¿Cuántas páginas puedo extraer de TripAdvisor al día?
Con una sola IP y pausas razonables: alrededor de 600–1.000 páginas al día. Con 20 proxies residenciales rotando: aproximadamente 200.000–300.000 páginas al día usando enfoques basados en requests. Selenium es más lento: calcula entre 8.000 y 12.000 páginas al día por proxy. El enfoque JSON oculto/GraphQL devuelve más datos por solicitud, así que quizá necesites muchas menos páginas totales para obtener la misma información.
Más información
