На прошлой неделе я попробовал выгрузить рейтинги отелей и количество отзывов примерно по 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 и автоматически предлагать подходящие поля для извлечения. Рабочий процесс действительно укладывается в два клика:
- Откройте страницу со списком на 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, что даёт естественное преимущество перед антибот-защитой.
Попробовать можно через — бесплатный тариф даёт 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) — парсинг 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.
Виртуальное окружение (рекомендуется):
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. Следите за их согласованностью. |
| Вместо данных появляется 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 |
Логика повторов с экспоненциальной задержкой
В большинстве статей такого кода нет. Вот переиспользуемая функция повторных попыток:
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 > 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 даёт больше данных на запрос, поэтому для того же объёма информации вам может понадобиться значительно меньше страниц.
Узнать больше