На прошлой неделе я попробовал выгрузить рейтинги отелей и количество отзывов примерно по 200 объектам в трёх европейских городах с TripAdvisor. Мой первый скрипт — обычный requests.get() со стандартными заголовками — на каждый запрос возвращал красивую ошибку 403 Forbidden. Ни одного байта полезных данных.
TripAdvisor — один из самых ценных открытых источников данных в туристической отрасли: более 1 миллиарда отзывов, свыше 8 миллионов карточек компаний и примерно 460 миллионов уникальных посетителей в месяц. Платформа влияет на более чем $60 миллиардов ежегодных расходов на поездки. Но получить эти данные программно? Вот тут и начинаются сложности. TripAdvisor использует DataDome для обнаружения ботов, Cloudflare WAF, TLS fingerprinting и JavaScript-challenges — многоуровневую защиту, которая блокирует большинство наивных попыток парсинга ещё до того, как они успеют начаться. Этот материал — тот самый ресурс, которого мне самому не хватало: сравнение трёх подходов к сбору данных на Python (плюс no-code вариант), готовый код для каждого метода, структурированный раздел по устранению антибот-проблем и переиспользуемые шаблоны для отелей, ресторанов и достопримечательностей. Если вы новичок в Python или уже опытный разработчик, этот гайд сэкономит вам кучу бесполезных 403.
Не хотите писать код? Соберите данные TripAdvisor простым способом
Сразу скажу честно: многие, кто ищет «scrape TripAdvisor with Python», на самом деле не горят желанием писать код. Им просто нужны данные — названия отелей, рейтинги, количество отзывов, цены — и желательно сразу в таблице. Если это про вас, есть куда более короткий путь.
Thunderbit — это расширение для Chrome с ИИ, которое мы разработали. Оно умеет читать любую страницу TripAdvisor и автоматически предлагать подходящие поля для извлечения. Рабочий процесс действительно укладывается в два клика:
- Откройте страницу со списком на TripAdvisor (например, результаты поиска «Hotels in Paris»).
- Нажмите «AI Suggest Fields» в боковой панели Thunderbit. ИИ просканирует страницу и предложит столбцы вроде Hotel Name, Rating, Review Count, Price и Location.
- Нажмите «Scrape». Thunderbit извлечёт данные из всех карточек на странице — а если нужно больше результатов, автоматически обработает пагинацию.
- Экспортируйте в Excel, Google Sheets, Airtable или Notion. Экспорт бесплатен на любом тарифе.
Thunderbit работает с отелями, ресторанами и достопримечательностями без каких-либо изменений в настройках — ИИ подстраивается под содержимое страницы. Для страниц с пагинацией он сам распознаёт кнопки «Next» и бесконечную прокрутку. А поскольку инструмент работает внутри вашего реального браузера Chrome, он использует ваши session cookies и browser fingerprint, что даёт естественное преимущество перед антибот-защитой.
Попробовать можно через Thunderbit Chrome Extension — бесплатный тариф даёт 6 страниц в месяц, этого достаточно, чтобы протестировать сценарий.
Если вам нужен программный контроль, собственная логика парсинга или вы планируете собрать 10 000+ страниц, тогда Python — ваш выбор. Читайте дальше.
Зачем парсить TripAdvisor на Python?
Данные TripAdvisor напрямую и измеримо влияют на бизнес. Исследование Cornell University показало, что рост Global Review Index отеля на 1 пункт из 100 приводит к увеличению средней дневной ставки на 0,89% и росту Revenue Per Available Room на 1,42%. Другое исследование ScienceDirect показало, что внешний рост рейтинга TripAdvisor на 1 звезду приносит отелю в среднем дополнительно $55 000–$75 000 в год. Отзывы — это не просто показатель для галочки, а реальный драйвер выручки.
Вот как разные команды используют данные TripAdvisor:
| Сценарий использования | Кто получает пользу | Какие данные нужны |
|---|---|---|
| Анализ конкурентов в гостиничном бизнесе | Сети отелей, revenue-менеджеры | Рейтинги, цены, объём отзывов, удобства |
| Исследование рынка ресторанов | Ресторанные группы, food-бренды | Тип кухни, ценовой диапазон, тональность отзывов |
| Отслеживание трендов по достопримечательностям | Туроператоры, туристические советы | Рейтинги популярности, сезонные паттерны |
| Анализ тональности | Исследователи, data-аналитики | Полные тексты отзывов, звёздные оценки, даты |
| Генерация лидов | Отделы продаж, туристические агентства | Названия компаний, контакты, локации |
Почему именно Python? Три причины. Во-первых, экосистема: BeautifulSoup, Selenium, Playwright, Scrapy, httpx, pandas — у Python более зрелые библиотеки для парсинга и анализа данных, чем у любого другого языка. Во-вторых, 71,7% разработчиков веб-скрейпинга используют Python, а значит, больше поддержки сообщества, больше ответов на StackOverflow и больше актуальных гайдов. В-третьих, удобство пайплайна: можно собирать данные через BeautifulSoup, очищать их через pandas, запускать анализ тональности через Hugging Face Transformers и строить дашборды — всё на одном языке. Без переключения контекста.
Три способа собрать данные TripAdvisor на Python (сравнение)
Во многих статьях выбирают один подход и строят весь материал вокруг него. Это не помогает, когда вам нужно принять решение до написания кода. Ниже — сравнительную таблицу, которую я сам хотел бы увидеть в начале:
| Подход | Скорость | Поддержка JS | Устойчивость к антибот-защите | Сложность | Лучше всего подходит для |
|---|---|---|---|---|---|
requests + BeautifulSoup | ⚡ Быстро (~120–200 страниц/мин в сыром виде) | ❌ Нет | ⚠️ Низкая | Просто | Статичные страницы со списками, небольшие проекты |
| Selenium / Headless Browser | 🐢 Медленно (~8–20 страниц/мин) | ✅ Полная | ⚠️ Средняя | Средняя | Динамический контент, клики по «Read more», cookie-баннеры |
| Скрытый JSON / GraphQL API | ⚡⚡ Самый быстрый (~200–600 страниц/мин в сыром виде) | N/A | ✅ Выше | Сложно | Массовое извлечение отзывов и карточек отелей |
| No-code (Thunderbit) | ⚡ Быстро | ✅ Встроено | ✅ Встроено | Самый простой | Неначинающие разработчики, разовые быстрые выгрузки |
Несколько важных оговорок. Указанные сырые скорости — теоретические: фактическую производительность ограничивает rate limit TripAdvisor (~10–15 запросов в минуту на IP), так что на практике вы упрётесь примерно в 10 страниц/мин на IP независимо от подхода. Метод со скрытым JSON даёт максимум данных за один запрос, а значит — меньше запросов в целом и ниже риск попасть под лимиты. Selenium в реальных тестах примерно в 5 раз медленнее, чем подходы на базе requests, зато это единственный вариант, если нужно нажимать кнопки или рендерить JavaScript.
Дальше в статье я разберу все три Python-метода с полным кодом. Выбирайте тот, который подходит под вашу задачу, или комбинируйте их (я часто использую requests+BS4 для списков и скрытый JSON для страниц с деталями).
Подготовка Python-окружения
Перед началом давайте настроим окружение. Вам понадобится Python 3.10+ (я рекомендую 3.12 или 3.13 — все основные пакеты поддерживают их без известных проблем).
Установите всё сразу:
pip install requests beautifulsoup4 selenium httpx parsel pandas curl-cffi
Примечания по пакетам:
requests(2.33.1) — HTTP-запросы, требует Python 3.10+beautifulsoup4(4.14.3) — парсинг HTMLselenium(4.43.0) — автоматизация браузера, требует Python 3.10+httpx(0.28.1) — асинхронный HTTP-клиентparsel(1.11.0) — CSS/XPath-селекторы, легче чем BS4pandas(3.0.2) — экспорт данных, требует Python 3.11+curl_cffi(0.15.0) — имитация TLS fingerprint, критично для обхода Cloudflare
ChromeDriver: если вы используете Selenium, есть хорошая новость — начиная с Selenium 4.6, Selenium Manager автоматически скачивает и кэширует нужный бинарник ChromeDriver. Ручная установка не нужна. Он динамически подбирает совместимую версию, так что вам не придётся переживать из-за несовпадения версий Chrome.
Виртуальное окружение (рекомендуется):
python -m venv tripadvisor-scraper
source tripadvisor-scraper/bin/activate # macOS/Linux
tripadvisor-scraper\Scripts\activate # Windows
Подход 1: парсинг TripAdvisor через Requests и BeautifulSoup
Это самый простой вариант. Он отлично подходит для страниц со списками — результатов поиска отелей или ресторанов, — где нужные данные уже есть в статичном HTML. Без браузера, без рендеринга JavaScript, с минимальным расходом ресурсов.
Понимаем структуру URL TripAdvisor
URL на TripAdvisor строятся по предсказуемым шаблонам в зависимости от категории:
- Отели:
https://www.tripadvisor.com/Hotels-g{locationId}-{Location_Name}-Hotels.html - Рестораны:
https://www.tripadvisor.com/Restaurants-g{locationId}-{Location_Name}.html - Достопримечательности:
https://www.tripadvisor.com/Attractions-g{locationId}-Activities-{Location_Name}.html
Пагинация использует параметр oa (offset anchors), который вставляется в URL. На каждой странице показывается 30 результатов:
- Страница 1: базовый URL (без
oa) - Страница 2:
Hotels-g187768-oa30-Italy-Hotels.html - Страница 3:
Hotels-g187768-oa60-Italy-Hotels.html
Для страниц с отзывами используется параметр or с шагом 10:
- Страница 1:
Reviews-or0-Hotel_Name.html - Страница 2:
Reviews-or10-Hotel_Name.html
Чтобы получить отзывы на всех языках, добавьте к URL ?filterLang=ALL.
Отправляем запросы с реалистичными заголовками
TripAdvisor очень внимательно проверяет заголовки. Запрос со стандартными Python-заголовками блокируется мгновенно. Нужно имитировать настоящий браузер Chrome:
import requests
import time
import random
session = requests.Session()
headers = {
"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",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Referer": "https://www.tripadvisor.com/",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-CH-UA": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
"Sec-CH-UA-Mobile": "?0",
"Sec-CH-UA-Platform": '"Windows"',
}
session.headers.update(headers)
url = "https://www.tripadvisor.com/Hotels-g187147-Paris_Ile_de_France-Hotels.html"
response = session.get(url)
print(f"Status: {response.status_code}")
print(f"Content length: {len(response.text)} characters")
Важная деталь: TripAdvisor проверяет, что User-Agent и заголовки Sec-CH-UA согласованы между собой. Если вы заявляете Chrome 135 в User-Agent, а в Sec-CH-UA у вас указан Chrome 120, вас, скорее всего, отметят как подозрительного клиента. Всегда ротируйте целые наборы заголовков, а не отдельные строки по одной.
Парсим список карточек через BeautifulSoup
Когда ответ успешно получен, извлечь данные можно через BeautifulSoup. TripAdvisor использует атрибуты data-automation и data-test-attribute, которые намного стабильнее, чем CSS-классы (они меняются довольно часто):
from bs4 import BeautifulSoup
soup = BeautifulSoup(response.text, "html.parser")
# Находим все карточки отелей
cards = soup.select('div[data-test-attribute="location-results-card"]')
hotels = []
for card in cards:
# Название отеля
title_el = card.select_one('div[data-automation="hotel-card-title"]')
name = title_el.get_text(strip=True) if title_el else None
# Ссылка на страницу отеля
link_el = card.select_one('div[data-automation="hotel-card-title"] a')
link = "https://www.tripadvisor.com" + link_el["href"] if link_el else None
# Рейтинг
rating_el = card.select_one('[data-automation="bubbleRatingValue"]')
rating = rating_el.get_text(strip=True) if rating_el else None
# Количество отзывов
review_el = card.select_one('[data-automation="bubbleReviewCount"]')
review_count = review_el.get_text(strip=True).replace(",", "").split()[0] if review_el else None
hotels.append({
"name": name,
"rating": rating,
"review_count": review_count,
"url": link,
})
print(f"Найдено {len(hotels)} отелей на этой странице")
for h in hotels[:3]:
print(h)
Про селекторы: TripAdvisor использует замаскированные CSS-классы вроде FGwzt, yyzcQ, которые меняются при каждом обновлении сайта. Атрибуты data-automation и data-test-target гораздо стабильнее. Всегда предпочитайте data-атрибуты CSS-классам.
Работа с пагинацией
Чтобы собрать данные со многих страниц, нужно пройтись по параметру offset с небольшой задержкой между запросами:
import pandas as pd
all_hotels = []
base_url = "https://www.tripadvisor.com/Hotels-g187147-oa{offset}-Paris_Ile_de_France-Hotels.html"
for page in range(5): # Первые 5 страниц
offset = page * 30
url = base_url.format(offset=offset) if page > 0 else "https://www.tripadvisor.com/Hotels-g187147-Paris_Ile_de_France-Hotels.html"
response = session.get(url)
if response.status_code != 200:
print(f"Страница {page + 1}: получен статус {response.status_code}, останавливаемся.")
break
soup = BeautifulSoup(response.text, "html.parser")
cards = soup.select('div[data-test-attribute="location-results-card"]')
for card in cards:
title_el = card.select_one('div[data-automation="hotel-card-title"]')
name = title_el.get_text(strip=True) if title_el else None
rating_el = card.select_one('[data-automation="bubbleRatingValue"]')
rating = rating_el.get_text(strip=True) if rating_el else None
review_el = card.select_one('[data-automation="bubbleReviewCount"]')
review_count = review_el.get_text(strip=True).replace(",", "").split()[0] if review_el else None
all_hotels.append({"name": name, "rating": rating, "review_count": review_count})
print(f"Страница {page + 1}: найдено {len(cards)} отелей")
time.sleep(random.uniform(3, 7)) # Случайная задержка, чтобы не упереться в лимиты
df = pd.DataFrame(all_hotels)
print(f"\nВсего собрано отелей: {len(df)}")
time.sleep(random.uniform(3, 7)) здесь очень важен. Порог rate limit у TripAdvisor — примерно 10–15 запросов в минуту на IP. Если идти быстрее, можно получить CAPTCHA или ошибку 429.
Ограничения этого подхода
Когда этот метод перестаёт работать? Requests+BS4 ломается, если:
- TripAdvisor отдаёт контент, отрендеренный JavaScript-ом (некоторые страницы результатов поиска требуют JS)
- Текст отзывов обрезан и скрыт за кнопкой «Read more»
- Антибот-защита усиливается до JavaScript-challenges или CAPTCHA
- Вам нужны данные, которые появляются только после client-side rendering (цены, доступность)
Для таких случаев нужен либо Selenium (Подход 2), либо скрытый JSON-метод (Подход 3).
Подход 2: парсинг TripAdvisor через Selenium (headless browser)
Selenium запускает настоящий браузер, а значит, умеет рендерить JavaScript, нажимать кнопки, проходить cookie-consent баннеры и взаимодействовать с динамическим контентом. Цена этого — примерно в 5 раз медленнее и 300–500 МБ RAM на один экземпляр браузера.
Настраиваем Selenium с антидетект-параметрами
«Из коробки» Selenium очень легко распознать. Fingerprinting TripAdvisor замечает его почти сразу. Нужно отключить флаги автоматизации:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
options = Options()
options.add_argument("--headless=new") # Новый headless-режим (Chrome 112+)
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_argument("--window-size=1920,1080")
options.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")
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option("useAutomationExtension", False)
driver = webdriver.Chrome(options=options)
# Убираем свойство webdriver из navigator
driver.execute_cdp_command("Page.addScriptToEvaluateOnNewDocument", {
"source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
})
Достаточно ли этого для TripAdvisor? Для небольших объёмов (до 50 страниц) такая настройка вместе с residential proxy обычно работает. Для больших объёмов может понадобиться undetected-chromedriver или nodriver — защита DataDome у TripAdvisor анализирует более 1000 сигналов на каждый запрос, включая TLS fingerprint, который обычный Selenium подделать не может.
Сбор результатов поиска отелей через Selenium
import time
import random
url = "https://www.tripadvisor.com/Hotels-g187147-Paris_Ile_de_France-Hotels.html"
driver.get(url)
# Ждём загрузки карточек отелей
wait = WebDriverWait(driver, 15)
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'div[data-test-attribute="location-results-card"]')))
# Обрабатываем всплывающее окно cookie consent, если оно появилось
try:
cookie_btn = driver.find_element(By.ID, "onetrust-accept-btn-handler")
cookie_btn.click()
time.sleep(1)
except:
pass # Баннер cookies не появился
# Извлекаем данные по отелям
cards = driver.find_elements(By.CSS_SELECTOR, 'div[data-test-attribute="location-results-card"]')
hotels = []
for card in cards:
try:
name = card.find_element(By.CSS_SELECTOR, 'div[data-automation="hotel-card-title"]').text
except:
name = None
try:
rating = card.find_element(By.CSS_SELECTOR, '[data-automation="bubbleRatingValue"]').text
except:
rating = None
try:
reviews = card.find_element(By.CSS_SELECTOR, '[data-automation="bubbleReviewCount"]').text
except:
reviews = None
hotels.append({"name": name, "rating": rating, "review_count": reviews})
print(f"Собрано {len(hotels)} отелей")
for h in hotels[:3]:
print(h)
На моей машине это заняло около 8 секунд на одну страницу — по сравнению с менее чем 1 секундой у requests+BS4. Если умножить эту разницу на сотни страниц, получится очень ощутимо.
Раскрываем «Read more» и собираем полные отзывы
Страницы отзывов обрезают длинные тексты за кнопкой «Read more». Selenium может нажать на неё:
review_url = "https://www.tripadvisor.com/Hotel_Review-g187147-d188726-Reviews-Le_Marais_Hotel-Paris_Ile_de_France.html"
driver.get(review_url)
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'div[data-reviewid]')))
time.sleep(2)
# Нажимаем все кнопки «Read more»
read_more_buttons = driver.find_elements(By.XPATH, '//button//*[contains(text(), "Read more")]/..')
for btn in read_more_buttons:
try:
driver.execute_script("arguments[0].click();", btn)
time.sleep(0.3)
except:
pass
# Извлекаем отзывы
review_elements = driver.find_elements(By.CSS_SELECTOR, 'div[data-reviewid]')
reviews = []
for rev in review_elements:
try:
title = rev.find_element(By.CSS_SELECTOR, 'div[data-test-target="review-title"]').text
except:
title = None
try:
body = rev.find_element(By.CSS_SELECTOR, 'q.IRsGHoPm span').text
except:
try:
body = rev.find_element(By.CSS_SELECTOR, 'p.partial_entry').text
except:
body = None
try:
rating_class = rev.find_element(By.CSS_SELECTOR, 'div[data-test-target="review-rating"] span').get_attribute("class")
# Рейтинг закодирован в классе вроде "ui_bubble_rating bubble_50" = 5.0
rating_num = [c for c in rating_class.split() if "bubble_" in c][0].replace("bubble_", "")
rating = int(rating_num) / 10
except:
rating = None
reviews.append({"title": title, "body": body, "rating": rating})
print(f"Собрано {len(reviews)} отзывов")
Добавляем ротацию прокси в Selenium
Для стабильного длительного парсинга нужна ротация прокси. Поскольку selenium-wire считается deprecated с января 2024 года, используйте встроенную поддержку прокси в Chrome:
# Для прокси без авторизации
proxy = "http://your-proxy-address:port"
options.add_argument(f"--proxy-server={proxy}")
# Для прокси с авторизацией используйте расширение Chrome или Selenium 4 BiDi protocol
Для программной ротации создавайте новый экземпляр драйвера с другим прокси для каждой партии запросов. Это не самый изящный вариант, но он надёжный.
Подход 3: скрытый JSON-метод (без парсинга HTML)
Большинство статей вообще не рассматривают этот способ, и зря — он самый быстрый и самый чистый из трёх. TripAdvisor встраивает структурированные данные прямо в HTML-страницы — внутри тегов <script> как JavaScript-переменные вроде pageManifest и urqlCache. Если извлечь этот JSON, вы получите более чистые данные (рейтинги как числа, даты в формате ISO) с меньшим числом запросов и без необходимости рендерить JavaScript.
Ищем встроенный JSON в исходнике страницы
Ключевая идея: можно просто сделать requests.get(), загрузить страницу и вытащить JSON из сырого HTML, вообще не рендеря JavaScript.
import requests
import re
import json
headers = {
"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",
"Accept-Language": "en-US,en;q=0.9",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Referer": "https://www.tripadvisor.com/",
"Sec-CH-UA": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
"Sec-CH-UA-Mobile": "?0",
"Sec-CH-UA-Platform": '"macOS"',
}
url = "https://www.tripadvisor.com/Hotel_Review-g188590-d194317-Reviews-NH_City_Centre_Amsterdam.html"
response = requests.get(url, headers=headers)
# Извлекаем JSON-блок pageManifest
match = re.search(r"pageManifest:({.+?})};", response.text)
if match:
page_data = json.loads(match.group(1))
print("Найдены данные pageManifest")
print(f"Ключи: {list(page_data.keys())[:10]}")
Как самостоятельно найти имя переменной: откройте любую страницу отеля TripAdvisor в Chrome, нажмите правой кнопкой мыши → View Page Source, затем Ctrl+F по pageManifest, urqlCache или aggregateRating. Данные уже там, их просто нужно распарсить.
Парсим JSON и извлекаем структурированные данные
TripAdvisor также встраивает schema.org данные application/ld+json, которые извлекать ещё проще:
from parsel import Selector
sel = Selector(text=response.text)
# Извлекаем структурированные данные JSON-LD
json_ld_scripts = sel.xpath("//script[@type='application/ld+json']/text()").getall()
for script in json_ld_scripts:
data = json.loads(script)
if isinstance(data, dict) and data.get("@type") in ["Hotel", "Restaurant", "TouristAttraction"]:
print(f"Название: {data.get('name')}")
print(f"Рейтинг: {data.get('aggregateRating', {}).get('ratingValue')}")
print(f"Количество отзывов: {data.get('aggregateRating', {}).get('reviewCount')}")
print(f"Ценовой диапазон: {data.get('priceRange')}")
print(f"Адрес: {data.get('address', {}).get('streetAddress')}")
print(f"Координаты: {data.get('geo', {}).get('latitude')}, {data.get('geo', {}).get('longitude')}")
break
Данные JSON-LD встроены в статический HTML и НЕ требуют рендеринга JavaScript. В них есть название объекта, агрегированный рейтинг, количество отзывов, адрес, координаты, ценовой диапазон и ссылки на фото — и всё это без разбора ни одного HTML-тега.
Для более богатых данных — отдельных отзывов, распределения оценок, списка удобств — нужен объект urqlCache:
# Извлекаем urqlCache для подробных данных по отзывам
cache_match = re.search(r'"urqlCache"\s*:\s*({.+?})\s*,\s*"redux"', response.text)
if cache_match:
cache_data = json.loads(cache_match.group(1))
# Ищем внутри кэша данные об отзывах
for key, value in cache_data.items():
if "reviews" in str(value).lower()[:100]:
reviews_data = json.loads(value.get("data", "{}")) if isinstance(value, dict) else None
if reviews_data:
print(f"Найдена запись кэша с отзывами: {key[:50]}...")
break
Точные JSON-пути иногда меняются, когда TripAdvisor обновляет фронтенд, но общая структура — JSON-LD для сводных данных, urqlCache для подробностей — остаётся стабильной уже много лет.
Реверс-инжиниринг GraphQL API TripAdvisor (для продвинутых)
Для массового сбора данных GraphQL-эндпоинты TripAdvisor возвращают уже структурированные данные напрямую. Это самый быстрый способ, но и самый требовательный к поддержке.
import httpx
import random
import string
def generate_request_id():
"""Генерирует значение заголовка X-Requested-By"""
random_chars = ''.join(random.choices(string.ascii_letters + string.digits, k=180))
return f"TNI1625!{random_chars}"
# Поиск отелей в Париже
search_payload = [{
"variables": {
"request": {
"query": "hotels in Paris",
"limit": 10,
"scope": "WORLDWIDE",
"locale": "en-US",
"scopeGeoId": 1,
"searchCenter": None,
"types": ["LOCATION", "QUERY_SUGGESTION", "RESCUE_RESULT"],
"locationTypes": ["GEO", "AIRPORT", "ACCOMMODATION", "ATTRACTION", "EATERY", "NEIGHBORHOOD"]
}
},
"extensions": {
"preRegisteredQueryId": "84b17ed122fbdbd4"
}
}]
graphql_headers = {
"Content-Type": "application/json",
"Accept": "*/*",
"Accept-Language": "en-US,en;q=0.9",
"Origin": "https://www.tripadvisor.com",
"Referer": "https://www.tripadvisor.com/Hotels",
"X-Requested-By": generate_request_id(),
"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",
}
with httpx.Client() as client:
response = client.post(
"https://www.tripadvisor.com/data/graphql/ids",
json=search_payload,
headers=graphql_headers
)
if response.status_code == 200:
results = response.json()
print(json.dumps(results, indent=2)[:1000])
else:
print(f"GraphQL-запрос не удался: {response.status_code}")
Для получения отзывов через GraphQL:
review_payload = [{
"variables": {
"locationId": 194317, # NH City Centre Amsterdam
"offset": 0,
"limit": 20,
"filters": {},
"sortType": None,
"sortBy": "date",
"language": "en",
"doMachineTranslation": False,
"photosPerReviewLimit": 3
},
"extensions": {
"preRegisteredQueryId": "ef1a9f94012220d3"
}
}]
with httpx.Client() as client:
response = client.post(
"https://www.tripadvisor.com/data/graphql/ids",
json=review_payload,
headers=graphql_headers
)
if response.status_code == 200:
data = response.json()
reviews = data[0]["data"]["locations"][0]["reviewListPage"]["reviews"]
total = data[0]["data"]["locations"][0]["reviewListPage"]["totalCount"]
print(f"Всего отзывов: {total}")
for r in reviews[:3]:
print(f" [{r['rating']}/5] {r['title']} - {r['createdDate']}")
Важная оговорка: значения preRegisteredQueryId (например, 84b17ed122fbdbd4 для поиска и ef1a9f94012220d3 для отзывов) могут перестать работать после нового деплоя TripAdvisor. В таком случае запросы будут молча падать. Вам придётся заново находить эти ID, отслеживая сетевые запросы в DevTools браузера.
Почему этот метод снижает потребность в прокси
Логика проста. При requests+BS4 сбор 100 страниц с карточками отелей требует 100 запросов. В скрытом JSON-методе один запрос отдаёт все нужные данные со страницы целиком — без дополнительных запросов на раскрытие отзывов или загрузку динамического контента. С GraphQL один API-вызов может вернуть сразу 20 отзывов. Меньше запросов = меньше риск попасть под rate limiting = меньше необходимости в ротации прокси. Для проектов небольшого и среднего масштаба (до 1 000 страниц) вам, возможно, вообще не понадобятся прокси, если вы добавите разумные задержки.
Собираем отели, рестораны и достопримечательности одним переиспользуемым скриптом
Четыре из пяти конкурирующих гайдов рассматривают только отели. Но у TripAdvisor три ключевые категории контента, и шаблоны URL, а также наборы полей в них различаются. Ниже — как собрать одну функцию, которая поддерживает все три.
Какие поля доступны по категориям
| Поле | Отели | Рестораны | Достопримечательности |
|---|---|---|---|
| Название | ✅ | ✅ | ✅ |
| Рейтинг | ✅ | ✅ | ✅ |
| Количество отзывов | ✅ | ✅ | ✅ |
| Цена / ценовой диапазон | ✅ | ✅ | Иногда |
| Адрес | ✅ | ✅ | ✅ |
| Тип кухни | ❌ | ✅ | ❌ |
| Продолжительность / тип тура | ❌ | ❌ | ✅ |
| Удобства | ✅ | ❌ | ❌ |
| Координаты | ✅ | ✅ | ✅ |
Строим переиспользуемую функцию scrape_tripadvisor()
import requests
from bs4 import BeautifulSoup
import pandas as pd
import time
import random
import re
import json
def scrape_tripadvisor(category, location_id, location_name, num_pages=3):
"""
Собирает списки TripAdvisor для отелей, ресторанов или достопримечательностей.
Args:
category: "hotels", "restaurants" или "attractions"
location_id: Geo ID TripAdvisor (например, "187147" для Парижа)
location_name: URL-friendly имя (например, "Paris_Ile_de_France")
num_pages: Количество страниц для сбора
"""
url_patterns = {
"hotels": "https://www.tripadvisor.com/Hotels-g{geo}-oa{offset}-{name}-Hotels.html",
"restaurants": "https://www.tripadvisor.com/Restaurants-g{geo}-oa{offset}-{name}.html",
"attractions": "https://www.tripadvisor.com/Attractions-g{geo}-oa{offset}-Activities-{name}.html",
}
first_page_patterns = {
"hotels": "https://www.tripadvisor.com/Hotels-g{geo}-{name}-Hotels.html",
"restaurants": "https://www.tripadvisor.com/Restaurants-g{geo}-{name}.html",
"attractions": "https://www.tripadvisor.com/Attractions-g{geo}-Activities-{name}.html",
}
headers = {
"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",
"Accept-Language": "en-US,en;q=0.9",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Referer": "https://www.tripadvisor.com/",
"Sec-CH-UA": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
"Sec-CH-UA-Mobile": "?0",
"Sec-CH-UA-Platform": '"Windows"',
}
session = requests.Session()
session.headers.update(headers)
all_items = []
for page in range(num_pages):
offset = page * 30
if page == 0:
url = first_page_patterns[category].format(geo=location_id, name=location_name)
else:
url = url_patterns[category].format(geo=location_id, offset=offset, name=location_name)
response = session.get(url)
if response.status_code != 200:
print(f" Страница {page + 1}: статус {response.status_code}, останавливаемся.")
break
soup = BeautifulSoup(response.text, "html.parser")
cards = soup.select('div[data-test-attribute="location-results-card"]')
for card in cards:
item = {"category": category}
title_el = card.select_one('div[data-automation="hotel-card-title"]') or card.select_one('a[data-automation]')
item["name"] = title_el.get_text(strip=True) if title_el else None
rating_el = card.select_one('[data-automation="bubbleRatingValue"]')
item["rating"] = rating_el.get_text(strip=True) if rating_el else None
review_el = card.select_one('[data-automation="bubbleReviewCount"]')
item["review_count"] = review_el.get_text(strip=True) if review_el else None
all_items.append(item)
print(f" Страница {page + 1}: найдено {len(cards)} объектов")
time.sleep(random.uniform(3, 7))
return pd.DataFrame(all_items)
# Примеры использования
print("=== Отели в Париже ===")
hotels_df = scrape_tripadvisor("hotels", "187147", "Paris_Ile_de_France", num_pages=2)
print(hotels_df.head())
print("\n=== Рестораны в Риме ===")
restaurants_df = scrape_tripadvisor("restaurants", "187791", "Rome_Lazio", num_pages=2)
print(restaurants_df.head())
print("\n=== Достопримечательности в Барселоне ===")
attractions_df = scrape_tripadvisor("attractions", "187497", "Barcelona_Catalonia", num_pages=2)
print(attractions_df.head())
Одна функция, три категории, ноль дублирования кода. Если TripAdvisor изменит селектор, вы исправите это в одном месте.
Что делать, если TripAdvisor блокирует вас (антибот-диагностика)
Это тот самый раздел, который мне больше всего был нужен в начале работы с TripAdvisor, и которого нет в структурированном виде у большинства конкурирующих гайдов. TripAdvisor одновременно использует DataDome (анализирует 5+ триллионов точек данных в день) и Cloudflare WAF. Ниже — диагностическая таблица для самых частых сбоев:
| Симптом | Вероятная причина | Решение |
|---|---|---|
| Ответ HTTP 403 | Отсутствуют или вызывают подозрения заголовки; Cloudflare JS challenge | Установите реалистичные заголовки User-Agent, Accept-Language, Referer и Sec-CH-UA. Следите за их согласованностью. |
| Вместо данных появляется CAPTCHA | Rate limiting или browser fingerprinting | Используйте residential proxy, добавляйте случайные задержки (2–7 секунд между запросами) |
| Пустой HTML или пустое тело страницы | JavaScript не отрендерился через requests | Перейдите на Selenium или извлекайте данные из скрытого JSON в исходнике страницы |
| Отзывы отображаются частично / «Read more» не раскрывается | Контент загружается по событию click | Используйте .click() в Selenium или извлекайте данные из встроенного JSON-блока |
| Отзывы только на одном языке | Не указан параметр языка | Добавьте к URL отзывов ?filterLang=ALL |
| Данные перестают загружаться после N страниц | Лимит на уровне сессии | Ротируйте сессии, очищайте cookies между партиями |
| HTTP 1020 Access Denied | IP/ASN заблокирован Cloudflare | Замените datacenter proxy на residential proxy |
| Цикл challenge (бесконечная CAPTCHA) | Сломана сохранность cookies | Прогрейте сессию, сначала зайдя на главную страницу; сохраняйте cookie jar |
Логика повторов с экспоненциальной задержкой
В большинстве статей такого кода нет. Вот переиспользуемая функция повторных попыток:
import time
import random
import requests
def fetch_with_retry(session, url, max_retries=4, base_delay=2, max_delay=60):
"""
Загружает URL с экспоненциальной задержкой и случайным джиттером.
На каждой попытке меняет User-Agent.
"""
user_agents = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
"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",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
]
for attempt in range(max_retries):
# Меняем User-Agent при повторных попытках
if attempt > 0:
session.headers["User-Agent"] = random.choice(user_agents)
try:
response = session.get(url, timeout=30)
if response.status_code == 200:
return response
if response.status_code == 429:
# Учитываем Retry-After, если он есть
retry_after = int(response.headers.get("Retry-After", base_delay * (2 ** attempt)))
print(f" Rate limited (429). Ждём {retry_after}с...")
time.sleep(retry_after)
continue
if response.status_code in (403, 503):
wait = min(base_delay * (2 ** attempt) + random.uniform(0, 1), max_delay)
print(f" Получен {response.status_code}. Повтор {attempt + 1}/{max_retries} через {wait:.1f}с...")
time.sleep(wait)
continue
# Другие ошибки — не повторяем
print(f" Неожиданный статус {response.status_code} для {url}")
return response
except requests.exceptions.Timeout:
wait = min(base_delay * (2 ** attempt) + random.uniform(0, 1), max_delay)
print(f" Таймаут. Повтор {attempt + 1}/{max_retries} через {wait:.1f}с...")
time.sleep(wait)
print(f" Все {max_retries} попыток исчерпаны для {url}")
return None
Ротация заголовков, прокси и сессий
Для длительного парсинга держите пул наборов заголовков и ротируйте их вместе:
import random
HEADER_SETS = [
{
"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",
"Sec-CH-UA": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
"Sec-CH-UA-Platform": '"Windows"',
},
{
"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",
"Sec-CH-UA": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
"Sec-CH-UA-Platform": '"macOS"',
},
{
"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",
"Sec-CH-UA": '"Google Chrome";v="134", "Not-A.Brand";v="8", "Chromium";v="134"',
"Sec-CH-UA-Platform": '"Windows"',
},
]
PROXY_LIST = [
"http://user:pass@residential-proxy-1:port",
"http://user:pass@residential-proxy-2:port",
# Добавьте больше residential proxy
]
def get_rotated_session():
"""Создаёт новую сессию с ротацией заголовков и прокси."""
session = requests.Session()
# Выбираем случайный набор заголовков
header_set = random.choice(HEADER_SETS)
base_headers = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Referer": "https://www.tripadvisor.com/",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-CH-UA-Mobile": "?0",
}
base_headers.update(header_set)
session.headers.update(base_headers)
# Выбираем случайный прокси
if PROXY_LIST:
proxy = random.choice(PROXY_LIST)
session.proxies = {"http": proxy, "https": proxy}
return session
Тип прокси имеет значение. Datacenter proxy TripAdvisor обычно блокирует почти сразу (HTTP 1020 Access Denied). Для устойчивого сбора данных нужны residential proxy — они идут через сети обычных пользователей и выглядят как реальные посетители. Ожидайте стоимость примерно $2,50–$8,40 за GB в зависимости от провайдера.
Экспорт и хранение собранных данных TripAdvisor
Когда данные уже собраны, привести их к рабочему формату совсем несложно.
Экспорт в CSV — самый частый вариант
import pandas as pd
df = pd.DataFrame(all_hotels)
df.to_csv("tripadvisor_hotels_paris.csv", index=False, encoding="utf-8-sig")
print(f"Экспортировано {len(df)} строк в CSV")
encoding='utf-8-sig' здесь важен: он помогает Excel корректно отображать нелатинские символы (французские акценты, китайские иероглифы и т. п.) при открытии CSV.
Экспорт в JSON — для вложенных данных
Когда отзывы вложены внутрь отелей, JSON сохраняет иерархию:
# Иерархическая структура
hotel_data = {
"property_id": "d194317",
"name": "NH City Centre Amsterdam",
"rating": 4.0,
"reviews": [
{"title": "Great location", "rating": 5, "date": "2025-03-15", "text": "..."},
{"title": "Average stay", "rating": 3, "date": "2025-03-10", "text": "..."},
]
}
# Для плоского анализа используйте json_normalize
flat_reviews = pd.json_normalize(
hotel_data,
record_path="reviews",
meta=["property_id", "name"]
)
flat_reviews.to_csv("reviews_flat.csv", index=False)
Подход из двух файлов для реляционных данных
Для больших наборов данных я обычно использую два CSV-файла:
hotels.csv— одна строка на объект (плоская структура)reviews.csv— одна строка на отзыв, сproperty_idкак внешним ключом
Такой формат удобно объединять в pandas, загружать в базу данных или импортировать в BI-инструменты.
Если вы не хотите возиться со всей этой логикой экспорта, Thunderbit позволяет экспортировать собранные данные напрямую в Excel, Google Sheets, Airtable или Notion — бесплатно и без кода. Это особенно полезно, когда нужно быстро поделиться результатами с нетехническими коллегами.
Советы по ответственному и эффективному сбору данных с TripAdvisor
Шесть коротких правил ответственного парсинга:
- Проверьте
robots.txt: в robots.txt у TripAdvisor полностью заблокированы боты для обучения ИИ (GPTBot, ClaudeBot и т. п.). Обычные crawlers сталкиваются с точечными ограничениями по путям. Посмотрите файл по адресуtripadvisor.com/robots.txt. - Добавляйте задержки: 3–7 секунд между запросами — безопасный диапазон. Быстрее 10–15 запросов в минуту на IP лучше не идти, иначе сработает rate limiting.
- Собирайте только публичные данные. Не логиньтесь для доступа к закрытому контенту.
- Храните данные безопасно и соблюдайте GDPR/CCPA, если обрабатываете персональные данные (например, имена авторов отзывов).
- Рассмотрите официальный API TripAdvisor, если вам нужны данные для коммерческого использования в масштабе. Developer Portal даёт доступ к информации о бизнесе плюс до 5 отзывов и 5 фото на объект — ограниченно, но легально и стабильно.
- Учитывайте правовой контекст: решение суда ЕС по Ryanair (декабрь 2025) усилило ограничения на сбор данных, основанные на ToS, по всей Европе. В правилах TripAdvisor прямо запрещён scraping. Делайте это ответственно и на свой риск.
Итоги
Теперь картина полная.
- Requests + BeautifulSoup — самый простой путь. Он хорошо работает для статичных страниц со списками, требует минимальной настройки и работает быстро. Начинайте с него, если вам нужно меньше 100 страниц и не требуется контент, отрендеренный JavaScript-ом.
- Selenium закрывает всё, что не умеет requests: динамический контент, кнопки «Read more», cookie-баннеры. Он в 5 раз медленнее и требует больше ресурсов, но это единственный вариант, когда нужна интеракция со страницей.
- Hidden JSON / GraphQL — самый чистый и быстрый подход. Он даёт структурированные данные без парсинга HTML, снижает количество запросов, а значит и потребность в прокси, и возвращает информацию в формате, удобном для анализа. Но он требует больше реверс-инжиниринга на старте и периодической поддержки, когда TripAdvisor меняет структуру данных.
Переиспользуемая функция scrape_tripadvisor() покрывает отели, рестораны и достопримечательности. Второй туториал вам, скорее всего, не понадобится.
А если в середине чтения вы поймёте, что кодить — не ваше, или вам просто нужно 50 отелей в таблице к концу дня, расширение Thunderbit для Chrome справится с этим за два клика: ИИ сам определит поля, автоматически обработает пагинацию и бесплатно экспортирует в Excel или Google Sheets. Python не нужен.
Если хотите углубиться, у нас есть ещё больше разборов по парсингу в блоге Thunderbit и на нашем YouTube-канале.
FAQ
1. Законно ли парсить TripAdvisor?
В Terms of Service TripAdvisor прямо запрещён scraping. Однако суды в целом считают, что сбор публично доступных данных (не за логином) не нарушает Computer Fraud and Abuse Act в США. При этом решение суда ЕС по Ryanair в 2025 году усилило ограничения, основанные на ToS, в Европе. Собирайте только публичные данные, соблюдайте robots.txt, не перепубликуйте защищённый авторским правом контент и при коммерческом использовании проконсультируйтесь с юристом.
2. Можно ли собрать данные TripAdvisor без Python?
Да. No-code инструменты вроде Thunderbit могут собирать TripAdvisor прямо из браузера с ИИ-распознаванием полей и автоматической пагинацией. Также можно использовать расширения браузера, дополнения для Google Sheets или коммерческие scraping API. Python даёт максимальный контроль и гибкость, но это не единственный вариант.
3. Как не попасть под блокировку при сборе данных с TripAdvisor?
Ключевые тактики: используйте реалистичные и согласованные заголовки, особенно User-Agent и Sec-CH-UA; ротируйте residential proxy (datacenter IP блокируются почти сразу); добавляйте случайные задержки 3–7 секунд между запросами; минимизируйте число запросов через скрытый JSON-метод; внедряйте retry-логику с экспоненциальной задержкой; и прогревайте сессии, сначала заходя на главную страницу.
4. Какие данные можно собрать с TripAdvisor?
Отели, рестораны и достопримечательности — включая названия, рейтинги, количество отзывов, ценовые диапазоны, адреса, координаты, удобства (для отелей), типы кухни (для ресторанов), длительность туров (для достопримечательностей), а также полный текст отзывов с индивидуальными рейтингами и датами. Скрытый JSON и GraphQL обычно дают самый богатый набор данных за один запрос.
5. Сколько страниц TripAdvisor можно собирать в день?
С одним IP и разумными задержками — примерно 600–1 000 страниц в день. С 20 rotating residential proxy — около 200 000–300 000 страниц в день при подходе на базе requests. Selenium медленнее: ожидайте 8 000–12 000 страниц в день на один прокси. Скрытый JSON / GraphQL даёт больше данных на запрос, поэтому для того же объёма информации вам может понадобиться значительно меньше страниц.
Узнать больше


