先週、TripAdvisorからヨーロッパ3都市にある約200件の宿泊施設について、評価とレビュー件数を取ろうとしました。最初に書いたスクリプトは、requests.get() にデフォルトヘッダーを足しただけのシンプルなもの。結果は見事に全リクエストで 403 Forbidden。使えるデータは1バイトも取れませんでした。
TripAdvisor は、旅行業界でもかなり豊富な公開データソースです。1億件以上のレビュー、800万件超のビジネス掲載情報、そして月間約4億6,000万人のユニーク訪問者を抱えています。年間の旅行支出に与える影響は600億ドル超とも言われています。とはいえ、これをプログラムで取ろうとすると話は別。TripAdvisor は DataDome のボット検知、Cloudflare の WAF、TLS フィンガープリンティング、JavaScript チャレンジを組み合わせた多層防御を採用していて、気軽なスクレイピングは始める前に弾かれます。
この記事は、私自身が欲しかった「1本で完結する実践ガイド」です。Python での3つのスクレイピング手法(さらにノーコード案も含む)を横並びで比較し、それぞれの完全なコード、体系的な対ボット対策のトラブルシューティング、ホテル・レストラン・観光スポットに共通して使える再利用可能なパターンまでまとめました。Python 初心者でも、経験豊富な開発者でも、これで無駄な 403 はかなり減らせるはずです。
コードを書きたくない人へ:TripAdvisorを簡単に取得する方法
最初に正直に言っておきます。"scrape TripAdvisor with Python" で検索する人の多くは、実はコードを書きたいわけではありません。欲しいのは、ホテル名、評価、レビュー件数、料金などのデータを、すぐにスプレッドシートへ出したいだけ。もしそれに当てはまるなら、もっと手っ取り早いルートがあります。
Thunderbit は、AI を搭載した Chrome 拡張機能です。TripAdvisor のどのページでも読み取り、抽出すべき列を自動で提案してくれます。流れは本当に2クリックです。
- TripAdvisor の一覧ページを開く(例: 「Paris のホテル」検索結果)
- Thunderbit のサイドバーで 「AI Suggest Fields」 をクリックする。AI がページを解析し、Hotel Name、Rating、Review Count、Price、Location などの列を提案します。
- 「Scrape」 をクリックする。Thunderbit がページ上の全掲載情報を抽出し、必要ならページネーションも自動処理します。
- Excel、Google Sheets、Airtable、Notion にエクスポートする。エクスポートはどのプランでも無料です。
Thunderbit は、ホテル、レストラン、観光スポットのどれにも設定変更なしで対応します。AI がそのページにある内容に合わせて柔軟に動くからです。ページ分割された結果にも対応していて、「次へ」ボタンや無限スクロールも自動で見つけます。さらに、実際の Chrome ブラウザ上で動くので、セッション Cookie やブラウザのフィンガープリントもそのまま引き継げます。これはボット検知に対してかなり自然な強みになります。
Thunderbit Chrome Extension で試せます。無料プランでは月6ページまで使えるので、ワークフローの確認には十分です。
一方で、プログラム制御や独自の解析ロジックが必要な場合、あるいは1万ページ以上をスクレイピングする予定なら、Python が向いています。続きをどうぞ。
なぜTripAdvisorをPythonでスクレイピングするのか?
TripAdvisor のデータは、ビジネスへの直接的で測定しやすい影響を持ちます。Cornell University の研究によると、ホテルの100点満点の Global Review Index が1ポイント上がると、平均日額料金は0.89%、RevPAR(販売可能客室あたり売上)は1.42%上昇します。別の ScienceDirect の研究では、TripAdvisor の評価が外部要因で1つ上がるだけで、平均的なホテルの年間収益が5万5,000〜7万5,000ドル増えると示されています。レビューは、見た目の指標ではなく売上を動かす要素です。
各チームが TripAdvisor データをどう使うかを見てみましょう。
| ユースケース | 役立つ担当者 | 必要なデータ |
|---|---|---|
| ホテルの競合分析 | ホテルチェーン、収益管理担当 | 評価、料金、レビュー数、設備 |
| レストラン市場調査 | レストラングループ、食品ブランド | 料理ジャンル、価格帯、レビューの傾向 |
| 観光スポットのトレンド追跡 | 旅行事業者、観光局 | 人気順位、季節変動パターン |
| 感情分析 | 研究者、データアナリスト | レビュー全文、星評価、日付 |
| リード獲得 | 営業チーム、旅行代理店 | 施設名、連絡先、所在地 |
では、なぜ Python なのか。理由は3つあります。1つ目はエコシステムです。BeautifulSoup、Selenium、Playwright、Scrapy、httpx、pandas など、スクレイピングとデータ分析のライブラリがかなり成熟しています。2つ目は、Web スクレイピング開発者の71.7%が Python を使っている こと。つまり、コミュニティの支援も、Stack Overflow の回答も、最新の解説記事も豊富です。3つ目は、処理の流れを1言語で完結できること。BeautifulSoup で取得し、pandas で整形し、Hugging Face Transformers で感情分析し、ダッシュボードまで作れる。コンテキスト切り替えが要りません。
PythonでTripAdvisorをスクレイピングする3つの方法(比較)
他のガイドは、ひとつの手法だけを推しがちです。でも、コードを書く前に比較したい人には、それでは足りません。そこで、私なら最初に欲しかった比較表を載せます。
| 手法 | 速度 | JS対応 | ボット対策耐性 | 難易度 | 向いている用途 |
|---|---|---|---|---|---|
requests + BeautifulSoup | ⚡ 高速(生の状態で約120〜200ページ/分) | ❌ なし | ⚠️ 低い | 簡単 | 静的な一覧ページ、小規模プロジェクト |
| Selenium / ヘッドレスブラウザ | 🐢 遅い(約8〜20ページ/分) | ✅ 完全対応 | ⚠️ 中程度 | 中 | 動的コンテンツ、「続きを読む」クリック、Cookie バナー |
| 非公開 JSON / GraphQL API | ⚡⚡ 最速(生の状態で約200〜600ページ/分) | N/A | ✅ 高い | 難しい | 大規模なレビュー/ホテル抽出 |
| ノーコード(Thunderbit) | ⚡ 高速 | ✅ 標準搭載 | ✅ 標準搭載 | 最も簡単 | 非エンジニア、単発のエクスポート |
いくつか大事な注意点があります。ここで示した生の速度は理論値です。TripAdvisor にはレート制限(IP あたり毎分約10〜15リクエスト)があるので、実際のスループットは手法に関係なく、1 IP あたりおおむね毎分10ページほどに落ち着きます。非公開 JSON の方法は1リクエストで取れる情報量が多いので、総リクエスト数を減らせて、レート制限にも触れにくくなります。Selenium は実運用では requests ベースより約5倍遅いですが、ボタンのクリックや JavaScript の描画が必要な場面では唯一の選択肢です。
このガイドの残りでは、3つの 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 セレクタ(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 はカテゴリごとにかなり予測しやすい形になっています。
- Hotels:
https://www.tripadvisor.com/Hotels-g{locationId}-{Location_Name}-Hotels.html - Restaurants:
https://www.tripadvisor.com/Restaurants-g{locationId}-{Location_Name}.html - Attractions:
https://www.tripadvisor.com/Attractions-g{locationId}-Activities-{Location_Name}.html
ページネーションには oa(offset anchors)パラメータが使われ、URL に差し込まれます。1ページあたり30件表示です。
- 1ページ目: ベースURL(
oaなし) - 2ページ目:
Hotels-g187768-oa30-Italy-Hotels.html - 3ページ目:
Hotels-g187768-oa60-Italy-Hotels.html
レビューページでは、オフセットは or で、10件ずつ増えます。
- 1ページ目:
Reviews-or0-Hotel_Name.html - 2ページ目:
Reviews-or10-Hotel_Name.html
全言語のレビューを取りたい場合は、URL の末尾に ?filterLang=ALL を付けます。
実在のブラウザに近いヘッダーでリクエストを送る
TripAdvisor はヘッダーをかなり厳しく見ます。Python のデフォルトヘッダーでは即ブロックされます。Chrome ブラウザを真似る必要があります。
import requests
import time
import random
session = requests.Session()
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Referer": "https://www.tripadvisor.com/",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-CH-UA": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
"Sec-CH-UA-Mobile": "?0",
"Sec-CH-UA-Platform": '"Windows"',
}
session.headers.update(headers)
url = "https://www.tripadvisor.com/Hotels-g187147-Paris_Ile_de_France-Hotels.html"
response = session.get(url)
print(f"Status: {response.status_code}")
print(f"Content length: {len(response.text)} characters")
重要なポイント: TripAdvisor は User-Agent と Sec-CH-UA の Client Hints が一致しているか確認します。User-Agent では Chrome 135 を名乗っているのに、Sec-CH-UA が Chrome 120 だと、不一致として検知されます。ヘッダーはバラバラにせず、セットごとローテーションしましょう。
BeautifulSoupで一覧を解析する
成功したレスポンスが取れたら、BeautifulSoup でデータを抜き出します。TripAdvisor は、頻繁に変わる CSS クラス名よりも、data-automation や data-test-attribute 属性を使っているため、こちらのほうが安定しています。
from bs4 import BeautifulSoup
soup = BeautifulSoup(response.text, "html.parser")
# ホテル一覧カードをすべて取得
cards = soup.select('div[data-test-attribute="location-results-card"]')
hotels = []
for card in cards:
# ホテル名
title_el = card.select_one('div[data-automation="hotel-card-title"]')
name = title_el.get_text(strip=True) if title_el else None
# 詳細ページへのリンク
link_el = card.select_one('div[data-automation="hotel-card-title"] a')
link = "https://www.tripadvisor.com" + link_el["href"] if link_el else None
# 評価
rating_el = card.select_one('[data-automation="bubbleRatingValue"]')
rating = rating_el.get_text(strip=True) if rating_el else None
# レビュー件数
review_el = card.select_one('[data-automation="bubbleReviewCount"]')
review_count = review_el.get_text(strip=True).replace(",", "").split()[0] if review_el else None
hotels.append({
"name": name,
"rating": rating,
"review_count": review_count,
"url": link,
})
print(f"このページで見つかったホテル数: {len(hotels)}")
for h in hotels[:3]:
print(h)
セレクタの注意点: TripAdvisor は FGwzt や yyzcQ のような難読化された CSS クラス名を使っていますが、これはサイト更新のたびに変わります。data-automation や data-test-target 属性のほうがずっと安定しています。クラス名よりデータ属性を優先してください。
ページネーションの処理
複数ページを取るには、オフセットをループしつつ、リクエストの間に適度な待機を入れます。
import pandas as pd
all_hotels = []
base_url = "https://www.tripadvisor.com/Hotels-g187147-oa{offset}-Paris_Ile_de_France-Hotels.html"
for page in range(5): # 最初の5ページ
offset = page * 30
url = base_url.format(offset=offset) if page > 0 else "https://www.tripadvisor.com/Hotels-g187147-Paris_Ile_de_France-Hotels.html"
response = session.get(url)
if response.status_code != 200:
print(f"ページ {page + 1}: ステータス {response.status_code} なので停止します。")
break
soup = BeautifulSoup(response.text, "html.parser")
cards = soup.select('div[data-test-attribute="location-results-card"]')
for card in cards:
title_el = card.select_one('div[data-automation="hotel-card-title"]')
name = title_el.get_text(strip=True) if title_el else None
rating_el = card.select_one('[data-automation="bubbleRatingValue"]')
rating = rating_el.get_text(strip=True) if rating_el else None
review_el = card.select_one('[data-automation="bubbleReviewCount"]')
review_count = review_el.get_text(strip=True).replace(",", "").split()[0] if review_el else None
all_hotels.append({"name": name, "rating": rating, "review_count": review_count})
print(f"ページ {page + 1}: {len(cards)} 件のホテルを取得")
time.sleep(random.uniform(3, 7)) # レート制限回避のためランダムに待機
df = pd.DataFrame(all_hotels)
print(f"\nスクレイピングしたホテル総数: {len(df)}")
time.sleep(random.uniform(3, 7)) はかなり重要です。TripAdvisor のレート制限は、おおむね IP あたり毎分10〜15リクエストです。これを超えると CAPTCHA や 429 エラーが出やすくなります。
この方法の限界
この方法がうまくいかなくなるのは、たとえば次のような場面です。
- TripAdvisor が JavaScript 描画済みのコンテンツを返す場合(検索結果ページの一部は JS が必要)
- レビュー本文が「続きを読む」ボタンの裏に隠れている場合
- ボット対策が JavaScript チャレンジや CAPTCHA にまで強化されている場合
- 料金や空室状況のように、クライアントサイド描画後にしか出ないデータが必要な場合
こうしたケースでは、Selenium(方法2)か非公開 JSON の方法(方法3)が必要です。
方法2: SeleniumでTripAdvisorをスクレイピングする(ヘッドレスブラウザ)
Selenium は実ブラウザを起動するので、JavaScript の描画、ボタンのクリック、Cookie 同意バナーの処理、動的コンテンツとのやり取りができます。代わりに、約5倍遅くなり、ブラウザ1インスタンスあたり 300〜500MB の RAM を消費します。
検知されにくい設定でSeleniumを構成する
そのままの Selenium はかなり簡単に見つかります。TripAdvisor のフィンガープリンティングはすぐ反応します。自動化フラグを無効化する必要があります。
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
options = Options()
options.add_argument("--headless=new") # 新しい headless モードを使用(Chrome 112+)
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_argument("--window-size=1920,1080")
options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36")
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option("useAutomationExtension", False)
driver = webdriver.Chrome(options=options)
# navigator から webdriver プロパティを削除
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
})
これでTripAdvisorに十分か? 小規模スクレイピング(50ページ未満)なら、住宅系プロキシを併用するとこの設定でうまくいくことが多いです。大量取得では、undetected-chromedriver や nodriver が必要になる場合があります。TripAdvisor の DataDome は、TLS フィンガープリントも含めて1,000以上のシグナルを解析しており、素の 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"{len(hotels)} 件のホテルを取得しました")
for h in hotels[:3]:
print(h)
私の環境では、1ページの取得に約8秒かかりました。requests+BS4 なら1秒未満です。この差は、数百ページを取るとすぐ大きくなります。
「続きを読む」を展開してレビュー全文を取得する
レビューページでは、長い本文が「続きを読む」ボタンの裏に隠れています。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_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")
# rating は "ui_bubble_rating bubble_50" のような class に埋め込まれている = 5.0
rating_num = [c for c in rating_class.split() if "bubble_" in c][0].replace("bubble_", "")
rating = int(rating_num) / 10
except:
rating = None
reviews.append({"title": title, "body": body, "rating": rating})
print(f"{len(reviews)} 件のレビューを取得しました")
Seleniumにプロキシローテーションを追加する
継続的にスクレイピングするなら、プロキシのローテーションが必要です。selenium-wire は 2024 年1月以降非推奨なので、Chrome の標準プロキシ対応を使います。
# 認証不要プロキシの場合
proxy = "http://your-proxy-address:port"
options.add_argument(f"--proxy-server={proxy}")
# 認証付きプロキシの場合は、Chrome 拡張機能か Selenium 4 の BiDi プロトコルを使う
プログラムでローテーションする場合は、バッチごとに別のプロキシで新しい driver インスタンスを作ります。きれいではないですが、信頼性は高いです。
方法3: 非公開JSONを使う(HTML解析を丸ごとスキップ)
多くのガイドはこの方法を完全に見落としていますが、実は3つの中で最速で、いちばんきれいです。TripAdvisor は構造化データを HTML 内に JSON として埋め込んでいます。pageManifest や urqlCache のような JavaScript 変数が <script> タグの中に入っているのです。この JSON を取り出せば、よりきれいなデータ(数値としての評価、ISO 形式の日付など)を、少ないリクエストで取得でき、JavaScript の描画も不要です。
ページソースに埋め込まれたJSONを見つける
ポイントは、requests.get() でページを取って、JavaScript を描画せずに生の HTML から JSON を抜き出せることです。
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("pageManifest データを見つけました")
print(f"Keys: {list(page_data.keys())[:10]}")
自分で変数名を探す方法: Chrome で TripAdvisor のホテルページを開き、右クリック → ページのソースを表示、のあと 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 タグを1つも解析せずに取れます。
より詳細なデータ(個別レビュー、評価内訳、設備リストなど)が必要なら、urqlCache オブジェクトを使います。
# 詳細レビュー用に urqlCache を抽出
cache_match = re.search(r'"urqlCache"\s*:\s*({.+?})\s*,\s*"redux"', response.text)
if cache_match:
cache_data = json.loads(cache_match.group(1))
# キャッシュをたどってレビュー関連データを探す
for key, value in cache_data.items():
if "reviews" in str(value).lower()[:100]:
reviews_data = json.loads(value.get("data", "{}")) if isinstance(value, dict) else None
if reviews_data:
print(f"レビューキャッシュ項目を発見: {key[:50]}...")
break
TripAdvisor がフロントエンドを更新すると JSON のパスが少し変わることはありますが、JSON-LD で要約データ、urqlCache で詳細データ、という大枠は何年も安定しています。
TripAdvisorのGraphQL APIを逆解析する(上級者向け)
大規模に取るなら、TripAdvisor の GraphQL エンドポイントが構造化データを直接返します。これが最速ですが、保守の手間は最も大きいです。
import httpx
import random
import string
def generate_request_id():
"""X-Requested-By ヘッダー値を生成する"""
random_chars = ''.join(random.choices(string.ascii_letters + string.digits, k=180))
return f"TNI1625!{random_chars}"
# パリのホテルを検索
search_payload = [{
"variables": {
"request": {
"query": "hotels in Paris",
"limit": 10,
"scope": "WORLDWIDE",
"locale": "en-US",
"scopeGeoId": 1,
"searchCenter": None,
"types": ["LOCATION", "QUERY_SUGGESTION", "RESCUE_RESULT"],
"locationTypes": ["GEO", "AIRPORT", "ACCOMMODATION", "ATTRACTION", "EATERY", "NEIGHBORHOOD"]
}
},
"extensions": {
"preRegisteredQueryId": "84b17ed122fbdbd4"
}
}]
graphql_headers = {
"Content-Type": "application/json",
"Accept": "*/*",
"Accept-Language": "en-US,en;q=0.9",
"Origin": "https://www.tripadvisor.com",
"Referer": "https://www.tripadvisor.com/Hotels",
"X-Requested-By": generate_request_id(),
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
}
with httpx.Client() as client:
response = client.post(
"https://www.tripadvisor.com/data/graphql/ids",
json=search_payload,
headers=graphql_headers
)
if response.status_code == 200:
results = response.json()
print(json.dumps(results, indent=2)[:1000])
else:
print(f"GraphQL 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}")
for r in reviews[:3]:
print(f" [{r['rating']}/5] {r['title']} - {r['createdDate']}")
重要な注意: preRegisteredQueryId の値(たとえば検索用の 84b17ed122fbdbd4、レビュー用の ef1a9f94012220d3)は、TripAdvisor が再デプロイすると壊れることがあります。その場合、リクエストは静かに失敗します。ブラウザの DevTools でネットワークリクエストを見て、クエリID を再発見する必要があります。
この方法でプロキシの必要性が下がる理由
仕組みは単純です。requests+BS4 では、ホテル詳細ページ100件を取るのに100リクエスト必要です。非公開 JSON の方法では、各リクエストで1ページに必要なデータをまとめて取れます。レビューの展開や動的コンテンツ読み込みの追加リクエストも不要です。GraphQL なら、1回の API 呼び出しで20件のレビューを返せます。リクエスト数が少ないほど、レート制限に触れる機会も減り、プロキシローテーションの必要性も下がります。小〜中規模のプロジェクト(1,000ページ未満)なら、適切な待機を入れればプロキシなしでも回せる場合があります。
1つの再利用可能なスクリプトでホテル・レストラン・観光スポットを取得する
競合記事の5本中4本はホテルしか扱いません。でも TripAdvisor には主要カテゴリが3つあり、URL パターンもデータ項目も少しずつ違います。そこで、3カテゴリすべてに対応する関数を作ってみましょう。
カテゴリごとの取得可能な項目
| 項目 | ホテル | レストラン | 観光スポット |
|---|---|---|---|
| 名称 | ✅ | ✅ | ✅ |
| 評価 | ✅ | ✅ | ✅ |
| レビュー件数 | ✅ | ✅ | ✅ |
| 料金/価格帯 | ✅ | ✅ | 場合による |
| 住所 | ✅ | ✅ | ✅ |
| 料理ジャンル | ❌ | ✅ | ❌ |
| 所要時間/ツアー種別 | ❌ | ❌ | ✅ |
| 設備 | ✅ | ❌ | ❌ |
| 座標 | ✅ | ✅ | ✅ |
再利用可能な scrape_tripadvisor() 関数を作る
import requests
from bs4 import BeautifulSoup
import pandas as pd
import time
import random
import re
import json
def scrape_tripadvisor(category, location_id, location_name, num_pages=3):
"""
TripAdvisor の一覧をホテル・レストラン・観光スポット横断でスクレイピングする。
Args:
category: "hotels", "restaurants", or "attractions"
location_id: TripAdvisor の geo ID(例: パリは "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 + 1}: ステータス {response.status_code} のため停止します。")
break
soup = BeautifulSoup(response.text, "html.parser")
cards = soup.select('div[data-test-attribute="location-results-card"]')
for card in cards:
item = {"category": category}
title_el = card.select_one('div[data-automation="hotel-card-title"]') or card.select_one('a[data-automation]')
item["name"] = title_el.get_text(strip=True) if title_el else None
rating_el = card.select_one('[data-automation="bubbleRatingValue"]')
item["rating"] = rating_el.get_text(strip=True) if rating_el else None
review_el = card.select_one('[data-automation="bubbleReviewCount"]')
item["review_count"] = review_el.get_text(strip=True) if review_el else None
all_items.append(item)
print(f" ページ {page + 1}: {len(cards)} 件取得")
time.sleep(random.uniform(3, 7))
return pd.DataFrame(all_items)
# 使用例
print("=== パリのホテル ===")
hotels_df = scrape_tripadvisor("hotels", "187147", "Paris_Ile_de_France", num_pages=2)
print(hotels_df.head())
print("\n=== ローマのレストラン ===")
restaurants_df = scrape_tripadvisor("restaurants", "187791", "Rome_Lazio", num_pages=2)
print(restaurants_df.head())
print("\n=== バルセロナの観光スポット ===")
attractions_df = scrape_tripadvisor("attractions", "187497", "Barcelona_Catalonia", num_pages=2)
print(attractions_df.head())
関数は1つ、カテゴリは3つ、コードの重複はゼロです。TripAdvisor がセレクタを変えてきても、修正は1か所で済みます。
TripAdvisorにブロックされたときの対処法(対ボットのトラブルシューティング)
これは私が TripAdvisor をスクレイピングし始めたときに一番欲しかった章です。でも、こうして整理してくれる競合記事はほとんどありません。TripAdvisor は DataDome(1日あたり5兆以上のデータポイントを解析)と Cloudflare WAF を併用しています。よくある失敗パターンを診断表にまとめました。
| 症状 | 主な原因 | 対処法 |
|---|---|---|
| HTTP 403 応答 | ヘッダー不足、または不自然なヘッダー。Cloudflare の JS チャレンジ | 現実的な User-Agent、Accept-Language、Referer、Sec-CH-UA を設定し、整合性を保つ |
| データの代わりに CAPTCHA ページが出る | レート制限またはブラウザフィンガープリント検知 | 住宅系プロキシをローテーションし、リクエスト間にランダム遅延(2〜7秒)を入れる |
| HTML が空、または本文が空白 | requests では JavaScript が描画されない | Selenium に切り替えるか、ページソース内の非公開 JSON を取り出す |
| レビューが途中までしか取れない / 「続きを読む」が展開されない | クリックイベント後に読み込まれるコンテンツ | Selenium の .click() を使うか、埋め込み JSON から抽出する |
| レビューが1言語しかない | 言語パラメータ不足 | レビューURLに ?filterLang=ALL を追加する |
| Nページ以降で読み込みが止まる | セッション単位のレート制限 | セッションを切り替え、バッチ間で Cookie をクリアする |
| HTTP 1020 Access Denied | Cloudflare による IP/ASN ブロック | データセンター系から住宅系プロキシに切り替える |
| チャレンジループ(無限 CAPTCHA) | Cookie の保持が壊れている | 最初にホームページを開いてセッションを温める。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" レート制限 (429)。{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" {response.status_code} を受信。{attempt + 1}/{max_retries} 回目の再試行を {wait:.1f}s 後に行います...")
time.sleep(wait)
continue
# その他のエラーコードは再試行しない
print(f" {url} で予期しないステータス {response.status_code}")
return response
except requests.exceptions.Timeout:
wait = min(base_delay * (2 ** attempt) + random.uniform(0, 1), max_delay)
print(f" タイムアウト。{attempt + 1}/{max_retries} 回目の再試行を {wait:.1f}s 後に行います...")
time.sleep(wait)
print(f" {url} に対する {max_retries} 回の再試行がすべて失敗しました")
return None
ヘッダー、プロキシ、セッションのローテーション
継続的に取るなら、ヘッダーのセットを複数用意してまとめてローテーションしましょう。
import random
HEADER_SETS = [
{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
"Sec-CH-UA": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
"Sec-CH-UA-Platform": '"Windows"',
},
{
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
"Sec-CH-UA": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
"Sec-CH-UA-Platform": '"macOS"',
},
{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
"Sec-CH-UA": '"Google Chrome";v="134", "Not-A.Brand";v="8", "Chromium";v="134"',
"Sec-CH-UA-Platform": '"Windows"',
},
]
PROXY_LIST = [
"http://user:pass@residential-proxy-1:port",
"http://user:pass@residential-proxy-2:port",
# 住宅系プロキシを追加
]
def get_rotated_session():
"""ローテーション済みのヘッダーとプロキシで新しいセッションを作る"""
session = requests.Session()
# ランダムなヘッダーセットを選ぶ
header_set = random.choice(HEADER_SETS)
base_headers = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Referer": "https://www.tripadvisor.com/",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-CH-UA-Mobile": "?0",
}
base_headers.update(header_set)
session.headers.update(base_headers)
# ランダムなプロキシを選ぶ
if PROXY_LIST:
proxy = random.choice(PROXY_LIST)
session.proxies = {"http": proxy, "https": proxy}
return session
プロキシの種類は重要です。 データセンター系プロキシは TripAdvisor にほぼ即ブロックされます(HTTP 1020 Access Denied)。継続的な取得には住宅系プロキシが必須です。一般ユーザーの回線経由に見えるため、実ユーザーと区別されにくいからです。料金はプロバイダによっておおむね 1GB あたり 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"{len(df)} 行を CSV に出力しました")
encoding='utf-8-sig' は重要です。Excel で CSV を開いたときに、フランス語のアクセント記号や中国語などの非ラテン文字を正しく表示するためです。
JSON 出力(ネストしたデータ向け)
レビューがホテルの下に入れ子になっている場合、JSON なら階層を保てます。
# 階層構造
hotel_data = {
"property_id": "d194317",
"name": "NH City Centre Amsterdam",
"rating": 4.0,
"reviews": [
{"title": "Great location", "rating": 5, "date": "2025-03-15", "text": "..."},
{"title": "Average stay", "rating": 3, "date": "2025-03-10", "text": "..."},
]
}
# フラットな分析には json_normalize を使う
flat_reviews = pd.json_normalize(
hotel_data,
record_path="reviews",
meta=["property_id", "name"]
)
flat_reviews.to_csv("reviews_flat.csv", index=False)
リレーショナルデータには2ファイル構成
大きなデータセットでは、私は2つの CSV を使います。
hotels.csv— 施設ごとに1行(フラット)reviews.csv— レビューごとに1行、property_idを外部キーにする
これなら pandas で結合しやすく、データベースに入れるのも、BI ツールに読み込むのも簡単です。
こういう出力処理を一切考えたくないなら、Thunderbit なら 取得したデータを直接エクスポート できます。Excel、Google Sheets、Airtable、Notion に、すべて無料・コード不要で出力可能です。非技術系のチームメンバーと共有したいときにも便利です。
責任ある効率的なTripAdvisorスクレイピングのコツ
責任あるスクレイピングの要点を6つにまとめます。
robots.txtを確認する: TripAdvisor の robots.txt は、GPTBot や ClaudeBot などの AI 学習ボットを全面的にブロックしています。通常のクローラには一部パス制限があります。tripadvisor.com/robots.txtで確認しましょう。- 待機時間を入れる: リクエスト間は3〜7秒が安全圏です。毎分10〜15リクエストを超えるとレート制限にかかりやすくなります。
- 公開データだけを取得する。 ログインして制限コンテンツに入るのは避けましょう。
- データは安全に保管し、 レビュー投稿者名など個人情報を扱う場合は GDPR/CCPA に従いましょう。
- 商用規模のデータが必要なら公式 API も検討する。 Developer Portal では、施設詳細に加えて、各ロケーションにつき最大5件のレビューと5枚の写真にアクセスできます。制限はありますが、合法で安定しています。
- 法的な背景にも注意する: EU Court の Ryanair 判決(2025年12月) により、EU 内で ToS ベースのスクレイピング禁止がより強化されました。TripAdvisor の利用規約でもスクレイピングは禁止されています。責任を持って、自己責任で行ってください。
まとめ
全体像はこれで揃いました。
- Requests + BeautifulSoup がいちばんシンプルです。静的な一覧ページに向いていて、準備も少なく、速いです。100ページ未満で、JavaScript 描画済みのコンテンツが不要なら、まずここから始めるのがよいでしょう。
- Selenium は、requests では対応できない動的コンテンツ、「続きを読む」ボタン、Cookie バナーなどを扱えます。速度は5倍遅く、リソース消費も大きいですが、ページと対話する必要があるならこれしかありません。
- 非公開 JSON / GraphQL は最もきれいで速い方法です。HTML を解析せずに構造化データを取れ、リクエスト数を減らせるのでプロキシも必要になりにくく、分析しやすい形式で返してくれます。ただし、初期の逆解析コストがあり、TripAdvisor がデータ構造を変えたときには保守が必要です。
再利用可能な scrape_tripadvisor() 関数を使えば、ホテル、レストラン、観光スポットに対応できます。これ以上のチュートリアルは不要なはずです。
途中で「やっぱりコーディングは向いてないかも」と思ったり、今日中にホテル50件をスプレッドシートへ入れたいだけなら、Thunderbit の Chrome 拡張機能 ならAIによる項目自動検出、ページネーション自動処理、Excel や Google Sheets への無料エクスポートを2クリックで実現できます。Python は不要です。
さらに深く学びたい方は、Thunderbit blog と YouTube チャンネル に、ほかのスクレイピング解説もあります。
FAQ
1. TripAdvisorをスクレイピングするのは合法ですか?
TripAdvisor の利用規約では、スクレイピングは禁止されています。ただし米国では、公開されているデータ(ログイン不要のデータ)を取得する行為は、一般に Computer Fraud and Abuse Act に違反しないと判断されることが多いです。とはいえ、2025年の EU Court の Ryanair 判決により、欧州では ToS ベースの制限が強化されました。公開データのみに絞り、robots.txt を尊重し、著作物を再公開せず、商用利用なら法務の確認をおすすめします。
2. PythonなしでもTripAdvisorをスクレイピングできますか?
はい。Thunderbit のようなノーコードツールなら、AI による項目検出とページネーション自動処理で、ブラウザから直接 TripAdvisor を取得できます。ブラウザ拡張、Google Sheets アドオン、商用スクレイピング API も選択肢です。Python は最も自由度が高いですが、唯一の方法ではありません。
3. TripAdvisorのスクレイピングでブロックされないにはどうすればいいですか?
重要なのは、現実的で一貫性のあるヘッダー(特に User-Agent と Sec-CH-UA)を使うこと、住宅系プロキシをローテーションすること(データセンター IP はすぐブロックされる)、リクエスト間に3〜7秒のランダム遅延を入れること、非公開 JSON を使って総リクエスト数を減らすこと、指数バックオフ付きの再試行を実装すること、そして深いページを取る前にホームページを開いてセッションを温めることです。
4. TripAdvisorからどんなデータを取得できますか?
ホテル、レストラン、観光スポットの名前、評価、レビュー件数、価格帯、住所、座標、設備(ホテル)、料理ジャンル(レストラン)、ツアー所要時間(観光スポット)、個別評価や日付付きのレビュー全文などが取得可能です。非公開 JSON と GraphQL の方法が、1リクエストあたり最も豊富なデータを返します。
5. TripAdvisorから1日に何ページくらい取得できますか?
1つの IP で、適切な待機を入れる場合は、1日あたり約600〜1,000ページが目安です。20本の住宅系プロキシを回すなら、requests ベースの方法で約20万〜30万ページ/日まで見込めます。Selenium は遅いので、プロキシ1本あたり約8,000〜12,000ページ/日を想定してください。非公開 JSON / GraphQL は1リクエストあたりの情報量が多いため、同じ情報量を得るのに必要な総ページ数はかなり少なくなります。
さらに読む


