Как собрать данные с TripAdvisor на Python (и не попасть под блокировку)

Последнее обновление: April 17, 2026

На прошлой неделе я попробовал выгрузить рейтинги отелей и количество отзывов примерно по 200 объектам в трёх европейских городах с TripAdvisor. Мой первый скрипт — обычный requests.get() со стандартными заголовками — на каждый запрос возвращал красивую ошибку 403 Forbidden. Ни одного байта полезных данных.

TripAdvisor — один из самых ценных открытых источников данных в туристической отрасли: более , свыше 8 миллионов карточек компаний и примерно 460 миллионов уникальных посетителей в месяц. Платформа влияет на более чем ежегодных расходов на поездки. Но получить эти данные программно? Вот тут и начинаются сложности. TripAdvisor использует DataDome для обнаружения ботов, Cloudflare WAF, TLS fingerprinting и JavaScript-challenges — многоуровневую защиту, которая блокирует большинство наивных попыток парсинга ещё до того, как они успеют начаться. Этот материал — тот самый ресурс, которого мне самому не хватало: сравнение трёх подходов к сбору данных на Python (плюс no-code вариант), готовый код для каждого метода, структурированный раздел по устранению антибот-проблем и переиспользуемые шаблоны для отелей, ресторанов и достопримечательностей. Если вы новичок в Python или уже опытный разработчик, этот гайд сэкономит вам кучу бесполезных 403.

Не хотите писать код? Соберите данные TripAdvisor простым способом

Сразу скажу честно: многие, кто ищет «scrape TripAdvisor with Python», на самом деле не горят желанием писать код. Им просто нужны данные — названия отелей, рейтинги, количество отзывов, цены — и желательно сразу в таблице. Если это про вас, есть куда более короткий путь.

— это расширение для Chrome с ИИ, которое мы разработали. Оно умеет читать любую страницу TripAdvisor и автоматически предлагать подходящие поля для извлечения. Рабочий процесс действительно укладывается в два клика:

  1. Откройте страницу со списком на TripAdvisor (например, результаты поиска «Hotels in Paris»).
  2. Нажмите «AI Suggest Fields» в боковой панели Thunderbit. ИИ просканирует страницу и предложит столбцы вроде Hotel Name, Rating, Review Count, Price и Location.
  3. Нажмите «Scrape». Thunderbit извлечёт данные из всех карточек на странице — а если нужно больше результатов, автоматически обработает пагинацию.
  4. Экспортируйте в Excel, Google Sheets, Airtable или Notion. Экспорт бесплатен на любом тарифе.

Thunderbit работает с отелями, ресторанами и достопримечательностями без каких-либо изменений в настройках — ИИ подстраивается под содержимое страницы. Для страниц с пагинацией он сам распознаёт кнопки «Next» и бесконечную прокрутку. А поскольку инструмент работает внутри вашего реального браузера Chrome, он использует ваши session cookies и browser fingerprint, что даёт естественное преимущество перед антибот-защитой.

Попробовать можно через — бесплатный тариф даёт 6 страниц в месяц, этого достаточно, чтобы протестировать сценарий.

Если вам нужен программный контроль, собственная логика парсинга или вы планируете собрать 10 000+ страниц, тогда Python — ваш выбор. Читайте дальше.

Зачем парсить TripAdvisor на Python?

Данные TripAdvisor напрямую и измеримо влияют на бизнес. Исследование показало, что рост Global Review Index отеля на 1 пункт из 100 приводит к увеличению средней дневной ставки на 0,89% и росту Revenue Per Available Room на 1,42%. Другое исследование показало, что внешний рост рейтинга TripAdvisor на 1 звезду приносит отелю в среднем дополнительно $55 000–$75 000 в год. Отзывы — это не просто показатель для галочки, а реальный драйвер выручки.

Вот как разные команды используют данные TripAdvisor:

Сценарий использованияКто получает пользуКакие данные нужны
Анализ конкурентов в гостиничном бизнесеСети отелей, revenue-менеджерыРейтинги, цены, объём отзывов, удобства
Исследование рынка ресторановРесторанные группы, food-брендыТип кухни, ценовой диапазон, тональность отзывов
Отслеживание трендов по достопримечательностямТуроператоры, туристические советыРейтинги популярности, сезонные паттерны
Анализ тональностиИсследователи, data-аналитикиПолные тексты отзывов, звёздные оценки, даты
Генерация лидовОтделы продаж, туристические агентстваНазвания компаний, контакты, локации

Почему именно Python? Три причины. Во-первых, экосистема: BeautifulSoup, Selenium, Playwright, Scrapy, httpx, pandas — у Python более зрелые библиотеки для парсинга и анализа данных, чем у любого другого языка. Во-вторых, используют 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 — все основные пакеты поддерживают их без известных проблем).

Установите всё сразу:

1pip install requests beautifulsoup4 selenium httpx parsel pandas curl-cffi

Примечания по пакетам:

  • requests (2.33.1) — HTTP-запросы, требует Python 3.10+
  • beautifulsoup4 (4.14.3) — парсинг HTML
  • selenium (4.43.0) — автоматизация браузера, требует Python 3.10+
  • httpx (0.28.1) — асинхронный HTTP-клиент
  • parsel (1.11.0) — CSS/XPath-селекторы, легче чем BS4
  • pandas (3.0.2) — экспорт данных, требует Python 3.11+
  • curl_cffi (0.15.0) — имитация TLS fingerprint, критично для обхода Cloudflare

ChromeDriver: если вы используете Selenium, есть хорошая новость — начиная с Selenium 4.6, Selenium Manager автоматически скачивает и кэширует нужный бинарник ChromeDriver. Ручная установка не нужна. Он динамически подбирает совместимую версию, так что вам не придётся переживать из-за несовпадения версий Chrome.

Виртуальное окружение (рекомендуется):

1python -m venv tripadvisor-scraper
2source tripadvisor-scraper/bin/activate  # macOS/Linux
3tripadvisor-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:

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")

Важная деталь: TripAdvisor проверяет, что User-Agent и заголовки Sec-CH-UA согласованы между собой. Если вы заявляете Chrome 135 в User-Agent, а в Sec-CH-UA у вас указан Chrome 120, вас, скорее всего, отметят как подозрительного клиента. Всегда ротируйте целые наборы заголовков, а не отдельные строки по одной.

Парсим список карточек через BeautifulSoup

Когда ответ успешно получен, извлечь данные можно через BeautifulSoup. TripAdvisor использует атрибуты data-automation и data-test-attribute, которые намного стабильнее, чем CSS-классы (они меняются довольно часто):

1from bs4 import BeautifulSoup
2soup = BeautifulSoup(response.text, "html.parser")
3# Находим все карточки отелей
4cards = soup.select('div[data-test-attribute="location-results-card"]')
5hotels = []
6for card in cards:
7    # Название отеля
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    # Ссылка на страницу отеля
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    # Рейтинг
14    rating_el = card.select_one('[data-automation="bubbleRatingValue"]')
15    rating = rating_el.get_text(strip=True) if rating_el else None
16    # Количество отзывов
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"Найдено {len(hotels)} отелей на этой странице")
26for h in hotels[:3]:
27    print(h)

Про селекторы: TripAdvisor использует замаскированные CSS-классы вроде FGwzt, yyzcQ, которые меняются при каждом обновлении сайта. Атрибуты data-automation и data-test-target гораздо стабильнее. Всегда предпочитайте data-атрибуты CSS-классам.

Работа с пагинацией

Чтобы собрать данные со многих страниц, нужно пройтись по параметру offset с небольшой задержкой между запросами:

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):  # Первые 5 страниц
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"Страница {page + 1}: получен статус \{response.status_code\}, останавливаемся.")
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"Страница {page + 1}: найдено {len(cards)} отелей")
22    time.sleep(random.uniform(3, 7))  # Случайная задержка, чтобы не упереться в лимиты
23df = pd.DataFrame(all_hotels)
24print(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 баннеры и взаимодействовать с динамическим контентом. Цена этого — примерно и 300–500 МБ RAM на один экземпляр браузера.

Настраиваем Selenium с антидетект-параметрами

«Из коробки» Selenium очень легко распознать. Fingerprinting TripAdvisor замечает его почти сразу. Нужно отключить флаги автоматизации:

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")  # Новый 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# Убираем свойство webdriver из navigator
15driver.execute_cdp_command("Page.addScriptToEvaluateOnNewDocument", {
16    "source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
17})

Достаточно ли этого для TripAdvisor? Для небольших объёмов (до 50 страниц) такая настройка вместе с residential proxy обычно работает. Для больших объёмов может понадобиться undetected-chromedriver или nodriver — защита DataDome у TripAdvisor анализирует более 1000 сигналов на каждый запрос, включая TLS fingerprint, который обычный Selenium подделать не может.

Сбор результатов поиска отелей через Selenium

1import time
2import random
3url = "https://www.tripadvisor.com/Hotels-g187147-Paris_Ile_de_France-Hotels.html"
4driver.get(url)
5# Ждём загрузки карточек отелей
6wait = WebDriverWait(driver, 15)
7wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'div[data-test-attribute="location-results-card"]')))
8# Обрабатываем всплывающее окно cookie consent, если оно появилось
9try:
10    cookie_btn = driver.find_element(By.ID, "onetrust-accept-btn-handler")
11    cookie_btn.click()
12    time.sleep(1)
13except:
14    pass  # Баннер cookies не появился
15# Извлекаем данные по отелям
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"Собрано {len(hotels)} отелей")
33for h in hotels[:3]:
34    print(h)

На моей машине это заняло около 8 секунд на одну страницу — по сравнению с менее чем 1 секундой у requests+BS4. Если умножить эту разницу на сотни страниц, получится очень ощутимо.

Раскрываем «Read more» и собираем полные отзывы

Страницы отзывов обрезают длинные тексты за кнопкой «Read more». Selenium может нажать на неё:

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# Нажимаем все кнопки «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# Извлекаем отзывы
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        # Рейтинг закодирован в классе вроде "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"Собрано {len(reviews)} отзывов")

Добавляем ротацию прокси в Selenium

Для стабильного длительного парсинга нужна ротация прокси. Поскольку selenium-wire считается deprecated с января 2024 года, используйте встроенную поддержку прокси в Chrome:

1# Для прокси без авторизации
2proxy = "http://your-proxy-address:port"
3options.add_argument(f"--proxy-server=\{proxy\}")
4# Для прокси с авторизацией используйте расширение Chrome или Selenium 4 BiDi protocol

Для программной ротации создавайте новый экземпляр драйвера с другим прокси для каждой партии запросов. Это не самый изящный вариант, но он надёжный.

Подход 3: скрытый JSON-метод (без парсинга HTML)

Большинство статей вообще не рассматривают этот способ, и зря — он самый быстрый и самый чистый из трёх. TripAdvisor встраивает структурированные данные прямо в HTML-страницы — внутри тегов <script> как JavaScript-переменные вроде pageManifest и urqlCache. Если извлечь этот JSON, вы получите более чистые данные (рейтинги как числа, даты в формате ISO) с меньшим числом запросов и без необходимости рендерить JavaScript.

Ищем встроенный JSON в исходнике страницы

Ключевая идея: можно просто сделать requests.get(), загрузить страницу и вытащить JSON из сырого HTML, вообще не рендеря 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# Извлекаем JSON-блок pageManifest
16match = re.search(r"pageManifest:({.+?})};", response.text)
17if match:
18    page_data = json.loads(match.group(1))
19    print("Найдены данные pageManifest")
20    print(f"Ключи: {list(page_data.keys())[:10]}")

Как самостоятельно найти имя переменной: откройте любую страницу отеля TripAdvisor в Chrome, нажмите правой кнопкой мыши → View Page Source, затем Ctrl+F по pageManifest, urqlCache или aggregateRating. Данные уже там, их просто нужно распарсить.

Парсим JSON и извлекаем структурированные данные

TripAdvisor также встраивает schema.org данные application/ld+json, которые извлекать ещё проще:

1from parsel import Selector
2sel = Selector(text=response.text)
3# Извлекаем структурированные данные 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"Название: {data.get('name')}")
9        print(f"Рейтинг: {data.get('aggregateRating', {}).get('ratingValue')}")
10        print(f"Количество отзывов: {data.get('aggregateRating', {}).get('reviewCount')}")
11        print(f"Ценовой диапазон: {data.get('priceRange')}")
12        print(f"Адрес: {data.get('address', {}).get('streetAddress')}")
13        print(f"Координаты: {data.get('geo', {}).get('latitude')}, {data.get('geo', {}).get('longitude')}")
14        break

Данные JSON-LD встроены в статический HTML и НЕ требуют рендеринга JavaScript. В них есть название объекта, агрегированный рейтинг, количество отзывов, адрес, координаты, ценовой диапазон и ссылки на фото — и всё это без разбора ни одного HTML-тега.

Для более богатых данных — отдельных отзывов, распределения оценок, списка удобств — нужен объект urqlCache:

1# Извлекаем urqlCache для подробных данных по отзывам
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    # Ищем внутри кэша данные об отзывах
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"Найдена запись кэша с отзывами: {key[:50]}...")
11                break

Точные JSON-пути иногда меняются, когда TripAdvisor обновляет фронтенд, но общая структура — JSON-LD для сводных данных, urqlCache для подробностей — остаётся стабильной уже много лет.

Реверс-инжиниринг GraphQL API TripAdvisor (для продвинутых)

Для массового сбора данных GraphQL-эндпоинты TripAdvisor возвращают уже структурированные данные напрямую. Это самый быстрый способ, но и самый требовательный к поддержке.

1import httpx
2import random
3import string
4def generate_request_id():
5    """Генерирует значение заголовка X-Requested-By"""
6    random_chars = ''.join(random.choices(string.ascii_letters + string.digits, k=180))
7    return f"TNI1625!\{random_chars\}"
8# Поиск отелей в Париже
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"GraphQL-запрос не удался: \{response.status_code\}")

Для получения отзывов через 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\}")
28        for r in reviews[:3]:
29            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()

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    Собирает списки TripAdvisor для отелей, ресторанов или достопримечательностей.
11    Args:
12        category: "hotels", "restaurants" или "attractions"
13        location_id: Geo ID TripAdvisor (например, "187147" для Парижа)
14        location_name: URL-friendly имя (например, "Paris_Ile_de_France")
15        num_pages: Количество страниц для сбора
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"  Страница {page + 1}: статус \{response.status_code\}, останавливаемся.")
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"  Страница {page + 1}: найдено {len(cards)} объектов")
61        time.sleep(random.uniform(3, 7))
62    return pd.DataFrame(all_items)
63# Примеры использования
64print("=== Отели в Париже ===")
65hotels_df = scrape_tripadvisor("hotels", "187147", "Paris_Ile_de_France", num_pages=2)
66print(hotels_df.head())
67print("\n=== Рестораны в Риме ===")
68restaurants_df = scrape_tripadvisor("restaurants", "187791", "Rome_Lazio", num_pages=2)
69print(restaurants_df.head())
70print("\n=== Достопримечательности в Барселоне ===")
71attractions_df = scrape_tripadvisor("attractions", "187497", "Barcelona_Catalonia", num_pages=2)
72print(attractions_df.head())

Одна функция, три категории, ноль дублирования кода. Если TripAdvisor изменит селектор, вы исправите это в одном месте.

Что делать, если TripAdvisor блокирует вас (антибот-диагностика)

Это тот самый раздел, который мне больше всего был нужен в начале работы с TripAdvisor, и которого нет в структурированном виде у большинства конкурирующих гайдов. TripAdvisor одновременно использует DataDome (анализирует в день) и Cloudflare WAF. Ниже — диагностическая таблица для самых частых сбоев:

СимптомВероятная причинаРешение
Ответ HTTP 403Отсутствуют или вызывают подозрения заголовки; Cloudflare JS challengeУстановите реалистичные заголовки User-Agent, Accept-Language, Referer и Sec-CH-UA. Следите за их согласованностью.
Вместо данных появляется CAPTCHARate 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 DeniedIP/ASN заблокирован CloudflareЗамените datacenter proxy на residential proxy
Цикл challenge (бесконечная CAPTCHA)Сломана сохранность cookiesПрогрейте сессию, сначала зайдя на главную страницу; сохраняйте cookie jar

Логика повторов с экспоненциальной задержкой

В большинстве статей такого кода нет. Вот переиспользуемая функция повторных попыток:

1import time
2import random
3import requests
4def fetch_with_retry(session, url, max_retries=4, base_delay=2, max_delay=60):
5    """
6    Загружает URL с экспоненциальной задержкой и случайным джиттером.
7    На каждой попытке меняет User-Agent.
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        # Меняем User-Agent при повторных попытках
17        if attempt &gt; 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                # Учитываем Retry-After, если он есть
25                retry_after = int(response.headers.get("Retry-After", base_delay * (2 ** attempt)))
26                print(f"  Rate limited (429). Ждём \{retry_after\}с...")
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"  Получен \{response.status_code\}. Повтор {attempt + 1}/\{max_retries\} через {wait:.1f}с...")
32                time.sleep(wait)
33                continue
34            # Другие ошибки — не повторяем
35            print(f"  Неожиданный статус \{response.status_code\} для \{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"  Таймаут. Повтор {attempt + 1}/\{max_retries\} через {wait:.1f}с...")
40            time.sleep(wait)
41    print(f"  Все \{max_retries\} попыток исчерпаны для \{url\}")
42    return None

Ротация заголовков, прокси и сессий

Для длительного парсинга держите пул наборов заголовков и ротируйте их вместе:

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    # Добавьте больше residential proxy
23]
24def get_rotated_session():
25    """Создаёт новую сессию с ротацией заголовков и прокси."""
26    session = requests.Session()
27    # Выбираем случайный набор заголовков
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    # Выбираем случайный прокси
41    if PROXY_LIST:
42        proxy = random.choice(PROXY_LIST)
43        session.proxies = {"http": proxy, "https": proxy}
44    return session

Тип прокси имеет значение. Datacenter proxy TripAdvisor обычно блокирует почти сразу (HTTP 1020 Access Denied). Для устойчивого сбора данных нужны residential proxy — они идут через сети обычных пользователей и выглядят как реальные посетители. Ожидайте стоимость примерно $2,50–$8,40 за GB в зависимости от провайдера.

Экспорт и хранение собранных данных TripAdvisor

Когда данные уже собраны, привести их к рабочему формату совсем несложно.

Экспорт в CSV — самый частый вариант

1import pandas as pd
2df = pd.DataFrame(all_hotels)
3df.to_csv("tripadvisor_hotels_paris.csv", index=False, encoding="utf-8-sig")
4print(f"Экспортировано {len(df)} строк в CSV")

encoding='utf-8-sig' здесь важен: он помогает Excel корректно отображать нелатинские символы (французские акценты, китайские иероглифы и т. п.) при открытии CSV.

Экспорт в JSON — для вложенных данных

Когда отзывы вложены внутрь отелей, JSON сохраняет иерархию:

1# Иерархическая структура
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# Для плоского анализа используйте 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)

Подход из двух файлов для реляционных данных

Для больших наборов данных я обычно использую два 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, если вам нужны данные для коммерческого использования в масштабе. даёт доступ к информации о бизнесе плюс до 5 отзывов и 5 фото на объект — ограниченно, но легально и стабильно.
  • Учитывайте правовой контекст: усилило ограничения на сбор данных, основанные на ToS, по всей Европе. В правилах TripAdvisor прямо запрещён scraping. Делайте это ответственно и на свой риск.

Итоги

Теперь картина полная.

  • Requests + BeautifulSoup — самый простой путь. Он хорошо работает для статичных страниц со списками, требует минимальной настройки и работает быстро. Начинайте с него, если вам нужно меньше 100 страниц и не требуется контент, отрендеренный JavaScript-ом.
  • Selenium закрывает всё, что не умеет requests: динамический контент, кнопки «Read more», cookie-баннеры. Он в 5 раз медленнее и требует больше ресурсов, но это единственный вариант, когда нужна интеракция со страницей.
  • Hidden JSON / GraphQL — самый чистый и быстрый подход. Он даёт структурированные данные без парсинга HTML, снижает количество запросов, а значит и потребность в прокси, и возвращает информацию в формате, удобном для анализа. Но он требует больше реверс-инжиниринга на старте и периодической поддержки, когда TripAdvisor меняет структуру данных.

Переиспользуемая функция scrape_tripadvisor() покрывает отели, рестораны и достопримечательности. Второй туториал вам, скорее всего, не понадобится.

А если в середине чтения вы поймёте, что кодить — не ваше, или вам просто нужно 50 отелей в таблице к концу дня, справится с этим за два клика: ИИ сам определит поля, автоматически обработает пагинацию и бесплатно экспортирует в Excel или Google Sheets. Python не нужен.

Если хотите углубиться, у нас есть ещё больше разборов по парсингу в и на нашем .

FAQ

1. Законно ли парсить TripAdvisor?

В Terms of Service TripAdvisor прямо запрещён scraping. Однако суды в целом считают, что сбор публично доступных данных (не за логином) не нарушает Computer Fraud and Abuse Act в США. При этом решение суда ЕС по Ryanair в 2025 году усилило ограничения, основанные на ToS, в Европе. Собирайте только публичные данные, соблюдайте robots.txt, не перепубликуйте защищённый авторским правом контент и при коммерческом использовании проконсультируйтесь с юристом.

2. Можно ли собрать данные TripAdvisor без Python?

Да. No-code инструменты вроде могут собирать 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 даёт больше данных на запрос, поэтому для того же объёма информации вам может понадобиться значительно меньше страниц.

Узнать больше

Попробуй Thunderbit

Собирай лиды и другие данные всего в 2 клика. На базе AI.

Получить Thunderbit Это бесплатно
Извлекай данные с помощью AI
Легко передавай данные в Google Sheets, Airtable или Notion
PRODUCT HUNT#1 Product of the Week