La scorsa settimana ho provato a recuperare valutazioni degli hotel e numero di recensioni per circa 200 strutture in tre città europee da TripAdvisor. Il mio primo script — un semplice requests.get() con header predefiniti — ha restituito un elegante 403 Forbidden a ogni singola richiesta. Nemmeno un byte di dati utili.
TripAdvisor è una delle fonti di dati pubbliche più ricche del settore travel: oltre , più di 8 milioni di attività presenti e circa 460 milioni di visitatori unici al mese. Influenza oltre di spesa turistica annua. Ma ottenere quei dati in modo programmatico? Qui le cose si complicano. TripAdvisor usa DataDome per il rilevamento dei bot, Cloudflare WAF, fingerprinting TLS e challenge JavaScript: una difesa a più livelli che blocca la maggior parte dei tentativi di scraping ingenui ancora prima che partano. Questa guida è la risorsa che avrei voluto avere: un confronto diretto tra tre approcci Python per lo scraping (più un’opzione no-code), codice completo per ciascuno, una sezione strutturata per il troubleshooting anti-bot e pattern riutilizzabili che funzionano per hotel, ristoranti e attrazioni. Che tu sia un principiante di Python o uno sviluppatore esperto, ti farà risparmiare un sacco di 403 buttati via.
Non vuoi scrivere codice? Estrarre dati da TripAdvisor nel modo più semplice
Voglio essere chiaro su un punto. Molte persone che cercano "scrape TripAdvisor with Python" in realtà non sono davvero interessate a scrivere codice. Vogliono solo i dati — nomi degli hotel, valutazioni, numero di recensioni, prezzi — in un foglio di calcolo, e in fretta. Se ti riconosci in questa descrizione, c’è una strada molto più breve.
è un’estensione Chrome basata su AI che abbiamo creato e che può leggere qualsiasi pagina di TripAdvisor e suggerire automaticamente le colonne giuste da estrarre. Il flusso di lavoro richiede davvero due clic:
- Apri una pagina elenco di TripAdvisor (ad esempio i risultati di ricerca "Hotels in Paris").
- Fai clic su "AI Suggest Fields" nella barra laterale di Thunderbit. L’AI analizza la pagina e propone colonne come Nome hotel, Valutazione, Numero di recensioni, Prezzo e Posizione.
- Fai clic su "Scrape." Thunderbit estrae i dati da ogni risultato nella pagina — e gestisce automaticamente la paginazione se ti servono più risultati.
- Esporta in Excel, Google Sheets, Airtable o Notion. Le esportazioni sono gratuite in tutti i piani.
Thunderbit funziona con hotel, ristoranti e attrazioni senza alcuna modifica di configurazione — l’AI si adatta a ciò che trova sulla pagina. Per i risultati paginati, riconosce automaticamente i pulsanti "Next" e lo scroll infinito. E poiché gira dentro il tuo vero browser Chrome, eredita i cookie di sessione e il fingerprint del browser, il che gli dà un vantaggio naturale contro il rilevamento dei bot.
Puoi provarlo con la — il piano gratuito offre 6 pagine/mese, abbastanza per testare il flusso.
Se ti serve controllo programmatico, logica di parsing personalizzata o prevedi di estrarre 10.000+ pagine, allora Python è la strada giusta. Continua a leggere.
Perché estrarre dati da TripAdvisor con Python?
I dati di TripAdvisor hanno un impatto aziendale diretto e misurabile. Uno ha rilevato che un aumento di 1 punto nel Global Review Index di un hotel su 100 punti porta a un incremento dello 0,89% della tariffa media giornaliera e dell’1,42% del Revenue Per Available Room. Un altro mostra che un aumento esogeno di 1 stella nella valutazione TripAdvisor si traduce in 55.000–75.000 dollari di ricavi annui aggiuntivi per un hotel medio. Le recensioni non sono solo vanity metrics: sono un motore di fatturato.
Ecco come diversi team usano i dati di TripAdvisor:
| Caso d’uso | Chi ne beneficia | Dati necessari |
|---|---|---|
| Analisi competitiva hotel | Catene alberghiere, revenue manager | Valutazioni, prezzi, volume recensioni, servizi |
| Ricerca di mercato ristorazione | Gruppi di ristorazione, brand food | Tipo di cucina, fasce di prezzo, sentiment delle recensioni |
| Monitoraggio trend attrazioni | Tour operator, enti del turismo | Ranking di popolarità, pattern stagionali |
| Analisi del sentiment | Ricercatori, data analyst | Testo completo delle recensioni, stelle, date |
| Generazione lead | Team sales, agenzie di viaggio | Nomi attività, contatti, posizioni |
Perché proprio Python? Tre motivi. Primo: l’ecosistema. BeautifulSoup, Selenium, Playwright, Scrapy, httpx, pandas — Python ha librerie per scraping e analisi dati più mature di qualsiasi altro linguaggio. Secondo: il usa Python, quindi c’è più supporto della community, più risposte su StackOverflow e guide più aggiornate. Terzo: il vantaggio della pipeline. Puoi fare scraping con BeautifulSoup, pulire i dati con pandas, eseguire analisi del sentiment con Hugging Face Transformers e costruire dashboard — tutto nello stesso linguaggio. Nessun cambio di contesto.
Tre modi per estrarre dati da TripAdvisor con Python (a confronto)
Ogni guida concorrente sceglie un approccio e basta. Non è molto utile quando devi decidere prima di scrivere codice. Ecco la tabella comparativa che avrei voluto qualcuno mi mostrasse:
| Approccio | Velocità | Supporto JS | Resistenza anti-bot | Complessità | Ideale per |
|---|---|---|---|---|---|
requests + BeautifulSoup | ⚡ Veloce (~120–200 pagine/min grezzo) | ❌ Nessuno | ⚠️ Bassa | Facile | Pagine elenco statiche, progetti piccoli |
| Selenium / Browser headless | 🐢 Lento (~8–20 pagine/min) | ✅ Completo | ⚠️ Media | Media | Contenuti dinamici, click su "Read more", banner cookie |
| JSON nascosto / API GraphQL | ⚡⚡ Il più veloce (~200–600 pagine/min grezzo) | N/A | ✅ Più alta | Difficile | Estrazione su larga scala di recensioni/hotel |
| No-code (Thunderbit) | ⚡ Veloce | ✅ Integrato | ✅ Integrato | Facilissimo | Non sviluppatori, export rapidi una tantum |
Alcune avvertenze importanti. Quelle velocità grezze sono teoriche: i rate limit di TripAdvisor (~10–15 richieste al minuto per IP) limitano la velocità reale a circa 10 pagine/minuto per IP, indipendentemente dall’approccio. Il metodo JSON nascosto ti permette di ottenere più dati per richiesta, quindi meno richieste totali e minore esposizione ai limiti. Selenium è 5 volte più lento degli approcci basati su requests nei benchmark reali, ma è l’unica opzione quando devi cliccare pulsanti o renderizzare JavaScript.
Il resto di questa guida ti mostra tutti e tre i metodi Python con codice completo. Scegli quello più adatto alla tua situazione, oppure combinali (io uso spesso requests+BS4 per le pagine elenco e JSON nascosto per le pagine dettaglio).
Configurare l’ambiente Python
Prima di iniziare, sistemiamo l’ambiente. Ti serve Python 3.10+ (consiglio 3.12 o 3.13 — tutti i pacchetti principali li supportano senza problemi noti).
Installa tutto in un colpo solo:
1pip install requests beautifulsoup4 selenium httpx parsel pandas curl-cffi
Note sui pacchetti:
requests(2.33.1) — richieste HTTP, richiede Python 3.10+beautifulsoup4(4.14.3) — parsing HTMLselenium(4.43.0) — automazione browser, richiede Python 3.10+httpx(0.28.1) — client HTTP asincronoparsel(1.11.0) — selettori CSS/XPath (più leggero di BS4)pandas(3.0.2) — esportazione dati, richiede Python 3.11+curl-cffi(0.15.0) — impersonificazione del fingerprint TLS (fondamentale per aggirare Cloudflare)
ChromeDriver: se usi Selenium, buona notizia — da Selenium 4.6 in poi, Selenium Manager scarica e memorizza automaticamente il binario corretto di ChromeDriver. Nessuna installazione manuale necessaria. La corrispondenza delle versioni è gestita dinamicamente, quindi non devi preoccuparti dei mismatch con Chrome.
Ambiente virtuale (consigliato):
1python -m venv tripadvisor-scraper
2source tripadvisor-scraper/bin/activate # macOS/Linux
3tripadvisor-scraper\Scripts\activate # Windows
Approccio 1: estrarre dati da TripAdvisor con Requests e BeautifulSoup
Questo è l’approccio più semplice. Funziona bene per le pagine elenco (risultati hotel, liste di ristoranti) in cui i dati che ti servono sono presenti nell’HTML statico. Niente browser, niente rendering JavaScript, risorse minime.
Capire la struttura degli URL di TripAdvisor
Gli URL di TripAdvisor seguono pattern prevedibili in base alla categoria:
- Hotel:
https://www.tripadvisor.com/Hotels-g{locationId}-{Location_Name}-Hotels.html - Ristoranti:
https://www.tripadvisor.com/Restaurants-g{locationId}-{Location_Name}.html - Attrazioni:
https://www.tripadvisor.com/Attractions-g{locationId}-Activities-{Location_Name}.html
La paginazione usa il parametro oa (offset anchors), inserito nell’URL. Ogni pagina mostra 30 risultati:
- Pagina 1: URL base (senza parametro
oa) - Pagina 2:
Hotels-g187768-oa30-Italy-Hotels.html - Pagina 3:
Hotels-g187768-oa60-Italy-Hotels.html
Per le pagine recensioni, il parametro di offset è or con incrementi di 10:
- Pagina 1:
Reviews-or0-Hotel_Name.html - Pagina 2:
Reviews-or10-Hotel_Name.html
Per ottenere le recensioni in tutte le lingue, aggiungi ?filterLang=ALL all’URL.
Inviare richieste con header realistici
TripAdvisor controlla gli header in modo aggressivo. Una richiesta con gli header Python di default viene bloccata subito. Devi imitare un vero browser Chrome:
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")
Dettaglio importante: TripAdvisor verifica che User-Agent e i Client Hints Sec-CH-UA siano coerenti. Se dichiari di essere Chrome 135 nel User-Agent ma il tuo Sec-CH-UA indica Chrome 120, verrai segnalato. Ruota sempre set completi di header, non i singoli header.
Analizzare le liste con BeautifulSoup
Una volta ottenuta una risposta valida, estrai i dati con BeautifulSoup. TripAdvisor usa attributi data-automation e data-test-attribute più stabili dei nomi delle classi CSS, che cambiano spesso:
1from bs4 import BeautifulSoup
2soup = BeautifulSoup(response.text, "html.parser")
3# Trova tutte le card degli hotel
4cards = soup.select('div[data-test-attribute="location-results-card"]')
5hotels = []
6for card in cards:
7 # Nome 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 # Link alla pagina dettaglio
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 # Valutazione
14 rating_el = card.select_one('[data-automation="bubbleRatingValue"]')
15 rating = rating_el.get_text(strip=True) if rating_el else None
16 # Numero di recensioni
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"Trovati {len(hotels)} hotel in questa pagina")
26for h in hotels[:3]:
27 print(h)
Nota sui selettori: TripAdvisor usa nomi di classi CSS offuscati (come FGwzt, yyzcQ) che cambiano a ogni aggiornamento del sito. Gli attributi data-automation e data-test-target sono molto più stabili. Preferisci sempre gli attributi data rispetto alle classi.
Gestire la paginazione
Per estrarre più pagine, fai un loop sul parametro di offset con una pausa rispettosa tra le richieste:
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): # Prime 5 pagine
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"Pagina {page + 1}: status {response.status_code}, interrompo.")
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"Pagina {page + 1}: trovati {len(cards)} hotel")
22 time.sleep(random.uniform(3, 7)) # Ritardo casuale per evitare rate limiting
23df = pd.DataFrame(all_hotels)
24print(f"\nTotale hotel estratti: {len(df)}")
Il time.sleep(random.uniform(3, 7)) è importante. La soglia di rate limit di TripAdvisor è circa 10–15 richieste al minuto per IP. Andare più veloce attiva CAPTCHA o errori 429.
Limiti di questo approccio
Dove si rompe? L’approccio requests+BS4 fallisce quando:
- TripAdvisor serve contenuti renderizzati via JavaScript (alcune pagine risultati richiedono JS)
- Il testo delle recensioni è troncato dietro il pulsante "Read more"
- Le difese anti-bot si spingono fino a challenge JavaScript o CAPTCHA
- Ti servono dati visibili solo dopo il rendering client-side (prezzi, disponibilità)
Per questi casi, ti serve Selenium (Approccio 2) oppure il metodo JSON nascosto (Approccio 3).
Approccio 2: estrarre dati da TripAdvisor con Selenium (browser headless)
Selenium avvia un browser reale, quindi può renderizzare JavaScript, cliccare pulsanti, gestire i banner di consenso cookie e interagire con contenuti dinamici. Il prezzo da pagare: è circa e consuma 300–500 MB di RAM per ogni istanza del browser.
Configurare Selenium con impostazioni anti-detection
Di default, Selenium è facilmente rilevabile. Il fingerprinting di TripAdvisor lo intercetta subito. Devi disabilitare i flag di automazione:
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") # Usa il nuovo headless mode (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# Rimuovi la proprietà webdriver da navigator
15driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
16 "source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
17})
Basta per TripAdvisor? Per scraping su piccola scala (meno di 50 pagine), questa configurazione con proxy residenziali di solito funziona. Per volumi maggiori, potresti aver bisogno di undetected-chromedriver o nodriver: la protezione DataDome di TripAdvisor analizza oltre 1.000 segnali per richiesta, inclusi i fingerprint TLS che il Selenium standard non riesce a imitare.
Estrarre i risultati hotel con Selenium
1import time
2import random
3url = "https://www.tripadvisor.com/Hotels-g187147-Paris_Ile_de_France-Hotels.html"
4driver.get(url)
5# Aspetta il caricamento delle card hotel
6wait = WebDriverWait(driver, 15)
7wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'div[data-test-attribute="location-results-card"]')))
8# Gestisci il popup di consenso cookie (se appare)
9try:
10 cookie_btn = driver.find_element(By.ID, "onetrust-accept-btn-handler")
11 cookie_btn.click()
12 time.sleep(1)
13except:
14 pass # Nessun popup cookie
15# Estrai i dati degli hotel
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"Estratti {len(hotels)} hotel")
33for h in hotels[:3]:
34 print(h)
Sul mio computer ci sono voluti circa 8 secondi per una singola pagina — contro meno di 1 secondo con requests+BS4. Questa differenza di 8x si accumula rapidamente quando devi estrarre centinaia di pagine.
Espandere "Read More" ed estrarre le recensioni complete
Le pagine recensioni troncano i testi lunghi dietro un pulsante "Read more". Selenium può cliccarlo:
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# Clicca tutti i pulsanti "Read more"
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# Estrai le recensioni
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 valutazione è codificata nella classe come "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"Estratte {len(reviews)} recensioni")
Aggiungere la rotazione dei proxy a Selenium
Per uno scraping continuo, serve la rotazione dei proxy. Dato che selenium-wire è deprecato da gennaio 2024, usa il supporto proxy integrato di Chrome:
1# Con proxy senza autenticazione
2proxy = "http://your-proxy-address:port"
3options.add_argument(f"--proxy-server={proxy}")
4# Per proxy con autenticazione, usa una estensione Chrome o il protocollo BiDi di Selenium 4
Per ruotare i proxy in modo programmatico, crea una nuova istanza del driver con un proxy diverso per ogni batch di richieste. Non è elegante, ma è affidabile.
Approccio 3: metodo JSON nascosto (senza parsare l’HTML)
Molte guide saltano del tutto questo approccio, ed è un peccato: è il più veloce e pulito dei tre. TripAdvisor incorpora dati strutturati in JSON direttamente nelle pagine HTML — dentro tag <script> come variabili JavaScript tipo pageManifest e urqlCache. Estrarre questo JSON ti dà dati più puliti (valutazioni come numeri, date in formato ISO), con meno richieste e senza bisogno di rendering JavaScript.
Trovare il JSON incorporato nel sorgente pagina
L’idea chiave: puoi usare un semplice requests.get() per scaricare la pagina, poi estrarre il JSON dall’HTML grezzo senza mai renderizzare 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# Estrai il blocco JSON pageManifest
16match = re.search(r"pageManifest:({.+?})};", response.text)
17if match:
18 page_data = json.loads(match.group(1))
19 print("Trovati dati pageManifest")
20 print(f"Chiavi: {list(page_data.keys())[:10]}")
Come trovare da solo il nome della variabile: apri una pagina hotel di TripAdvisor in Chrome, clic destro → Visualizza sorgente pagina, poi Ctrl+F per pageManifest o urqlCache o aggregateRating. I dati sono lì, pronti per essere estratti.
Parsare il JSON ed estrarre dati strutturati
TripAdvisor incorpora anche dati schema.org application/ld+json che sono ancora più facili da estrarre:
1from parsel import Selector
2sel = Selector(text=response.text)
3# Estrai i dati strutturati 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"Nome: {data.get('name')}")
9 print(f"Valutazione: {data.get('aggregateRating', {}).get('ratingValue')}")
10 print(f"Numero recensioni: {data.get('aggregateRating', {}).get('reviewCount')}")
11 print(f"Fascia prezzo: {data.get('priceRange')}")
12 print(f"Indirizzo: {data.get('address', {}).get('streetAddress')}")
13 print(f"Coordinate: {data.get('geo', {}).get('latitude')}, {data.get('geo', {}).get('longitude')}")
14 break
I dati JSON-LD sono incorporati nell’HTML statico e NON richiedono rendering JavaScript. Ti restituiscono nome della struttura, rating aggregato, numero di recensioni, indirizzo, coordinate, fascia di prezzo e URL delle foto — senza dover parsare nemmeno un tag HTML.
Per dati più ricchi (recensioni individuali, breakdown delle valutazioni, liste dei servizi), serve l’oggetto urqlCache:
1# Estrai urqlCache per i dettagli delle recensioni
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 # Naviga nella cache per trovare i dati delle recensioni
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"Trovata voce di cache recensioni: {key[:50]}...")
11 break
I path JSON esatti cambiano di tanto in tanto quando TripAdvisor aggiorna il frontend, ma la struttura generale — JSON-LD per i dati di sintesi, urqlCache per i dati dettagliati — è stabile da anni.
Reverse engineering dell’API GraphQL di TripAdvisor (avanzato)
Per estrazioni su larga scala, gli endpoint GraphQL di TripAdvisor restituiscono i dati già strutturati. È il metodo più veloce, ma richiede anche più manutenzione.
1import httpx
2import random
3import string
4def generate_request_id():
5 """Genera il valore dell'header X-Requested-By"""
6 random_chars = ''.join(random.choices(string.ascii_letters + string.digits, k=180))
7 return f"TNI1625!{random_chars}"
8# Cerca hotel a Parigi
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"Richiesta GraphQL fallita: {response.status_code}")
Per recuperare recensioni via 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"Totale recensioni: {total}")
28 for r in reviews[:3]:
29 print(f" [{r['rating']}/5] {r['title']} - {r['createdDate']}")
Avvertenza importante: i valori preRegisteredQueryId (come 84b17ed122fbdbd4 per la ricerca e ef1a9f94012220d3 per le recensioni) possono rompersi quando TripAdvisor effettua nuovi deploy. Quando succede, le richieste falliscono senza messaggi evidenti. Dovrai riscoprire gli ID delle query monitorando le richieste di rete negli strumenti DevTools del browser.
Perché questo metodo riduce il bisogno di proxy
La matematica è semplice. Con requests+BS4, estrarre 100 pagine dettaglio hotel richiede 100 richieste. Con il metodo JSON nascosto, ogni richiesta restituisce tutti i dati necessari da un solo caricamento pagina — senza richieste aggiuntive per espandere recensioni o caricare contenuti dinamici. Con GraphQL, una singola chiamata API può restituire 20 recensioni alla volta. Meno richieste = meno esposizione ai rate limit = meno necessità di ruotare i proxy. Per progetti piccoli o medi (meno di 1.000 pagine), potresti anche non aver bisogno di proxy se aggiungi ritardi ragionevoli.
Estrarre hotel, ristoranti e attrazioni con un solo script riutilizzabile
Quattro guide su cinque coprono solo gli hotel. Ma TripAdvisor ha tre categorie principali di contenuto, e i pattern degli URL e i campi dati cambiano tra una e l’altra. Ecco come costruire una funzione che le gestisce tutte.
Campi disponibili per categoria
| Campo | Hotel | Ristoranti | Attrazioni |
|---|---|---|---|
| Nome | ✅ | ✅ | ✅ |
| Valutazione | ✅ | ✅ | ✅ |
| Numero recensioni | ✅ | ✅ | ✅ |
| Prezzo/Fascia di prezzo | ✅ | ✅ | A volte |
| Indirizzo | ✅ | ✅ | ✅ |
| Tipo di cucina | ❌ | ✅ | ❌ |
| Durata/Tipo tour | ❌ | ❌ | ✅ |
| Servizi | ✅ | ❌ | ❌ |
| Coordinate | ✅ | ✅ | ✅ |
Costruire una funzione scrape_tripadvisor() riutilizzabile
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 Estrae dati da TripAdvisor per hotel, ristoranti o attrazioni.
11 Args:
12 category: "hotels", "restaurants", o "attractions"
13 location_id: geo ID di TripAdvisor (es. "187147" per Parigi)
14 location_name: nome compatibile con l'URL (es. "Paris_Ile_de_France")
15 num_pages: numero di pagine da estrarre
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" Pagina {page + 1}: status {response.status_code}, interrompo.")
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" Pagina {page + 1}: trovati {len(cards)} elementi")
61 time.sleep(random.uniform(3, 7))
62 return pd.DataFrame(all_items)
63# Esempi di utilizzo
64print("=== Hotel a Parigi ===")
65hotels_df = scrape_tripadvisor("hotels", "187147", "Paris_Ile_de_France", num_pages=2)
66print(hotels_df.head())
67print("\n=== Ristoranti a Roma ===")
68restaurants_df = scrape_tripadvisor("restaurants", "187791", "Rome_Lazio", num_pages=2)
69print(restaurants_df.head())
70print("\n=== Attrazioni a Barcellona ===")
71attractions_df = scrape_tripadvisor("attractions", "187497", "Barcelona_Catalonia", num_pages=2)
72print(attractions_df.head())
Una sola funzione, tre categorie, zero duplicazione di codice. Se TripAdvisor cambia un selettore, lo correggi in un punto solo.
Cosa fare quando TripAdvisor ti blocca (troubleshooting anti-bot)
Questa è la sezione che mi sarebbe servita di più quando ho iniziato a fare scraping su TripAdvisor, ed è anche la sezione che nessuna guida concorrente organizza bene. TripAdvisor usa DataDome (che analizza al giorno) insieme a Cloudflare WAF. Ecco una tabella diagnostica per i guasti più comuni:
| Sintomo | Causa probabile | Soluzione |
|---|---|---|
| Risposta HTTP 403 | Header mancanti o sospetti; challenge JavaScript di Cloudflare | Imposta header realistici User-Agent, Accept-Language, Referer e Sec-CH-UA. Assicurati che siano coerenti. |
| Pagina CAPTCHA al posto dei dati | Rate limiting o fingerprinting del browser | Ruota proxy residenziali, aggiungi ritardi casuali (2–7 secondi tra le richieste) |
| HTML vuoto o body della pagina bianco | JavaScript non renderizzato da requests | Passa a Selenium oppure estrai il JSON nascosto dal sorgente pagina |
| Recensioni parziali / "Read more" non si espande | Contenuto caricato solo su click | Usa .click() con Selenium oppure estrai dal blob JSON incorporato |
| Recensioni solo in una lingua | Parametro lingua mancante | Aggiungi ?filterLang=ALL all’URL delle recensioni |
| I dati smettono di caricarsi dopo N pagine | Limite basato sulla sessione | Ruota le sessioni, cancella i cookie tra un batch e l’altro |
| HTTP 1020 Access Denied | IP/ASN bannato da Cloudflare | Passa da proxy datacenter a proxy residenziali |
| Loop di challenge (CAPTCHA infinito) | Persistenza dei cookie rotta | Scalda le sessioni visitando prima la homepage; mantieni il cookie jar |
Logica di retry con exponential backoff
Nessun articolo concorrente mostra davvero questo codice. Ecco una funzione di retry riutilizzabile:
1import time
2import random
3import requests
4def fetch_with_retry(session, url, max_retries=4, base_delay=2, max_delay=60):
5 """
6 Recupera un URL con exponential backoff e jitter.
7 Ruota il User-Agent a ogni retry.
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 # Ruota il User-Agent al retry
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 # Rispetta l'header Retry-After, se presente
25 retry_after = int(response.headers.get("Retry-After", base_delay * (2 ** attempt)))
26 print(f" Rate limit raggiunto (429). Attendo {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" Ricevuto {response.status_code}. Retry {attempt + 1}/{max_retries} tra {wait:.1f}s...")
32 time.sleep(wait)
33 continue
34 # Altri codici errore — non ritento
35 print(f" Status inatteso {response.status_code} per {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. Retry {attempt + 1}/{max_retries} tra {wait:.1f}s...")
40 time.sleep(wait)
41 print(f" Esauriti tutti i {max_retries} retry per {url}")
42 return None
Ruotare header, proxy e sessioni
Per uno scraping continuativo, mantieni un pool di set di header e ruotali insieme:
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 # Aggiungi altri proxy residenziali
23]
24def get_rotated_session():
25 """Crea una nuova sessione con header e proxy ruotati."""
26 session = requests.Session()
27 # Scegli un set di header casuale
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 # Scegli un proxy casuale
41 if PROXY_LIST:
42 proxy = random.choice(PROXY_LIST)
43 session.proxies = {"http": proxy, "https": proxy}
44 return session
Il tipo di proxy conta. I proxy datacenter vengono bloccati quasi subito da TripAdvisor (HTTP 1020 Access Denied). I proxy residenziali sono obbligatori per uno scraping continuativo: passano attraverso ISP consumer e sono indistinguibili dagli utenti reali. Aspettati di pagare $2,50–$8,40/GB a seconda del provider.
Esportare e salvare i dati estratti da TripAdvisor
Una volta ottenuti i dati, portarli in un formato utilizzabile è semplice.
Export CSV (il più comune)
1import pandas as pd
2df = pd.DataFrame(all_hotels)
3df.to_csv("tripadvisor_hotels_paris.csv", index=False, encoding="utf-8-sig")
4print(f"Esportate {len(df)} righe in CSV")
L’encoding='utf-8-sig' è importante: fa sì che Excel visualizzi correttamente i caratteri non latini (accenti francesi, caratteri cinesi, ecc.) quando apri il CSV.
Export JSON (per dati annidati)
Quando hai recensioni annidate sotto gli hotel, JSON preserva la gerarchia:
1# Struttura gerarchica
2hotel_data = {
3 "property_id": "d194317",
4 "name": "NH City Centre Amsterdam",
5 "rating": 4.0,
6 "reviews": [
7 {"title": "Ottima posizione", "rating": 5, "date": "2025-03-15", "text": "..."},
8 {"title": "Soggiorno nella media", "rating": 3, "date": "2025-03-10", "text": "..."},
9 ]
10}
11# Per analisi piatte, 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)
Approccio in due file per dati relazionali
Per dataset grandi, io uso due file CSV:
hotels.csv— una riga per struttura (flat)reviews.csv— una riga per recensione, conproperty_idcome chiave esterna
In questo modo è facile fare join in pandas, caricare i dati in un database o importarli in tool BI.
Se non vuoi gestire tutta questa logica di export, Thunderbit ti permette di in Excel, Google Sheets, Airtable o Notion — tutto gratis, senza codice. Utile quando devi condividere i risultati con colleghi non tecnici.
Consigli per uno scraping di TripAdvisor responsabile ed efficiente
Scraping responsabile in sei punti:
- Controlla
robots.txt: ilrobots.txtdi TripAdvisor blocca completamente i bot di AI training (GPTBot, ClaudeBot, ecc.). I crawler standard trovano restrizioni selettive su alcuni percorsi. Verificalo sutripadvisor.com/robots.txt. - Aggiungi ritardi: 3–7 secondi tra le richieste è un intervallo ragionevole. Andare più veloce di 10–15 richieste al minuto per IP fa scattare il rate limiting.
- Estrai solo dati pubblici. Non fare login per accedere a contenuti riservati.
- Conserva i dati in modo sicuro e rispetta GDPR/CCPA se gestisci informazioni personali (nomi dei recensori, ecc.).
- Valuta l’API ufficiale di TripAdvisor se ti servono dati su scala commerciale. Il offre accesso ai dettagli delle attività più fino a 5 recensioni e 5 foto per località: limitato, ma legale e stabile.
- Tieni presente il contesto legale: la ha rafforzato nell’UE le restrizioni basate sui Termini di servizio contro lo scraping. I Termini di TripAdvisor vietano esplicitamente lo scraping. Procedi con responsabilità e a tuo rischio.
Conclusione
Questo è il quadro completo.
- Requests + BeautifulSoup è il percorso più semplice. Funziona per le pagine elenco statiche, richiede poca configurazione ed è veloce. Parti da qui se estrai meno di 100 pagine e non ti servono contenuti renderizzati via JavaScript.
- Selenium gestisce tutto ciò che requests non può: contenuti dinamici, pulsanti "Read more", banner cookie. È 5 volte più lento e richiede più risorse, ma è l’unica opzione quando devi interagire con la pagina.
- JSON nascosto / GraphQL è l’approccio più pulito e veloce. Ti dà dati strutturati senza parsare HTML, riduce il numero di richieste (e quindi il bisogno di proxy) e restituisce dati già pronti per l’analisi. Richiede più reverse engineering iniziale e qualche manutenzione quando TripAdvisor cambia la struttura dei dati.
La funzione riutilizzabile scrape_tripadvisor() copre hotel, ristoranti e attrazioni. Non dovresti aver bisogno di un secondo tutorial.
E se a metà guida ti rendi conto che programmare non fa per te — oppure ti servono solo 50 hotel in un foglio di calcolo entro fine giornata — l’ può farlo in due clic, con rilevamento campi basato su AI, paginazione automatica ed export gratuito verso Excel o Google Sheets. Nessun Python richiesto.
Se vuoi andare più a fondo, trovi altri tutorial di scraping sul e sul nostro .
FAQ
1. È legale fare scraping di TripAdvisor?
I Termini di servizio di TripAdvisor vietano esplicitamente lo scraping. Tuttavia, in generale i tribunali hanno ritenuto che estrarre dati pubblicamente accessibili (non protetti da login) non violi il Computer Fraud and Abuse Act negli Stati Uniti. Detto questo, la sentenza UE Ryanair del 2025 ha rafforzato in Europa le restrizioni basate sui Termini di servizio. Estrai solo dati pubblici, rispetta robots.txt, non ripubblicare contenuti protetti da copyright e consulta un legale se usi i dati a fini commerciali.
2. Posso estrarre dati da TripAdvisor senza Python?
Sì. Strumenti no-code come possono estrarre dati da TripAdvisor direttamente dal browser con rilevamento campi basato su AI e paginazione automatica. Puoi anche usare estensioni browser, add-on per Google Sheets o API commerciali di scraping. Python offre il massimo controllo e flessibilità, ma non è l’unica opzione.
3. Come evito di farmi bloccare quando faccio scraping su TripAdvisor?
Le tattiche chiave: usa header realistici e coerenti (soprattutto User-Agent e Sec-CH-UA), ruota proxy residenziali (gli IP datacenter vengono bloccati subito), aggiungi ritardi casuali di 3–7 secondi tra le richieste, usa il metodo JSON nascosto per ridurre al minimo le richieste totali, implementa retry con exponential backoff e scalda le sessioni visitando la homepage prima di passare alle pagine profonde.
4. Quali dati posso estrarre da TripAdvisor?
Hotel, ristoranti e attrazioni — inclusi nomi, valutazioni, numero di recensioni, fasce di prezzo, indirizzi, coordinate, servizi (hotel), tipo di cucina (ristoranti), durata dei tour (attrazioni) e testo completo delle recensioni con singole valutazioni e date. Gli approcci JSON nascosto e GraphQL restituiscono i dati più ricchi per ogni richiesta.
5. Quante pagine posso estrarre da TripAdvisor al giorno?
Con un singolo IP e ritardi ragionevoli: circa 600–1.000 pagine al giorno. Con 20 proxy residenziali in rotazione: circa 200.000–300.000 pagine al giorno usando approcci basati su requests. Selenium è più lento: aspettati 8.000–12.000 pagine al giorno per proxy. L’approccio JSON nascosto/GraphQL ti fa ottenere più dati per richiesta, quindi potresti aver bisogno di molte meno pagine totali per raccogliere la stessa quantità di informazioni.
Scopri di più
