使用 Python 抓取 TripAdvisor 数据(避免被封)

最后更新于 April 17, 2026

上周,我试着从 TripAdvisor 抓取三座欧洲城市大约 200 家酒店的评分和评论数量。我的第一版脚本——一个只用了默认请求头的基础 requests.get()——每次请求都直接回我一个相当漂亮的 403 Forbidden。结果就是:一字节有用数据都没拿到。

TripAdvisor 是旅游行业最丰富的公开数据源之一:它有超过 、800 多万条商家信息,以及每月约 4.6 亿独立访客。它每年还会影响超过 的旅游支出。但如果你想用程序把这些数据抓下来,事情就没那么简单了。TripAdvisor 叠了好几层防护,包括 DataDome 机器人检测、Cloudflare WAF、TLS 指纹识别和 JavaScript 挑战,足够把大多数天真的爬取尝试挡在门外。本文就是我当时最希望有人写给我的那种完整指南:把三种 Python 抓取方案正面对比一下(外加一个无代码方案),每种方法都给你完整代码,再配上结构化的反爬排错部分,以及可以复用到酒店、餐厅和景点的模式。不管你是 Python 新手,还是已经写过不少爬虫的老手,这篇文章都能帮你少踩很多 403 的坑。

不想写代码?用更简单的方式抓取 TripAdvisor

先把话说直白一点。很多搜索“用 Python 抓取 TripAdvisor”的人,其实并不是真的很想写代码。他们只是想赶紧把酒店名称、评分、评论数、价格这些数据弄到表格里。如果你也是这样,那其实有一条更省事的路。

是我们做的一款 AI 驱动 Chrome 扩展,能直接读取任意 TripAdvisor 页面,并自动帮你推荐要提取的字段。整个流程真的就两步:

  1. 打开 TripAdvisor 列表页(比如“巴黎酒店”的搜索结果页)。
  2. 在 Thunderbit 侧边栏点击“AI Suggest Fields”。AI 会扫描页面,自动给你列出酒店名称、评分、评论数、价格、位置等字段。
  3. 点击“Scrape”。 Thunderbit 会把页面上的每一条列表数据都抓下来——如果还有更多结果,它也会自动处理分页。
  4. 导出 到 Excel、Google Sheets、Airtable 或 Notion。所有套餐都支持免费导出。

Thunderbit 不需要额外配置,就能直接适配酒店、餐厅和景点——AI 会根据页面内容自动调整。对于分页结果,它还能自动识别“下一页”按钮和无限滚动。更重要的是,它是在你真实的 Chrome 浏览器里运行的,会沿用你的会话 Cookie 和浏览器指纹,所以面对机器人检测时天然更占优势。

你可以通过 试用一下——免费套餐每月可以抓 6 页,足够你把整个流程跑通。

如果你需要程序化控制、自定义解析逻辑,或者打算抓 10,000+ 页,那 Python 还是更合适的。继续往下看。

为什么要用 Python 抓取 TripAdvisor?

TripAdvisor 数据对业务的影响是直接而且可量化的。 发现,酒店的 Global Review Index 每提高 1 分,平均每日房价会提升 0.89%,可售客房收益会提升 1.42%。另一项 也表明,TripAdvisor 评分如果额外增加 1 星,平均酒店每年能多赚 55,000–75,000 美元。评论不只是“好看”的指标,它们真的会影响收入。

不同团队会这样用 TripAdvisor 数据:

使用场景受益对象需要的数据
酒店竞品分析酒店集团、收益经理评分、价格、评论量、设施
餐厅市场调研餐饮集团、食品品牌菜系类型、价格区间、评论情绪
景点趋势追踪旅行社、旅游局热门排名、季节性模式
情绪分析研究人员、数据分析师完整评论文本、星级、日期
线索开发销售团队、旅行社商家名称、联系方式、位置

为什么偏偏是 Python?有三个原因。第一是生态成熟:BeautifulSoup、Selenium、Playwright、Scrapy、httpx、pandas——在抓取和数据分析这件事上,Python 的库比别的语言都更成熟。第二是普及率高, 都在用 Python,这意味着社区支持更多、StackOverflow 现成答案更多、教程更新也更快。第三是整个流程更顺:你可以用 BeautifulSoup 抓取,用 pandas 清洗,用 Hugging Face Transformers 做情绪分析,再做仪表盘,全部都在同一种语言里完成,不用来回切上下文。

用 Python 抓取 TripAdvisor 的三种方法(对比)

很多对比文章都会只讲一种方法,然后一路讲到底。但在你真正开始写代码之前,这种写法其实不太实用。下面这张对比表,是我当初最希望有人直接给我的:

方法速度JS 支持反爬能力复杂度最适合
requests + BeautifulSoup⚡ 快(原始速度约 120–200 页/分钟)❌ 无⚠️ 低简单静态列表页、小规模项目
Selenium / 无头浏览器🐢 慢(约 8–20 页/分钟)✅ 完整支持⚠️ 中等中等动态内容、“Read more” 点击、Cookie 横幅
隐藏 JSON / GraphQL API⚡⚡ 最快(原始速度约 200–600 页/分钟)N/A✅ 更强困难大规模评论/酒店数据提取
无代码(Thunderbit)⚡ 快✅ 内置支持✅ 内置支持最简单非开发者、临时导出任务

这里有几个很重要的提醒。上面的原始速度只是理论值——TripAdvisor 的速率限制(每个 IP 约每分钟 10–15 次请求)会把实际吞吐量压到差不多每个 IP 每分钟 10 页左右,不管你用哪种方法。隐藏 JSON 方法能让每次请求拿到更多数据,所以总请求数更少,暴露在限流下的风险也更低。Selenium 在真实场景里比基于 requests 的方法慢 5 倍,但当你需要点击按钮或渲染 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 指纹伪装(绕过 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

方法一:用 Requests 和 BeautifulSoup 抓取 TripAdvisor

这是最简单的方法。它特别适合抓列表页(酒店搜索结果、餐厅列表),因为你要的数据本身就已经在静态 HTML 里了。不需要浏览器,不需要 JavaScript 渲染,资源占用也很低。

了解 TripAdvisor 的 URL 规律

TripAdvisor 的 URL 在类别上有比较固定的模式:

  • 酒店: 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-AgentSec-CH-UA 客户端提示是不是一致。如果你的 User-Agent 说自己是 Chrome 135,但 Sec-CH-UA 却显示 Chrome 120,就很容易被标记。请始终成组轮换整套请求头,不要只改其中一个头。

用 BeautifulSoup 解析列表页

拿到成功响应后,就可以用 BeautifulSoup 提取数据了。TripAdvisor 使用的 data-automationdata-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"Found {len(hotels)} hotels on this page")
26for h in hotels[:3]:
27    print(h)

关于选择器: TripAdvisor 会用混淆后的 CSS 类名(比如 FGwztyyzcQ),这些类名会随着网站更新而变化。data-automationdata-test-target 这类属性稳定得多。记住,优先用数据属性,而不是 class 名称。

处理分页

如果你想抓多页内容,可以在请求之间加一点礼貌性的延迟,然后循环偏移参数:

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 {page + 1}: Got status {response.status_code}, stopping.")
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 {page + 1}: {len(cards)} hotels found")
22    time.sleep(random.uniform(3, 7))  # 随机延迟,避免触发限流
23df = pd.DataFrame(all_hotels)
24print(f"\nTotal hotels scraped: {len(df)}")

time.sleep(random.uniform(3, 7)) 这一句很重要。TripAdvisor 的限流阈值大概是每个 IP 每分钟 10–15 次请求。速度再快一点,就可能触发验证码或者 429 错误。

这种方法的局限

什么时候会失效?requests+BS4 在这些情况下会遇到麻烦:

  • TripAdvisor 返回的是 JavaScript 渲染内容(有些搜索结果页需要 JS)
  • 评论正文被“Read more”按钮截断
  • 反爬升级成 JavaScript 挑战或验证码
  • 你需要的数据只有在客户端渲染后才会出现(比如价格、可用性)

碰到这些场景时,就得用 Selenium(方法二)或者隐藏 JSON 方法(方法三)。

方法二:用 Selenium 抓取 TripAdvisor(无头浏览器)

Selenium 会启动真实浏览器,这意味着它可以渲染 JavaScript、点击按钮、处理 Cookie 同意弹窗,并和动态内容交互。代价也很明显:速度大约会慢 ,而且每个浏览器实例会吃掉 300–500MB 内存。

配置带反检测设置的 Selenium

开箱即用的 Selenium 很容易被识别。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")  # 使用新版无头模式(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# 从 navigator 中移除 webdriver 属性
15driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
16    "source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
17})

这对 TripAdvisor 够用吗? 对小规模抓取(50 页以内)来说,配合住宅代理通常还能跑。如果规模再大一点,你可能就需要 undetected-chromedrivernodriver 了——TripAdvisor 的 DataDome 防护会分析每个请求超过 1,000 个信号,包括普通 Selenium 根本伪造不了的 TLS 指纹。

用 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 同意弹窗(如果出现)
9try:
10    cookie_btn = driver.find_element(By.ID, "onetrust-accept-btn-handler")
11    cookie_btn.click()
12    time.sleep(1)
13except:
14    pass  # 没有弹窗
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"Scraped {len(hotels)} hotels")
33for h in hotels[:3]:
34    print(h)

在我的机器上,这一页大概花了 8 秒,而 requests+BS4 只要不到 1 秒。这个 8 倍差距一旦放大到几百页,就会很明显。

展开“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        # 评分编码在 class 里,例如 "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"Scraped {len(reviews)} reviews")

给 Selenium 加上代理轮换

如果你要持续抓取,就需要代理轮换。由于 selenium-wire 自 2024 年 1 月起已经弃用,建议直接用 Chrome 内置代理支持:

1# 使用无需认证的代理
2proxy = "http://your-proxy-address:port"
3options.add_argument(f"--proxy-server={proxy}")
4# 对于需要认证的代理,可以用 Chrome 扩展,或者 Selenium 4 的 BiDi 协议

如果要程序化轮换代理,最稳的做法就是每一批请求创建一个新的 driver 实例。虽然看起来不够优雅,但非常稳定。

方法三:隐藏 JSON 方法(直接跳过 HTML 解析)

很多教程会完全跳过这一招,但它其实是三种方法里最快、最干净的一种。TripAdvisor 会在 HTML 页面里直接嵌入结构化 JSON,放在 <script> 标签里的 JavaScript 变量中,比如 pageManifesturqlCache。把这些 JSON 提出来之后,你能拿到更干净的数据(评分是数字、日期是 ISO 格式),请求更少,也不需要 JavaScript 渲染。

在页面源码中找到嵌入的 JSON

核心思路是:你可以直接用 requests.get() 抓页面,再从原始 HTML 里提取 JSON,完全不需要渲染 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# 提取 pageManifest JSON 块
16match = re.search(r"pageManifest:({.+?})};", response.text)
17if match:
18    page_data = json.loads(match.group(1))
19    print("Found pageManifest data")
20    print(f"Keys: {list(page_data.keys())[:10]}")

怎么自己找变量名: 在 Chrome 里打开任意 TripAdvisor 酒店页面,右键 → 查看网页源代码,然后用 Ctrl+F 搜索 pageManifesturqlCacheaggregateRating。这些数据都在那儿,等着你去解析。

解析 JSON 并提取结构化数据

TripAdvisor 还会嵌入 application/ld+json 的 schema.org 数据,这个更容易提取:

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"Name: {data.get('name')}")
9        print(f"Rating: {data.get('aggregateRating', {}).get('ratingValue')}")
10        print(f"Review Count: {data.get('aggregateRating', {}).get('reviewCount')}")
11        print(f"Price Range: {data.get('priceRange')}")
12        print(f"Address: {data.get('address', {}).get('streetAddress')}")
13        print(f"Coordinates: {data.get('geo', {}).get('latitude')}, {data.get('geo', {}).get('longitude')}")
14        break

JSON-LD 数据就嵌在静态 HTML 里,不需要 JavaScript 渲染。它可以直接给你商家名称、综合评分、评论数、地址、坐标、价格区间和图片 URL——完全不用去解析任何 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"Found review cache entry: {key[:50]}...")
11                break

TripAdvisor 更新前端时,具体 JSON 路径偶尔会变,但整体结构——JSON-LD 负责概要数据,urqlCache 负责详细数据——这些年一直都算稳定。

逆向 TripAdvisor 的 GraphQL API(高级)

如果你要做大规模提取,TripAdvisor 的 GraphQL 接口会直接返回结构化数据。这是最快的方法,但维护成本也最高。

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 request failed: {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 reviews: {total}")
28        for r in reviews[:3]:
29            print(f"  [{r['rating']}/5] {r['title']} - {r['createdDate']}")

重要提醒: preRegisteredQueryId 的值(比如搜索用的 84b17ed122fbdbd4 和评论用的 ef1a9f94012220d3)在 TripAdvisor 重新部署后可能会失效。一旦失效,请求会静默失败。你需要通过浏览器 DevTools 盯网络请求,重新找到这些 query ID。

为什么这种方法更少依赖代理

逻辑其实很简单。用 requests+BS4 抓 100 个酒店详情页,需要 100 次请求;而隐藏 JSON 方法里,每次请求都能从单页加载里拿到所需的全部数据,不用再额外请求展开评论或者加载动态内容。GraphQL 更进一步,单次 API 调用就能一次拿到 20 条评论。请求越少,就越不容易触发限流,也越不需要频繁轮换代理。对于中小型项目(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    参数:
12        category: "hotels"、"restaurants" 或 "attractions"
13        location_id: TripAdvisor 的 geo ID(例如巴黎为 "187147")
14        location_name: 适用于 URL 的名称(例如 "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 {page + 1}: Status {response.status_code}, stopping.")
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 {page + 1}: {len(cards)} items found")
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 挑战设置真实且一致的 User-AgentAccept-LanguageRefererSec-CH-UA 请求头,确保它们彼此一致。
显示验证码页面而不是数据触发限流或浏览器指纹识别轮换住宅代理,在请求之间增加随机延迟(2–7 秒)
HTML 为空或页面主体空白requests 没有渲染 JavaScript改用 Selenium,或直接从页面源码中的隐藏 JSON 提取
评论只显示一部分 / “Read more” 无法展开内容需要点击事件触发加载用 Selenium 的 .click(),或从内嵌 JSON 块中提取
评论只显示一种语言缺少语言参数在评论 URL 后追加 ?filterLang=ALL
抓到 N 页后数据停止加载基于会话的速率限制轮换会话,在每批任务间清理 Cookie
HTTP 1020 Access DeniedCloudflare 封禁了 IP/ASN从数据中心代理切换到住宅代理
挑战循环(无限验证码)Cookie 持久化失效先访问首页“预热”会话;保持 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). Waiting {retry_after}s...")
27                time.sleep(retry_after)
28                continue
29            if response.status_code in (403, 503):
30                wait = min(base_delay * (2 ** attempt) + random.uniform(0, 1), max_delay)
31                print(f"  Got {response.status_code}. Retry {attempt + 1}/{max_retries} in {wait:.1f}s...")
32                time.sleep(wait)
33                continue
34            # 其他错误码,不重试
35            print(f"  Unexpected status {response.status_code} for {url}")
36            return response
37        except requests.exceptions.Timeout:
38            wait = min(base_delay * (2 ** attempt) + random.uniform(0, 1), max_delay)
39            print(f"  Timeout. Retry {attempt + 1}/{max_retries} in {wait:.1f}s...")
40            time.sleep(wait)
41    print(f"  All {max_retries} retries exhausted for {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    # 添加更多住宅代理
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

代理类型很重要。 数据中心代理几乎会被 TripAdvisor 直接封掉(HTTP 1020 Access Denied)。如果要持续抓取,必须用住宅代理——它们通过普通消费者 ISP 出口,看起来和真实用户几乎没区别。不同服务商的价格大概在每 GB 2.50–8.40 美元之间。

导出并存储抓取到的 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"Exported {len(df)} rows to 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 TripAdvisor 的 robots.txt 会完全屏蔽 AI 训练机器人(GPTBot、ClaudeBot 等)。标准爬虫则会有部分路径限制。你可以在 tripadvisor.com/robots.txt 查看。
  • 加延迟: 每次请求间隔 3–7 秒会更稳妥。速度超过每个 IP 每分钟 10–15 次请求,容易触发限流。
  • 只抓公开数据。 不要通过登录去拿受限内容。
  • 妥善存储数据,如果涉及个人信息(比如评论者姓名),要遵守 GDPR/CCPA。
  • 如果要商业级数据,考虑 TripAdvisor 官方 API。 可以提供商家详情,以及每个地点最多 5 条评论和 5 张照片——虽然有限,但合法而且稳定。
  • 注意法律环境: 强化了欧盟范围内基于服务条款的抓取限制。TripAdvisor 的服务条款明确禁止抓取。请负责任地抓取,并自行承担风险。

总结

整体情况就是这样。

  • Requests + BeautifulSoup 是最简单的路子。它适合静态列表页,设置少、速度快。如果你抓少于 100 页,而且不需要 JavaScript 渲染内容,就从这里开始。
  • Selenium 能处理 requests 搞不定的一切:动态内容、“Read more” 按钮、Cookie 横幅。它慢 5 倍、资源占用更高,但当你需要和页面交互时,它就是唯一选择。
  • 隐藏 JSON / GraphQL 是最干净、最快的方式。它不需要解析 HTML 就能拿到结构化数据,减少请求次数(因此也更少需要代理),而且返回的数据很适合分析。代价是前期要做更多逆向分析,TripAdvisor 改结构时也得偶尔维护一下。

可复用的 scrape_tripadvisor() 函数已经把酒店、餐厅和景点三类内容都覆盖了。你其实不该再需要第二篇教程。

如果你看到这里才发现自己其实不想写代码——或者你只是想在今天结束前把 50 家酒店放进表格里—— 只要两次点击,就能靠 AI 字段识别、自动分页和免费导出到 Excel 或 Google Sheets 帮你搞定。不需要 Python。

如果你还想继续深入,我们在 还有更多抓取教程。

常见问题

1. 抓取 TripAdvisor 合法吗?

TripAdvisor 的服务条款明确禁止抓取。不过,法院通常认为,抓取公开可访问、且不需要登录的数据,并不违反美国的 Computer Fraud and Abuse Act。话虽如此,2025 年欧盟法院的 Ryanair 裁决强化了欧洲地区基于 ToS 的限制。请只抓公开数据,尊重 robots.txt,不要转载受版权保护的内容,如果用于商业用途,最好咨询法律顾问。

2. 不用 Python 能抓 TripAdvisor 吗?

可以。像 这样的无代码工具,能直接在浏览器里抓 TripAdvisor,带有 AI 字段识别和自动分页功能。你也可以用浏览器扩展、Google Sheets 插件,或者商业抓取 API。Python 给你最多的控制权和灵活性,但它不是唯一选择。

3. 如何避免在抓 TripAdvisor 时被封?

核心策略包括:使用真实且一致的请求头(尤其是 User-AgentSec-CH-UA)、轮换住宅代理(数据中心 IP 很容易被封)、每次请求之间加 3–7 秒随机延迟、用隐藏 JSON 方法减少总请求数、实现指数退避重试逻辑,以及在抓深层页面前先访问首页“预热”会话。

4. 我能从 TripAdvisor 抓到哪些数据?

你可以抓酒店、餐厅和景点的数据——包括名称、评分、评论数、价格区间、地址、坐标、设施(酒店)、菜系类型(餐厅)、行程时长(景点),以及带单独评分和日期的完整评论文本。隐藏 JSON 和 GraphQL 方法能在每次请求里拿到最丰富的数据。

5. TripAdvisor 一天能抓多少页?

如果只用单个 IP,并且加上合理延迟:大约每天 600–1,000 页。使用 20 个轮换住宅代理:请求式方法每天大约能抓 200,000–300,000 页。Selenium 更慢——每个代理每天大约 8,000–12,000 页。隐藏 JSON/GraphQL 方法让每次请求拿到最多数据,所以为了得到同样的信息,你可能需要更少的总页数。

了解更多

Ke
Ke
CTO @ Thunderbit. Ke is the person everyone pings when data gets messy. He's spent his career turning tedious, repetitive work into quiet little automations that just run. If you've ever wished a spreadsheet could fill itself in, Ke has probably already built the thing that does it.
目录

试试 Thunderbit

只需 2 次点击即可抓取线索和其他数据。AI 驱动。

获取 Thunderbit 免费使用
使用 AI 提取数据
轻松将数据传输到 Google Sheets、Airtable 或 Notion
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week