上週,我試著從 TripAdvisor 抓取三個歐洲城市、約 200 間飯店的評分與評論數量。我的第一支程式——一個使用預設 headers 的基本 requests.get()——每一次請求都回傳漂亮的 403 Forbidden。完全沒有拿到任何有用資料。
TripAdvisor 是旅遊產業中最豐富的公開資料來源之一:超過 10 億則評論、800 多萬筆商家列表,以及每月約 4.6 億名不重複訪客。它一年影響超過 $600 億 的旅遊支出。但要用程式化方式取得這些資料?那就沒那麼簡單了。TripAdvisor 會使用 DataDome bot 偵測、Cloudflare WAF、TLS 指紋辨識與 JavaScript challenge——這是一套層層疊加的防護機制,會在大多數天真的爬取嘗試開始前就把它們擋下來。這篇指南就是我當初最希望能找到的資源:三種 Python 爬取方式的正面比較(外加一個免寫程式的選項)、每種方法的完整程式碼、結構化的反機器人排查章節,以及可重複使用、適用於飯店、餐廳與景點的通用寫法。不管你是 Python 新手還是資深開發者,這篇應該都能幫你省下大量白白浪費的 403。
不想寫程式?用更簡單的方法抓 TripAdvisor
我先講清楚一件事。很多搜尋「用 Python 抓 TripAdvisor」的人,其實並不是真的非得寫程式不可。他們只是想快速拿到資料——像是飯店名稱、評分、評論數、價格——而且最好是直接進到試算表裡。如果你也是這樣,那其實有更短的路。
Thunderbit 是我們打造的一款 AI Chrome 擴充功能,可以讀取任何 TripAdvisor 頁面,並自動建議最適合擷取的欄位。整個流程真的只要兩步:
- 打開 TripAdvisor 的列表頁(例如「Paris 的飯店」搜尋結果)。
- 在 Thunderbit 側邊欄點選「AI Suggest Fields」。AI 會掃描頁面,並提出像是飯店名稱、評分、評論數、價格與地點這類欄位。
- 點選「Scrape」。Thunderbit 會把頁面上所有列表的資料抓下來——如果你需要更多結果,它還會自動處理分頁。
- 匯出 到 Excel、Google Sheets、Airtable 或 Notion。所有方案都可免費匯出。
Thunderbit 可直接跨飯店、餐廳與景點使用,完全不需要額外設定——AI 會依照頁面內容自動適應。對於有分頁的結果,它會自動偵測「Next」按鈕與無限捲動。由於它是在你真實的 Chrome 瀏覽器裡運作,會沿用你的 session cookies 和瀏覽器指紋,對抗 bot 偵測時也更有優勢。
你可以試試 Thunderbit Chrome Extension——免費方案每月提供 6 頁,足夠測試整個流程。
如果你需要程式化控制、自訂解析邏輯,或者打算抓 10,000+ 頁,Python 就是正解。繼續看下去吧。
為什麼要用 Python 抓 TripAdvisor?
TripAdvisor 資料對商業有直接且可衡量的影響。康乃爾大學研究 指出,飯店的 100 分制 Global Review Index 每提升 1 分,平均每日房價會增加 0.89%,可售房收入(Revenue Per Available Room)則增加 1.42%。另一份 ScienceDirect 研究 顯示,TripAdvisor 評分若外生性提升 1 顆星,平均飯店每年可多出 55,000–75,000 美元的收入。評論不只是表面數字,而是真正的營收驅動因素。
以下是不同團隊如何使用 TripAdvisor 資料:
| 使用情境 | 受益對象 | 需要的資料 |
|---|---|---|
| 飯店競品分析 | 飯店集團、收益管理人員 | 評分、價格、評論量、設施 |
| 餐廳市場研究 | 餐飲集團、食品品牌 | 菜系、價格區間、評論情緒 |
| 景點趨勢追蹤 | 旅行社、觀光局 | 熱門排名、季節性模式 |
| 情緒分析 | 研究人員、資料分析師 | 完整評論文字、星等、日期 |
| 潛在客戶開發 | 業務團隊、旅行社 | 商家名稱、聯絡資訊、地點 |
為什麼特別推薦 Python?有三個原因。第一是生態系:BeautifulSoup、Selenium、Playwright、Scrapy、httpx、pandas——Python 的爬取與資料分析函式庫比其他語言都更成熟。第二是 71.7% 的網頁爬取開發者 都在用 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 在實務測試中比 request 型方法慢約 5 倍,但只要你需要點按按鈕或渲染 JavaScript,它就是唯一選項。
接下來的內容會把這三種 Python 方法都完整講一遍。你可以挑最適合自己情境的,或者混著用(我常常用 requests+BS4 抓列表頁,再用隱藏 JSON 抓詳細頁)。
設定你的 Python 環境
在開始之前,先把環境準備好。你需要 Python 3.10+(我推薦 3.12 或 3.13——主流套件都已支援,而且沒有已知問題)。
一次安裝所有套件:
pip install requests beautifulsoup4 selenium httpx parsel pandas curl-cffi
套件說明:
requests(2.33.1) — HTTP 請求,需要 Python 3.10+beautifulsoup4(4.14.3) — HTML 解析selenium(4.43.0) — 瀏覽器自動化,需要 Python 3.10+httpx(0.28.1) — 非同步 HTTP 客戶端parsel(1.11.0) — CSS/XPath selector(比 BS4 更輕量)pandas(3.0.2) — 資料匯出,需要 Python 3.11+curl_cffi(0.15.0) — TLS 指紋模擬(繞過 Cloudflare 的關鍵)
**ChromeDriver:**如果你使用 Selenium,有個好消息——自 Selenium 4.6 起,Selenium Manager 會自動下載並快取正確版本的 ChromeDriver。你不需要手動安裝,它會自動動態匹配版本,不必擔心 Chrome 版本不相容的問題。
虛擬環境(建議):
python -m venv tripadvisor-scraper
source tripadvisor-scraper/bin/activate # macOS/Linux
tripadvisor-scraper\Scripts\activate # Windows
方法 1:使用 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
評論頁的 offset 參數是 or,每次增加 10:
- 第 1 頁:
Reviews-or0-Hotel_Name.html - 第 2 頁:
Reviews-or10-Hotel_Name.html
如果要抓取所有語言的評論,請在 URL 後面加上 ?filterLang=ALL。
使用真實感更高的 Headers 發送請求
TripAdvisor 會嚴格檢查 headers。用 Python 預設 headers 發送請求,幾乎會立刻被封。你需要模擬真實的 Chrome 瀏覽器:
import requests
import time
import random
session = requests.Session()
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Referer": "https://www.tripadvisor.com/",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-CH-UA": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
"Sec-CH-UA-Mobile": "?0",
"Sec-CH-UA-Platform": '"Windows"',
}
session.headers.update(headers)
url = "https://www.tripadvisor.com/Hotels-g187147-Paris_Ile_de_France-Hotels.html"
response = session.get(url)
print(f"Status: {response.status_code}")
print(f"Content length: {len(response.text)} characters")
**關鍵細節:**TripAdvisor 會檢查你的 User-Agent 與 Sec-CH-UA Client Hints 是否一致。如果你的 User-Agent 宣稱自己是 Chrome 135,但 Sec-CH-UA 卻寫 Chrome 120,就很容易被標記。記得一次旋轉整組 headers,不要只換單一 header。
使用 BeautifulSoup 解析列表頁
成功拿到回應後,就可以用 BeautifulSoup 擷取資料。TripAdvisor 會使用 data-automation 與 data-test-attribute 這些屬性,它們比 CSS class 名稱穩定得多(後者經常變動):
from bs4 import BeautifulSoup
soup = BeautifulSoup(response.text, "html.parser")
# 找出所有飯店列表卡片
cards = soup.select('div[data-test-attribute="location-results-card"]')
hotels = []
for card in cards:
# 飯店名稱
title_el = card.select_one('div[data-automation="hotel-card-title"]')
name = title_el.get_text(strip=True) if title_el else None
# 詳細頁連結
link_el = card.select_one('div[data-automation="hotel-card-title"] a')
link = "https://www.tripadvisor.com" + link_el["href"] if link_el else None
# 評分
rating_el = card.select_one('[data-automation="bubbleRatingValue"]')
rating = rating_el.get_text(strip=True) if rating_el else None
# 評論數
review_el = card.select_one('[data-automation="bubbleReviewCount"]')
review_count = review_el.get_text(strip=True).replace(",", "").split()[0] if review_el else None
hotels.append({
"name": name,
"rating": rating,
"review_count": review_count,
"url": link,
})
print(f"Found {len(hotels)} hotels on this page")
for h in hotels[:3]:
print(h)
**選擇器提醒:**TripAdvisor 會使用像 FGwzt、yyzcQ 這類混淆過的 CSS class 名稱,而且每次網站更新都可能變動。data-automation 與 data-test-target 屬性穩定得多。請優先使用 data 屬性,不要依賴 class 名稱。
處理分頁
要抓多頁資料,可以用 offset 參數迴圈處理,並在每次請求之間加入禮貌性的延遲:
import pandas as pd
all_hotels = []
base_url = "https://www.tripadvisor.com/Hotels-g187147-oa{offset}-Paris_Ile_de_France-Hotels.html"
for page in range(5): # 前 5 頁
offset = page * 30
url = base_url.format(offset=offset) if page > 0 else "https://www.tripadvisor.com/Hotels-g187147-Paris_Ile_de_France-Hotels.html"
response = session.get(url)
if response.status_code != 200:
print(f"Page {page + 1}: Got status {response.status_code}, stopping.")
break
soup = BeautifulSoup(response.text, "html.parser")
cards = soup.select('div[data-test-attribute="location-results-card"]')
for card in cards:
title_el = card.select_one('div[data-automation="hotel-card-title"]')
name = title_el.get_text(strip=True) if title_el else None
rating_el = card.select_one('[data-automation="bubbleRatingValue"]')
rating = rating_el.get_text(strip=True) if rating_el else None
review_el = card.select_one('[data-automation="bubbleReviewCount"]')
review_count = review_el.get_text(strip=True).replace(",", "").split()[0] if review_el else None
all_hotels.append({"name": name, "rating": rating, "review_count": review_count})
print(f"Page {page + 1}: {len(cards)} hotels found")
time.sleep(random.uniform(3, 7)) # 隨機延遲,避免觸發速率限制
df = pd.DataFrame(all_hotels)
print(f"\nTotal hotels scraped: {len(df)}")
time.sleep(random.uniform(3, 7)) 很重要。TripAdvisor 的速率限制大約是每個 IP 每分鐘 10–15 次請求。速度一旦超過這個範圍,就可能觸發 CAPTCHA 或 429 錯誤。
這種方法的限制
什麼時候這招會失效?requests+BS4 在以下情況會失敗:
- TripAdvisor 回傳 JavaScript 渲染的內容(有些搜尋結果頁需要 JS)
- 評論文字被「Read more」按鈕截斷
- 反機器人機制升級成 JavaScript challenge 或 CAPTCHA
- 你需要只在前端渲染後才會出現的資料(價格、可訂房狀況)
遇到這些情況,你就需要 Selenium(方法 2)或隱藏 JSON 方法(方法 3)。
方法 2:使用 Selenium 抓 TripAdvisor(無頭瀏覽器)
Selenium 會啟動真實瀏覽器,因此可以渲染 JavaScript、點按按鈕、處理 cookie 同意橫幅,並與動態內容互動。代價是:它大約 慢 5 倍,而且每個瀏覽器實例會吃掉 300–500MB 記憶體。
用反偵測設定配置 Selenium
Selenium 開箱即用其實很容易被偵測。TripAdvisor 的指紋辨識會立刻抓到它。你需要關掉 automation flags:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
options = Options()
options.add_argument("--headless=new") # 使用新的 headless 模式(Chrome 112+)
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_argument("--window-size=1920,1080")
options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36")
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option("useAutomationExtension", False)
driver = webdriver.Chrome(options=options)
# 移除 navigator 上的 webdriver 屬性
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
})
這樣就夠抓 TripAdvisor 了嗎? 如果是小規模爬取(少於 50 頁),再搭配住宅代理,這樣通常可行。若規模更大,你可能需要 undetected-chromedriver 或 nodriver——TripAdvisor 的 DataDome 防護會分析每次請求超過 1,000 個訊號,包括 TLS 指紋,而原生 Selenium 無法偽裝這些資訊。
用 Selenium 抓飯店搜尋結果
import time
import random
url = "https://www.tripadvisor.com/Hotels-g187147-Paris_Ile_de_France-Hotels.html"
driver.get(url)
# 等待飯店卡片載入
wait = WebDriverWait(driver, 15)
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'div[data-test-attribute="location-results-card"]')))
# 處理 cookie 同意彈窗(如果有)
try:
cookie_btn = driver.find_element(By.ID, "onetrust-accept-btn-handler")
cookie_btn.click()
time.sleep(1)
except:
pass # 沒有 cookie 彈窗
# 擷取飯店資料
cards = driver.find_elements(By.CSS_SELECTOR, 'div[data-test-attribute="location-results-card"]')
hotels = []
for card in cards:
try:
name = card.find_element(By.CSS_SELECTOR, 'div[data-automation="hotel-card-title"]').text
except:
name = None
try:
rating = card.find_element(By.CSS_SELECTOR, '[data-automation="bubbleRatingValue"]').text
except:
rating = None
try:
reviews = card.find_element(By.CSS_SELECTOR, '[data-automation="bubbleReviewCount"]').text
except:
reviews = None
hotels.append({"name": name, "rating": rating, "review_count": reviews})
print(f"Scraped {len(hotels)} hotels")
for h in hotels[:3]:
print(h)
我在自己的機器上抓一個頁面大約花了 8 秒——相比之下,requests+BS4 不到 1 秒。當你要抓幾百頁時,這個 8 倍差距會很快累積起來。
展開「Read more」並抓完整評論
評論頁會把較長的評論截斷,並藏在「Read more」按鈕後面。Selenium 可以把它點開:
review_url = "https://www.tripadvisor.com/Hotel_Review-g187147-d188726-Reviews-Le_Marais_Hotel-Paris_Ile_de_France.html"
driver.get(review_url)
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'div[data-reviewid]')))
time.sleep(2)
# 點開所有「Read more」按鈕
read_more_buttons = driver.find_elements(By.XPATH, '//button//*[contains(text(), "Read more")]/..')
for btn in read_more_buttons:
try:
driver.execute_script("arguments[0].click();", btn)
time.sleep(0.3)
except:
pass
# 擷取評論
review_elements = driver.find_elements(By.CSS_SELECTOR, 'div[data-reviewid]')
reviews = []
for rev in review_elements:
try:
title = rev.find_element(By.CSS_SELECTOR, 'div[data-test-target="review-title"]').text
except:
title = None
try:
body = rev.find_element(By.CSS_SELECTOR, 'q.IRsGHoPm span').text
except:
try:
body = rev.find_element(By.CSS_SELECTOR, 'p.partial_entry').text
except:
body = None
try:
rating_class = rev.find_element(By.CSS_SELECTOR, 'div[data-test-target="review-rating"] span').get_attribute("class")
# 星等藏在 class 中,例如 "ui_bubble_rating bubble_50" = 5.0
rating_num = [c for c in rating_class.split() if "bubble_" in c][0].replace("bubble_", "")
rating = int(rating_num) / 10
except:
rating = None
reviews.append({"title": title, "body": body, "rating": rating})
print(f"Scraped {len(reviews)} reviews")
為 Selenium 加上代理輪換
如果要長時間持續爬取,就需要代理輪換。由於 selenium-wire 自 2024 年 1 月起已被棄用,建議改用 Chrome 內建的代理支援:
# 不需要驗證的代理
proxy = "http://your-proxy-address:port"
options.add_argument(f"--proxy-server={proxy}")
# 若代理需要驗證,請使用 Chrome 擴充功能或 Selenium 4 的 BiDi 協議
若要程式化輪換代理,可以為每一批請求建立一個新的 driver 實例。雖然不算優雅,但很穩定。
方法 3:隱藏 JSON 法(直接跳過 HTML 解析)
大多數教學都完全忽略這個方法,這很可惜——因為它是三種方法中最快、也最乾淨的。TripAdvisor 會把結構化資料直接嵌在 HTML 頁面中——藏在 <script> 標籤裡,作為像 pageManifest 與 urqlCache 這類 JavaScript 變數。把這些 JSON 抽出來,你就能拿到更乾淨的資料(例如以數字表示的評分、ISO 格式日期),同時減少請求數,而且不需要 JavaScript 渲染。
在頁面原始碼中找出嵌入的 JSON
關鍵做法很簡單:你可以直接用 requests.get() 抓頁面,然後從原始 HTML 中解析 JSON,完全不用渲染 JavaScript。
import requests
import re
import json
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
"Accept-Language": "en-US,en;q=0.9",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Referer": "https://www.tripadvisor.com/",
"Sec-CH-UA": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
"Sec-CH-UA-Mobile": "?0",
"Sec-CH-UA-Platform": '"macOS"',
}
url = "https://www.tripadvisor.com/Hotel_Review-g188590-d194317-Reviews-NH_City_Centre_Amsterdam.html"
response = requests.get(url, headers=headers)
# 擷取 pageManifest JSON 區塊
match = re.search(r"pageManifest:({.+?})};", response.text)
if match:
page_data = json.loads(match.group(1))
print("Found pageManifest data")
print(f"Keys: {list(page_data.keys())[:10]}")
**如果要自己找變數名稱:**在 Chrome 開啟任何 TripAdvisor 飯店頁,右鍵 → 檢視頁面原始碼,然後用 Ctrl+F 搜尋 pageManifest、urqlCache 或 aggregateRating。資料就藏在那裡,等著被解析。
解析 JSON 並提取結構化資料
TripAdvisor 也會嵌入 application/ld+json 的 schema.org 資料,這個甚至更容易擷取:
from parsel import Selector
sel = Selector(text=response.text)
# 擷取 JSON-LD 結構化資料
json_ld_scripts = sel.xpath("//script[@type='application/ld+json']/text()").getall()
for script in json_ld_scripts:
data = json.loads(script)
if isinstance(data, dict) and data.get("@type") in ["Hotel", "Restaurant", "TouristAttraction"]:
print(f"Name: {data.get('name')}")
print(f"Rating: {data.get('aggregateRating', {}).get('ratingValue')}")
print(f"Review Count: {data.get('aggregateRating', {}).get('reviewCount')}")
print(f"Price Range: {data.get('priceRange')}")
print(f"Address: {data.get('address', {}).get('streetAddress')}")
print(f"Coordinates: {data.get('geo', {}).get('latitude')}, {data.get('geo', {}).get('longitude')}")
break
JSON-LD 資料是直接嵌在靜態 HTML 裡的,不需要 JavaScript 渲染。它會提供商家名稱、整體評分、評論數、地址、座標、價格區間與照片 URL——完全不需要解析任何 HTML 標籤。
如果你想拿更完整的資料(例如單筆評論、評分分布、設施清單),就要用 urqlCache 物件:
# 擷取 urqlCache 以取得詳細評論資料
cache_match = re.search(r'"urqlCache"\s*:\s*({.+?})\s*,\s*"redux"', response.text)
if cache_match:
cache_data = json.loads(cache_match.group(1))
# 走訪 cache 找評論資料
for key, value in cache_data.items():
if "reviews" in str(value).lower()[:100]:
reviews_data = json.loads(value.get("data", "{}")) if isinstance(value, dict) else None
if reviews_data:
print(f"Found review cache entry: {key[:50]}...")
break
TripAdvisor 更新前端時,實際 JSON 路徑偶爾會變,但大方向——JSON-LD 用於摘要資料、urqlCache 用於細節資料——多年來都相當穩定。
逆向 TripAdvisor 的 GraphQL API(進階)
如果你要大規模擷取,TripAdvisor 的 GraphQL 端點可以直接回傳結構化資料。這是最快的方法,但維護成本也最高。
import httpx
import random
import string
def generate_request_id():
"""產生 X-Requested-By header 的值"""
random_chars = ''.join(random.choices(string.ascii_letters + string.digits, k=180))
return f"TNI1625!{random_chars}"
# 搜尋 Paris 的飯店
search_payload = [{
"variables": {
"request": {
"query": "hotels in Paris",
"limit": 10,
"scope": "WORLDWIDE",
"locale": "en-US",
"scopeGeoId": 1,
"searchCenter": None,
"types": ["LOCATION", "QUERY_SUGGESTION", "RESCUE_RESULT"],
"locationTypes": ["GEO", "AIRPORT", "ACCOMMODATION", "ATTRACTION", "EATERY", "NEIGHBORHOOD"]
}
},
"extensions": {
"preRegisteredQueryId": "84b17ed122fbdbd4"
}
}]
graphql_headers = {
"Content-Type": "application/json",
"Accept": "*/*",
"Accept-Language": "en-US,en;q=0.9",
"Origin": "https://www.tripadvisor.com",
"Referer": "https://www.tripadvisor.com/Hotels",
"X-Requested-By": generate_request_id(),
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
}
with httpx.Client() as client:
response = client.post(
"https://www.tripadvisor.com/data/graphql/ids",
json=search_payload,
headers=graphql_headers
)
if response.status_code == 200:
results = response.json()
print(json.dumps(results, indent=2)[:1000])
else:
print(f"GraphQL request failed: {response.status_code}")
抓取評論的 GraphQL 範例如下:
review_payload = [{
"variables": {
"locationId": 194317, # NH City Centre Amsterdam
"offset": 0,
"limit": 20,
"filters": {},
"sortType": None,
"sortBy": "date",
"language": "en",
"doMachineTranslation": False,
"photosPerReviewLimit": 3
},
"extensions": {
"preRegisteredQueryId": "ef1a9f94012220d3"
}
}]
with httpx.Client() as client:
response = client.post(
"https://www.tripadvisor.com/data/graphql/ids",
json=review_payload,
headers=graphql_headers
)
if response.status_code == 200:
data = response.json()
reviews = data[0]["data"]["locations"][0]["reviewListPage"]["reviews"]
total = data[0]["data"]["locations"][0]["reviewListPage"]["totalCount"]
print(f"Total reviews: {total}")
for r in reviews[:3]:
print(f" [{r['rating']}/5] {r['title']} - {r['createdDate']}")
**重要提醒:**像 84b17ed122fbdbd4(搜尋)和 ef1a9f94012220d3(評論)這類 preRegisteredQueryId,在 TripAdvisor 重新部署時可能會失效。失效時請求通常會靜默失敗,你需要透過瀏覽器 DevTools 監看網路請求,重新找出 query ID。
為什麼這種方法可以減少代理需求
邏輯很簡單。用 requests+BS4 時,抓 100 個飯店詳細頁就需要 100 次請求。使用隱藏 JSON 方法時,每次請求都能從單一頁面載入拿到你需要的全部資料——不必再額外請求展開評論或載入動態內容。用 GraphQL 時,一次 API 呼叫就能回傳 20 則評論。請求越少 = 暴露在速率限制下的機會越低 = 越不需要輪換代理。對於中小型專案(少於 1,000 頁),只要你加上合理延遲,甚至可能完全不需要代理。
用一支可重用的程式抓飯店、餐廳與景點
五分之四的競爭教學只涵蓋飯店。但 TripAdvisor 有三大核心內容類別,而且 URL 與資料欄位在不同類別之間會有差異。下面教你如何寫一個同時處理三種內容的函式。
各類別可取得的資料欄位
| 欄位 | 飯店 | 餐廳 | 景點 |
|---|---|---|---|
| 名稱 | ✅ | ✅ | ✅ |
| 評分 | ✅ | ✅ | ✅ |
| 評論數 | ✅ | ✅ | ✅ |
| 價格/價格區間 | ✅ | ✅ | 有時候 |
| 地址 | ✅ | ✅ | ✅ |
| 菜系 | ❌ | ✅ | ❌ |
| 停留時間/行程類型 | ❌ | ❌ | ✅ |
| 設施 | ✅ | ❌ | ❌ |
| 座標 | ✅ | ✅ | ✅ |
建立可重用的 scrape_tripadvisor() 函式
import requests
from bs4 import BeautifulSoup
import pandas as pd
import time
import random
import re
import json
def scrape_tripadvisor(category, location_id, location_name, num_pages=3):
"""
抓取 TripAdvisor 的飯店、餐廳或景點列表。
參數:
category: "hotels"、"restaurants" 或 "attractions"
location_id: TripAdvisor geo ID(例如 Paris 是 "187147")
location_name: 方便放進 URL 的名稱(例如 "Paris_Ile_de_France")
num_pages: 要抓取的頁數
"""
url_patterns = {
"hotels": "https://www.tripadvisor.com/Hotels-g{geo}-oa{offset}-{name}-Hotels.html",
"restaurants": "https://www.tripadvisor.com/Restaurants-g{geo}-oa{offset}-{name}.html",
"attractions": "https://www.tripadvisor.com/Attractions-g{geo}-oa{offset}-Activities-{name}.html",
}
first_page_patterns = {
"hotels": "https://www.tripadvisor.com/Hotels-g{geo}-{name}-Hotels.html",
"restaurants": "https://www.tripadvisor.com/Restaurants-g{geo}-{name}.html",
"attractions": "https://www.tripadvisor.com/Attractions-g{geo}-Activities-{name}.html",
}
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
"Accept-Language": "en-US,en;q=0.9",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Referer": "https://www.tripadvisor.com/",
"Sec-CH-UA": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
"Sec-CH-UA-Mobile": "?0",
"Sec-CH-UA-Platform": '"Windows"',
}
session = requests.Session()
session.headers.update(headers)
all_items = []
for page in range(num_pages):
offset = page * 30
if page == 0:
url = first_page_patterns[category].format(geo=location_id, name=location_name)
else:
url = url_patterns[category].format(geo=location_id, offset=offset, name=location_name)
response = session.get(url)
if response.status_code != 200:
print(f" Page {page + 1}: Status {response.status_code}, stopping.")
break
soup = BeautifulSoup(response.text, "html.parser")
cards = soup.select('div[data-test-attribute="location-results-card"]')
for card in cards:
item = {"category": category}
title_el = card.select_one('div[data-automation="hotel-card-title"]') or card.select_one('a[data-automation]')
item["name"] = title_el.get_text(strip=True) if title_el else None
rating_el = card.select_one('[data-automation="bubbleRatingValue"]')
item["rating"] = rating_el.get_text(strip=True) if rating_el else None
review_el = card.select_one('[data-automation="bubbleReviewCount"]')
item["review_count"] = review_el.get_text(strip=True) if review_el else None
all_items.append(item)
print(f" Page {page + 1}: {len(cards)} items found")
time.sleep(random.uniform(3, 7))
return pd.DataFrame(all_items)
# 使用範例
print("=== Paris 的飯店 ===")
hotels_df = scrape_tripadvisor("hotels", "187147", "Paris_Ile_de_France", num_pages=2)
print(hotels_df.head())
print("\n=== Rome 的餐廳 ===")
restaurants_df = scrape_tripadvisor("restaurants", "187791", "Rome_Lazio", num_pages=2)
print(restaurants_df.head())
print("\n=== Barcelona 的景點 ===")
attractions_df = scrape_tripadvisor("attractions", "187497", "Barcelona_Catalonia", num_pages=2)
print(attractions_df.head())
一個函式,三個類別,零重複程式碼。如果 TripAdvisor 改了 selector,你只要在一個地方修正即可。
TripAdvisor 擋你時該怎麼辦(反機器人排查)
這是我一開始抓 TripAdvisor 時最需要的章節,也是其他教學最少會結構化提供的部分。TripAdvisor 同時使用 DataDome(每天分析超過 5 兆筆資料點)與 Cloudflare WAF。下面是常見失敗情境的診斷表:
| 症狀 | 可能原因 | 解法 |
|---|---|---|
| HTTP 403 回應 | 缺少或可疑的 headers;Cloudflare JS challenge | 設定真實且一致的 User-Agent、Accept-Language、Referer 與 Sec-CH-UA headers,並確保彼此一致。 |
| 出現 CAPTCHA 頁面而不是資料 | 速率限制或瀏覽器指紋被識別 | 輪換住宅代理,加入隨機延遲(每次請求間 2–7 秒) |
| HTML 為空或頁面內容空白 | requests 沒有渲染 JavaScript | 改用 Selenium,或直接從頁面原始碼中的隱藏 JSON 擷取 |
| 評論只顯示部分內容/"Read more" 無法展開 | 內容在點擊事件後才載入 | 使用 Selenium 的 .click(),或直接解析嵌入的 JSON blob |
| 評論只出現單一語言 | 缺少語言參數 | 在評論 URL 後加上 ?filterLang=ALL |
| 抓到第 N 頁後就不再載入 | 基於 session 的速率限制 | 重新建立 session、每批次清除 cookies |
| HTTP 1020 Access Denied | IP/ASN 被 Cloudflare 封鎖 | 從資料中心代理改用住宅代理 |
| 無限 CAPTCHA 迴圈 | cookie 持久化失效 | 先訪問首頁暖機 session;維持 cookie jar |
使用指數退避的重試邏輯
一般文章其實不會把這段程式碼寫出來。下面是可重用的重試函式:
import time
import random
import requests
def fetch_with_retry(session, url, max_retries=4, base_delay=2, max_delay=60):
"""
使用指數退避與抖動抓取 URL。
每次重試都會輪換 User-Agent。
"""
user_agents = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
]
for attempt in range(max_retries):
# 每次重試都輪換 User-Agent
if attempt > 0:
session.headers["User-Agent"] = random.choice(user_agents)
try:
response = session.get(url, timeout=30)
if response.status_code == 200:
return response
if response.status_code == 429:
# 如果有 Retry-After 標頭,就尊重它
retry_after = int(response.headers.get("Retry-After", base_delay * (2 ** attempt)))
print(f" Rate limited (429). Waiting {retry_after}s...")
time.sleep(retry_after)
continue
if response.status_code in (403, 503):
wait = min(base_delay * (2 ** attempt) + random.uniform(0, 1), max_delay)
print(f" Got {response.status_code}. Retry {attempt + 1}/{max_retries} in {wait:.1f}s...")
time.sleep(wait)
continue
# 其他錯誤碼不重試
print(f" Unexpected status {response.status_code} for {url}")
return response
except requests.exceptions.Timeout:
wait = min(base_delay * (2 ** attempt) + random.uniform(0, 1), max_delay)
print(f" Timeout. Retry {attempt + 1}/{max_retries} in {wait:.1f}s...")
time.sleep(wait)
print(f" All {max_retries} retries exhausted for {url}")
return None
旋轉 headers、代理與 session
如果要長期持續爬取,請維護一組 headers 池,並一起輪換:
import random
HEADER_SETS = [
{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
"Sec-CH-UA": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
"Sec-CH-UA-Platform": '"Windows"',
},
{
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
"Sec-CH-UA": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
"Sec-CH-UA-Platform": '"macOS"',
},
{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
"Sec-CH-UA": '"Google Chrome";v="134", "Not-A.Brand";v="8", "Chromium";v="134"',
"Sec-CH-UA-Platform": '"Windows"',
},
]
PROXY_LIST = [
"http://user:pass@residential-proxy-1:port",
"http://user:pass@residential-proxy-2:port",
# 加入更多住宅代理
]
def get_rotated_session():
"""建立一個帶有輪換 headers 與代理的新 session。"""
session = requests.Session()
# 隨機選一組 headers
header_set = random.choice(HEADER_SETS)
base_headers = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Referer": "https://www.tripadvisor.com/",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-CH-UA-Mobile": "?0",
}
base_headers.update(header_set)
session.headers.update(base_headers)
# 隨機選一個代理
if PROXY_LIST:
proxy = random.choice(PROXY_LIST)
session.proxies = {"http": proxy, "https": proxy}
return session
代理類型很重要。 資料中心代理會被 TripAdvisor 幾乎立刻封掉(HTTP 1020 Access Denied)。要長期穩定爬取,住宅代理幾乎是必需品——它們透過一般消費者 ISP 路由,外觀上與真實使用者幾乎無異。依供應商不同,費用大約是每 GB $2.50–$8.40。
匯出與儲存你的 TripAdvisor 資料
一旦拿到資料,把它整理成可用格式其實很簡單。
匯出 CSV(最常見)
import pandas as pd
df = pd.DataFrame(all_hotels)
df.to_csv("tripadvisor_hotels_paris.csv", index=False, encoding="utf-8-sig")
print(f"Exported {len(df)} rows to CSV")
encoding='utf-8-sig' 很重要——這能確保 Excel 開啟 CSV 時,正確顯示非拉丁字元(例如法文重音、中文等)。
匯出 JSON(適合巢狀資料)
當你的評論是巢狀在飯店底下時,JSON 可以保留資料階層:
# 階層式結構
hotel_data = {
"property_id": "d194317",
"name": "NH City Centre Amsterdam",
"rating": 4.0,
"reviews": [
{"title": "地點很好", "rating": 5, "date": "2025-03-15", "text": "..."},
{"title": "普通住宿", "rating": 3, "date": "2025-03-10", "text": "..."},
]
}
# 若要做平面分析,可用 json_normalize
flat_reviews = pd.json_normalize(
hotel_data,
record_path="reviews",
meta=["property_id", "name"]
)
flat_reviews.to_csv("reviews_flat.csv", index=False)
關聯式資料建議使用兩個檔案
對於大型資料集,我通常會用兩個 CSV:
hotels.csv— 每筆一間飯店(平面資料)reviews.csv— 每筆一則評論,並用property_id作為外鍵
這樣在 pandas 裡合併、匯入資料庫,或丟到 BI 工具裡都很方便。
如果你不想處理這些匯出邏輯,Thunderbit 可以直接讓你把 抓到的資料匯出 到 Excel、Google Sheets、Airtable 或 Notion——全部免費、完全不用寫程式。當你需要把結果分享給非技術同事時,特別方便。
負責任且有效率地抓 TripAdvisor 的建議
以下六點是比較負責任的爬取原則:
- **檢查
robots.txt:**TripAdvisor 的 robots.txt 會完全封鎖 AI 訓練 bots(像 GPTBot、ClaudeBot 等)。一般爬蟲則面臨部分路徑限制。可在tripadvisor.com/robots.txt查看。 - **加入延遲:**每次請求間隔 3–7 秒是較安全的範圍。速度超過每個 IP 每分鐘 10–15 次請求,容易觸發速率限制。
- **只抓公開資料。**不要登入去存取受限內容。
- 安全儲存資料,如果涉及個資(例如評論者姓名),請遵守 GDPR/CCPA。
- 若需要商業級資料,考慮 TripAdvisor 官方 API。開發者入口網站 可提供商家資訊,以及每個地點最多 5 則評論與 5 張照片——限制較多,但合法且穩定。
- 注意法律背景:歐盟法院 Ryanair 判決(2025 年 12 月) 強化了歐盟內基於服務條款的爬取禁止。TripAdvisor 的服務條款明確禁止爬取。請負責任地抓取,並自行承擔風險。
總結
完整輪廓就是這樣。
- Requests + BeautifulSoup 是最簡單的路線。它適合靜態列表頁、設定最少、速度也快。如果你要抓少於 100 頁,而且不需要 JavaScript 渲染內容,先從這裡開始。
- Selenium 能處理 requests 做不到的事:動態內容、「Read more」按鈕、cookie 橫幅。它慢 5 倍、資源消耗也高,但只要你需要跟頁面互動,它就是必需品。
- 隱藏 JSON / GraphQL 是最乾淨、最快的方案。它不需要解析 HTML 就能拿到結構化資料,減少請求數(也就降低對代理的需求),而且回傳的資料格式更適合直接分析。不過它需要先做較多逆向工程,而且當 TripAdvisor 的資料結構變動時,偶爾也需要維護。
可重用的 scrape_tripadvisor() 函式已經涵蓋飯店、餐廳與景點。你不需要再找第二篇教學。
如果你在教學中途發現自己其實不想寫程式——或只是今天下班前需要 50 間飯店資料進到試算表——Thunderbit Chrome 擴充功能 可以用兩個點擊完成,還具備 AI 欄位辨識、自動分頁,以及免費匯出到 Excel 或 Google Sheets。完全不需要 Python。
如果你想更深入了解,我們在 Thunderbit 部落格 和 YouTube 頻道 還有更多爬取實作教學。
常見問題
1. 抓 TripAdvisor 合法嗎?
TripAdvisor 的服務條款明確禁止爬取。不過,法院普遍認為,抓取公開可見、未登入保護的資料,通常不違反美國的 Computer Fraud and Abuse Act。話雖如此,2025 年歐盟法院 Ryanair 判決已強化歐洲對於基於服務條款之限制。如果你只抓公開資料、遵守 robots.txt、不重製受著作權保護的內容,且在商業使用前諮詢法律意見,風險會比較可控。
2. 不用 Python 也能抓 TripAdvisor 嗎?
可以。像 Thunderbit 這類免寫程式工具,就能直接從瀏覽器抓 TripAdvisor,並具備 AI 欄位辨識與自動分頁功能。你也可以使用瀏覽器擴充功能、Google Sheets 外掛或商業爬取 API。Python 雖然控制力與彈性最高,但不是唯一選擇。
3. 要怎麼避免抓 TripAdvisor 時被封?
重點做法有:使用真實且一致的 headers(尤其是 User-Agent 與 Sec-CH-UA)、輪換住宅代理(資料中心 IP 幾乎會立刻被封)、每次請求間加入 3–7 秒隨機延遲、用隱藏 JSON 方法減少總請求數、加入指數退避重試邏輯,以及在深入頁面前先拜訪首頁暖機 session。
4. TripAdvisor 可以抓哪些資料?
飯店、餐廳與景點——包括名稱、評分、評論數、價格區間、地址、座標、設施(飯店)、菜系(餐廳)、行程時長(景點),以及完整評論文字、個別評分與日期。隱藏 JSON 與 GraphQL 方法能在每次請求中拿到最豐富的資料。
5. 我一天可以從 TripAdvisor 抓多少頁?
單一 IP 並搭配合理延遲的情況下:約 600–1,000 頁/天。若使用 20 個輪換住宅代理:採用 request 型方法時,大約可達 200,000–300,000 頁/天。Selenium 會慢很多——每個代理約可抓 8,000–12,000 頁/天。隱藏 JSON/GraphQL 方法能讓你每次請求拿到最多資料,所以為了取得相同資訊,總頁數可能少很多。
延伸閱讀


