지난주에 TripAdvisor에서 유럽 3개 도시의 호텔 약 200곳, 그 평점과 리뷰 수를 긁어 보려 했어요. 첫 스크립트는 기본 requests.get()에 기본 헤더만 얹은, 더없이 단순한 형태였죠. 돌아온 건 매 요청마다 정갈하게 찍히는 403 Forbidden뿐. 쓸 만한 데이터는 한 바이트도 못 건졌어요.
TripAdvisor는 여행 업계에서 가장 풍성한 공개 데이터 소스 중 하나예요. 리뷰 10억 개 이상, 비즈니스 등록 800만 개 이상, 월간 고유 방문자 약 4억 6천만 명을 거느리고, 연간 여행 지출에도 600억 달러(약 83조 원) 이상 영향을 줘요. 그런데 이 데이터를 프로그램으로 가져오려는 순간 이야기가 확 달라져요. DataDome 봇 탐지, Cloudflare WAF, TLS 지문 분석, JavaScript 챌린지가 겹겹이 깔려 있어서, 단순한 시도는 출발선도 못 넘고 막혀버리거든요. 이 가이드는 제가 그때 절실히 원했던 자료예요. Python 스크래핑 3가지 방식에 코드 없는 대안까지 정면으로 비교하고, 각 방식의 전체 코드, 안티봇 문제 해결 섹션, 호텔·레스토랑·관광지에 두루 재사용할 패턴까지 담았어요. Python 초보든 숙련 개발자든, 403 에러만 반복해서 보는 시간을 크게 줄여줄 거예요.
코드 작성이 부담스럽다면? TripAdvisor를 더 쉽게 스크래핑하는 방법
먼저 분명히 해둘게요. "Python으로 TripAdvisor 스크래핑"을 검색하는 분들 상당수는 코드 자체가 목적이 아니에요. 호텔 이름, 평점, 리뷰 수, 가격을 얼른 스프레드시트로 옮기고 싶을 뿐이죠. 그렇다면 훨씬 가까운 길이 있어요.
Thunderbit은 저희가 만든 AI 기반 Chrome 확장 프로그램으로, TripAdvisor 페이지를 읽어 들인 뒤 추출할 컬럼을 알아서 제안해줘요. 흐름은 딱 두 단계예요.
- TripAdvisor 목록 페이지를 엽니다 (예: "파리 호텔" 검색 결과).
- Thunderbit 사이드바에서 "AI Suggest Fields"를 클릭해요. AI가 페이지를 스캔해 Hotel Name, Rating, Review Count, Price, Location 같은 컬럼을 제안해요.
- "Scrape"를 클릭해요. Thunderbit이 페이지의 모든 목록에서 데이터를 뽑고, 필요하면 페이지네이션도 알아서 처리해요.
- Excel, Google Sheets, Airtable, Notion으로 내보내요. 모든 요금제에서 내보내기는 무료예요.
Thunderbit은 별도 설정 없이 호텔, 레스토랑, 관광지에 두루 통해요. AI가 페이지 구조에 맞춰 스스로 적응하거든요. 여러 페이지로 이어진 결과면 "Next" 버튼이나 무한 스크롤도 자동으로 감지하고요. 게다가 실제 Chrome 브라우저 안에서 돌기 때문에 세션 쿠키와 브라우저 지문을 그대로 살려서, 봇 탐지에 자연스러운 이점도 있어요.
Thunderbit Chrome Extension에서 바로 써볼 수 있어요. 무료 요금제는 월 6페이지까지라 워크플로를 가늠하기엔 충분해요.
프로그래밍 제어가 필요하거나, 맞춤 파싱 로직이 필요하거나, 1만 페이지 이상을 긁을 계획이라면 Python이 답이에요. 계속 읽어보세요.
왜 TripAdvisor를 Python으로 스크래핑해야 할까요?
TripAdvisor 데이터는 비즈니스에 직접적이고 측정 가능한 영향을 줘요. 코넬 대학교 연구를 보면, 호텔의 100점 만점 Global Review Index가 1점 오를 때 평균 일일 요금은 0.89% 오르고, 객실당 매출(Revenue Per Available Room)은 1.42% 늘어요. 또 다른 ScienceDirect 연구는 TripAdvisor 평점이 외부 요인으로 1점 오르면 평균 호텔의 연간 매출이 5만 5천~7만 5천 달러 늘어난다고 봤죠. 리뷰는 보여주기용 지표가 아니라 실제 수익을 만드는 요소예요.
팀별로 TripAdvisor 데이터를 어떻게 쓰는지 정리한 표예요.
| 활용 사례 | 도움이 되는 대상 | 필요한 데이터 |
|---|---|---|
| 호텔 경쟁사 분석 | 호텔 체인, 수익 관리자 | 평점, 가격, 리뷰 수, 편의시설 |
| 레스토랑 시장 조사 | 레스토랑 그룹, 식음료 브랜드 | 요리 유형, 가격대, 리뷰 감성 |
| 관광지 트렌드 추적 | 여행사, 관광청 | 인기 순위, 계절별 패턴 |
| 감성 분석 | 연구자, 데이터 분석가 | 전체 리뷰 텍스트, 별점, 날짜 |
| 리드 발굴 | 영업팀, 여행사 | 비즈니스명, 연락처, 위치 |
그럼 왜 하필 Python일까요? 이유는 셋이에요. 첫째, 생태계가 막강해요. BeautifulSoup, Selenium, Playwright, Scrapy, httpx, pandas까지, Python만큼 스크래핑·데이터 분석 라이브러리가 성숙한 언어는 드물어요. 둘째, 웹 스크래핑 개발자의 71.7%가 Python을 써요. 커뮤니티 지원이 두텁고, StackOverflow 답변도 풍부하며, 최신 가이드도 쉽게 찾아요. 셋째, 파이프라인이 한 언어로 끝나요. BeautifulSoup으로 긁고, pandas로 정제하고, Hugging Face Transformers로 감성 분석하고, 대시보드까지 같은 언어로 처리하니 문맥 전환이 없어요.
Python으로 TripAdvisor를 스크래핑하는 3가지 방법 비교
경쟁 가이드는 대개 한 방식만 골라 밀어붙여요. 코드를 쓰기 전에 어떤 방법을 고를지 판단해야 하는 입장에선 별 도움이 안 되죠. 제가 그때 누군가에게 받고 싶었던 비교표는 이래요.
| 방식 | 속도 | JS 지원 | 안티봇 내성 | 난이도 | 적합한 경우 |
|---|---|---|---|---|---|
requests + BeautifulSoup | ⚡ 빠름(원시 기준 약 120~200페이지/분) | ❌ 없음 | ⚠️ 낮음 | 쉬움 | 정적 목록 페이지, 소규모 프로젝트 |
| Selenium / 헤드리스 브라우저 | 🐢 느림(약 8~20페이지/분) | ✅ 완전 지원 | ⚠️ 중간 | 보통 | 동적 콘텐츠, “Read more” 클릭, 쿠키 배너 |
| 숨겨진 JSON / GraphQL API | ⚡⚡ 가장 빠름(원시 기준 약 200~600페이지/분) | N/A | ✅ 높음 | 어려움 | 대규모 리뷰/호텔 추출 |
| 노코드(Thunderbit) | ⚡ 빠름 | ✅ 내장 | ✅ 내장 | 가장 쉬움 | 비개발자, 빠른 일회성 내보내기 |
짚어둘 게 있어요. 위 속도는 이론값이에요. TripAdvisor 속도 제한이 대략 IP당 분당 10~15요청이라, 실제 처리량은 방식과 무관하게 IP당 분당 약 10페이지로 묶여요. 숨겨진 JSON은 요청 1회당 데이터를 가장 많이 가져와 총 요청 수를 줄이고, 그만큼 속도 제한에 덜 걸리죠. Selenium은 실측에서 요청 기반보다 약 5배 느리지만, 버튼 클릭이나 JavaScript 렌더링이 필요할 땐 사실상 유일한 선택지예요.
이제 3가지 방법을 각각 완전한 코드와 함께 볼게요. 상황에 맞게 골라도, 섞어도 돼요. 저는 목록 페이지는 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은 카테고리별로 예측 가능한 패턴을 따라요.
- 호텔:
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 브라우저처럼 보이게 만들어야 해요.
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 속성은 훨씬 안정적이에요. 항상 클래스명보다 데이터 속성을 먼저 쓰세요.
페이지네이션 처리하기
여러 페이지를 긁으려면 요청 사이에 적절한 지연을 두면서 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}: 상태 코드 {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 {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가 필요해요)
- 리뷰가 "Read more" 버튼 뒤에 잘려 있을 때
- 안티봇 장치가 JavaScript 챌린지나 CAPTCHA로 단계적으로 세질 때
- 가격, 재고 여부처럼 클라이언트 측 렌더링 후에야 나타나는 데이터가 필요할 때
이럴 땐 Selenium(방법 2)이나 숨겨진 JSON 방식(방법 3)이 필요해요.
방법 2: Selenium으로 TripAdvisor 스크래핑하기(헤드리스 브라우저)
Selenium은 진짜 브라우저를 띄우니까 JavaScript를 렌더링하고, 버튼을 클릭하고, 쿠키 동의 배너를 처리하고, 동적 콘텐츠와 상호작용할 수 있어요. 대신 속도는 약 5배 느리고, 브라우저 인스턴스당 RAM 300~500MB를 써요.
안티탐지 설정과 함께 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") # 새로운 헤드리스 모드 사용(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"]')))
# 쿠키 동의 팝업 처리(나타나는 경우)
try:
cookie_btn = driver.find_element(By.ID, "onetrust-accept-btn-handler")
cookie_btn.click()
time.sleep(1)
except:
pass # 쿠키 팝업 없음
# 호텔 데이터 추출
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)
이 코드는 제 환경에서 한 페이지를 가져오는 데 약 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")
# 클래스에 "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"{len(reviews)}개의 리뷰를 스크래핑했습니다")
Selenium에 프록시 회전 추가하기
지속적으로 긁으려면 프록시 회전이 필요해요. selenium-wire는 2024년 1월부터 deprecated 상태이니, Chrome 내장 프록시 지원을 쓰세요.
# 인증이 필요 없는 프록시 사용 시
proxy = "http://your-proxy-address:port"
options.add_argument(f"--proxy-server={proxy}")
# 인증이 필요한 프록시는 Chrome 확장 프로그램 또는 Selenium 4의 BiDi 프로토콜 사용
프로그래밍적으로 프록시를 회전하려면 요청 배치마다 다른 프록시로 새 driver 인스턴스를 만들면 돼요. 세련되진 않아도 안정적이에요.
방법 3: 숨겨진 JSON 방식(HTML 파싱을 아예 건너뛰기)
많은 가이드가 이 방법을 통째로 건너뛰어요. 사실 셋 중 가장 빠르고 깔끔한데 말이죠. TripAdvisor는 HTML 페이지 안에 구조화된 데이터를 JSON으로 직접 박아둬요. <script> 태그 속 pageManifest, urqlCache 같은 JavaScript 변수 안에요. 이 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.8,*/*;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"키 목록: {list(page_data.keys())[:10]}")
변수명을 직접 찾는 방법: Chrome에서 TripAdvisor 호텔 페이지를 연 뒤, 우클릭 → 페이지 소스 보기 → pageManifest, urqlCache, aggregateRating를 Ctrl+F로 검색해 보세요. 데이터가 그 안에 숨어 있어요.
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))
# 캐시를 탐색해 리뷰 데이터 찾기
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 요청 실패: {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은 페이지 한 번 로드에 데이터가 한꺼번에 나오니, 리뷰 펼치기나 동적 콘텐츠용 추가 요청이 줄죠. GraphQL은 한 번의 호출로 리뷰 20개를 가져오기도 하고요. 요청이 적을수록 속도 제한에 덜 걸리고 프록시 회전도 덜 필요해요. 1,000페이지 미만 프로젝트라면 적절한 지연만으로 프록시 없이 갈 수도 있어요.
하나의 재사용 가능한 스크립트로 호텔·레스토랑·관광지 모두 스크래핑하기
경쟁 가이드 5개 중 4개는 호텔만 다뤄요. 하지만 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 목록을 스크래핑합니다.
Args:
category: "hotels", "restaurants", 또는 "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 {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 {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())
함수 하나로 세 카테고리를 다 처리하니 중복 코드가 없어요. TripAdvisor가 선택자를 바꿔도 한 곳만 고치면 돼요.
TripAdvisor가 막아설 때 해야 할 일(안티봇 문제 해결)
제가 TripAdvisor를 처음 긁을 때 가장 절실했던 부분이 바로 여기예요. 그런데 경쟁 가이드 중 구조적으로 정리해주는 곳은 거의 없죠. TripAdvisor는 DataDome(하루 5조 개 이상의 데이터 포인트 분석)와 Cloudflare WAF를 함께 써요. 가장 흔한 실패 모드의 진단표는 이래요.
| 증상 | 가능한 원인 | 해결 방법 |
|---|---|---|
| HTTP 403 응답 | 헤더 누락 또는 수상한 헤더, Cloudflare JS 챌린지 | 현실적인 User-Agent, Accept-Language, Referer, Sec-CH-UA 헤더 사용. 헤더 일관성 유지. |
| 데이터 대신 CAPTCHA 페이지 표시 | 속도 제한 또는 브라우저 지문 탐지 | 리지덴셜 프록시 회전, 랜덤 지연 추가(요청 사이 2~7초) |
| HTML이 비어 있거나 본문이 없는 페이지 | requests가 JavaScript를 렌더링하지 않음 | Selenium으로 전환하거나 페이지 소스에서 숨겨진 JSON 추출 |
| 리뷰가 일부만 보이거나 “Read more”가 펼쳐지지 않음 | 클릭 이벤트에서 로딩되는 콘텐츠 | Selenium .click() 사용 또는 임베디드 JSON 블록에서 추출 |
| 리뷰가 한 언어로만 표시됨 | 언어 파라미터 누락 | 리뷰 URL에 ?filterLang=ALL 추가 |
| N페이지 이후 데이터 로딩 중단 | 세션 기반 속도 제한 | 세션 회전, 배치 사이 쿠키 초기화 |
| HTTP 1020 Access Denied | Cloudflare가 IP/ASN 차단 | 데이터센터 프록시 대신 리지덴셜 프록시 사용 |
| 챌린지 루프(CAPTCHA 무한 반복) | 쿠키 유지 실패 | 먼저 홈페이지를 방문해 세션 예열, 쿠키 잔존 유지 |
지수 백오프가 있는 재시도 로직
경쟁 글은 정작 이 코드를 안 보여주는 경우가 많아요. 재사용 가능한 재시도 함수는 이래요.
import time
import random
import requests
def fetch_with_retry(session, url, max_retries=4, base_delay=2, max_delay=60):
"""
지수 백오프와 jitter를 사용해 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}초 대기 중...")
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}초 후 다시 시도...")
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}초 후 다시 시도...")
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). 지속적인 스크래핑엔 리지덴셜 프록시가 사실상 필수죠. 일반 사용자 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"CSV로 {len(df)}행을 내보냈습니다")
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)
관계형 데이터를 위한 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초는 안전한 범위예요. IP당 분당 1015요청보다 빠르면 속도 제한에 걸릴 수 있어요. - 공개 데이터만 스크래핑하세요. 로그인해야 보이는 콘텐츠엔 접근하지 마세요.
- 데이터를 안전하게 저장하고, 리뷰어 이름 같은 개인 정보를 다룬다면 개인정보보호법(GDPR/CCPA)을 지키세요.
- 상업적 규모의 데이터가 필요하면 TripAdvisor 공식 API를 고려하세요. Developer Portal은 비즈니스 정보와 위치당 리뷰 최대 5개, 사진 5개까지 줘요. 제한적이지만 합법적이고 안정적이죠.
- 법적 맥락을 인지하세요: 2025년 12월 EU 법원 Ryanair 판결은 EU 전역에서 이용약관 기반 스크래핑 금지를 더 강하게 만들었어요. TripAdvisor 서비스 약관은 스크래핑을 명시적으로 금지하고요. 책임감 있게, 본인 위험 하에 스크래핑하세요.
마무리
이제 전체 그림이 잡혔을 거예요.
- requests + BeautifulSoup은 가장 단순한 길이에요. 정적 목록 페이지에 잘 맞고, 설정이 거의 없으며, 빠르죠. 100페이지 미만을 긁고 JavaScript 렌더링 데이터가 필요 없다면 여기서 시작하세요.
- Selenium은 requests가 못 하는 일을 해내요. 동적 콘텐츠, "Read more" 버튼, 쿠키 배너가 대표적이죠. 속도는 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년 EU 법원 Ryanair 판결은 유럽에서 약관 기반 제한을 더 강하게 만들었죠. 공개 데이터만 긁고, 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 페이지를 몇 개까지 스크래핑할 수 있나요?
단일 IP에 적절한 지연을 쓰면 하루 약 6001,000페이지가 현실적이에요. 20개의 회전형 리지덴셜 프록시를 쓰면 요청 기반 방식으로 하루 약 20만30만 페이지까지도 가능하고요. Selenium은 더 느려서 프록시당 하루 8,000~12,000페이지 정도를 잡으면 돼요. 숨겨진 JSON/GraphQL 방식은 요청당 데이터가 가장 많아서, 같은 정보를 얻는 데 드는 총 페이지 수가 훨씬 적을 수 있어요.
더 알아보기


