สัปดาห์ที่แล้วผมลองดึงข้อมูลเรตติ้งโรงแรมและจำนวนรีวิวของที่พักประมาณ 200 แห่งใน 3 เมืองใหญ่ของยุโรปจาก TripAdvisor สคริปต์ตัวแรกของผม — ใช้แค่ requests.get() กับ header ค่าเริ่มต้น — ได้กลับมาเป็น 403 Forbidden แบบสวยๆ ทุกคำขอ ไม่มีข้อมูลที่ใช้ได้แม้แต่ไบต์เดียว
TripAdvisor เป็นหนึ่งในแหล่งข้อมูลสาธารณะที่มีค่ามากที่สุดในอุตสาหกรรมท่องเที่ยว: มีรีวิวมากกว่า 1 พันล้านรีวิว, รายชื่อธุรกิจมากกว่า 8 ล้านรายการ และมีผู้เข้าชมไม่ซ้ำต่อเดือนราว 460 ล้านคน แถมยังมีอิทธิพลต่อยอดใช้จ่ายด้านการท่องเที่ยวต่อปีมากกว่า $60 พันล้าน แต่ถ้าจะดึงข้อมูลพวกนี้แบบโปรแกรมมิ่งล่ะ? ตรงนี้แหละที่เริ่มยาก TripAdvisor ใช้ทั้ง DataDome bot detection, Cloudflare WAF, TLS fingerprinting และ JavaScript challenge เป็นเกราะป้องกันหลายชั้นที่หยุดการสแครปแบบง่ายๆ ตั้งแต่ยังไม่ทันเริ่ม คู่มือนี้คือแหล่งข้อมูลเดียวที่ผมอยากให้มีตั้งแต่แรก: เปรียบเทียบ 3 แนวทางสแครปด้วย Python แบบตัวต่อตัว (พร้อมตัวเลือกแบบไม่ต้องเขียนโค้ด), โค้ดครบทุกวิธี, ส่วนแก้ปัญหา anti-bot แบบเป็นระบบ และแพตเทิร์นที่เอาไปใช้ซ้ำได้กับทั้งโรงแรม ร้านอาหาร และสถานที่ท่องเที่ยว ไม่ว่าคุณจะเพิ่งเริ่มใช้ Python หรือเป็นนักพัฒนาที่มีประสบการณ์ คู่มือนี้จะช่วยประหยัดเวลาที่เสียไปกับ 403 ได้เยอะมาก
ไม่อยากเขียนโค้ด? ดึงข้อมูล TripAdvisor แบบง่ายที่สุด
ขอพูดตรงๆ ไว้ก่อน หลายคนที่ค้นคำว่า "scrape TripAdvisor with Python" จริงๆ แล้วไม่ได้อยากเขียนโค้ดขนาดนั้น พวกเขาแค่อยากได้ข้อมูล — ชื่อโรงแรม เรตติ้ง จำนวนรีวิว ราคา — ลงสเปรดชีตให้เร็วที่สุด ถ้าคุณเป็นแบบนั้น มีทางที่สั้นกว่ามาก
Thunderbit คือส่วนขยาย Chrome ที่ทำงานด้วย AI ซึ่งเราสร้างขึ้นมาให้สามารถอ่านหน้า TripAdvisor ได้ทุกหน้า และแนะนำคอลัมน์ที่ควรดึงออกมาโดยอัตโนมัติ เวิร์กโฟลว์นี้ง่ายจริงๆ แค่สองคลิก:
- เปิดหน้ารายการของ TripAdvisor (เช่น หน้าผลการค้นหา “Hotels in Paris”)
- คลิก “AI Suggest Fields” ในแถบด้านข้างของ Thunderbit AI จะสแกนหน้าและเสนอคอลัมน์อย่าง Hotel Name, Rating, Review Count, Price และ Location
- คลิก “Scrape.” Thunderbit จะดึงข้อมูลจากทุกรายการในหน้า — และจัดการ pagination ให้โดยอัตโนมัติถ้าคุณต้องการผลลัพธ์เพิ่ม
- Export ไปยัง Excel, Google Sheets, Airtable หรือ Notion การส่งออกฟรีในทุกแพ็กเกจ
Thunderbit ใช้งานได้กับโรงแรม ร้านอาหาร และสถานที่ท่องเที่ยวโดยไม่ต้องปรับค่าอะไร AI จะปรับตัวตามเนื้อหาบนหน้าเอง สำหรับผลลัพธ์หลายหน้า มันตรวจจับปุ่ม “Next” และ infinite scroll ให้อัตโนมัติ และเพราะมันรันอยู่ใน Chrome จริงๆ ของคุณ จึงใช้คุกกี้เซสชันและ browser fingerprint ของคุณ ทำให้ได้เปรียบตามธรรมชาติในการรับมือ bot detection
คุณลองได้ด้วย Thunderbit Chrome Extension — แพ็กเกจฟรีให้ 6 หน้า/เดือน เพียงพอสำหรับทดสอบเวิร์กโฟลว์
ถ้าคุณต้องการการควบคุมแบบโปรแกรมมิ่ง, ต้องการ logic การแยกข้อมูลที่ปรับแต่งเอง หรือวางแผนจะดึงมากกว่า 10,000 หน้า Python คือคำตอบ อ่านต่อได้เลย
ทำไมต้องใช้ Python ดึงข้อมูลจาก TripAdvisor?
ข้อมูล TripAdvisor ส่งผลต่อธุรกิจได้โดยตรงและวัดผลได้จริง งานวิจัยจาก Cornell University พบว่า เมื่อ Global Review Index ของโรงแรมเพิ่มขึ้น 1 คะแนนจาก 100 คะแนน อัตราค่าห้องเฉลี่ยต่อวันจะเพิ่มขึ้น 0.89% และ Revenue Per Available Room จะเพิ่มขึ้น 1.42% ขณะที่งานวิจัยอีกชิ้นจาก ScienceDirect ระบุว่าเมื่อเรตติ้งใน TripAdvisor เพิ่มขึ้น 1 ดาวโดยปัจจัยภายนอก รายได้ต่อปีของโรงแรมทั่วไปจะเพิ่มขึ้นราว $55,000–$75,000 รีวิวจึงไม่ใช่แค่ตัวเลขสวยๆ แต่เป็นตัวขับรายได้จริง
ทีมต่างๆ ใช้ข้อมูล TripAdvisor กันแบบนี้:
| กรณีใช้งาน | ใครได้ประโยชน์ | ต้องใช้ข้อมูลอะไร |
|---|---|---|
| วิเคราะห์คู่แข่งโรงแรม | เชนโรงแรม, revenue manager | เรตติ้ง, ราคา, ปริมาณรีวิว, สิ่งอำนวยความสะดวก |
| วิจัยตลาดร้านอาหาร | กลุ่มร้านอาหาร, แบรนด์อาหาร | ประเภทอาหาร, ช่วงราคา, sentiment ของรีวิว |
| ติดตามเทรนด์สถานที่ท่องเที่ยว | ผู้ประกอบการทัวร์, หน่วยงานท่องเที่ยว | อันดับความนิยม, รูปแบบตามฤดูกาล |
| วิเคราะห์ sentiment | นักวิจัย, data analyst | ข้อความรีวิวเต็ม, ดาวเรตติ้ง, วันที่ |
| สร้างลีด | ทีมขาย, เอเจนซี่ท่องเที่ยว | ชื่อธุรกิจ, ข้อมูลติดต่อ, ที่ตั้ง |
แล้วทำไมต้องเป็น Python โดยเฉพาะ? มี 3 เหตุผล หนึ่ง คือ ecosystem ที่แข็งแรง: BeautifulSoup, Selenium, Playwright, Scrapy, httpx, pandas — Python มีไลบรารีด้าน scraping และ data analysis ที่โตเต็มที่มากกว่าภาษาอื่น สองคือ 71.7% ของนักพัฒนาเว็บสแครปปิง ใช้ Python ซึ่งแปลว่ามี community support เยอะ, คำตอบใน StackOverflow เยอะ และคู่มือที่อัปเดตเสมอ สามคือข้อได้เปรียบของทั้ง pipeline: คุณสแครปด้วย BeautifulSoup, ทำความสะอาดข้อมูลด้วย pandas, วิเคราะห์ sentiment ด้วย Hugging Face Transformers และสร้างแดชบอร์ดได้ในภาษาเดียว ไม่ต้องสลับบริบทไปมา
3 วิธีดึงข้อมูล TripAdvisor ด้วย Python (แบบเทียบกันชัดๆ)
คู่มือส่วนใหญ่จะเลือกแค่วิธีเดียวแล้วพุ่งไปเลย ซึ่งไม่ค่อยช่วยเท่าไรถ้าคุณยังต้องตัดสินใจ ก่อน จะเขียนโค้ด นี่คือตารางเปรียบเทียบที่ผมอยากให้มีคนยื่นให้ตั้งแต่แรก:
| วิธี | ความเร็ว | รองรับ JS | รับมือบอทได้ดีแค่ไหน | ความซับซ้อน | เหมาะกับ |
|---|---|---|---|---|---|
requests + BeautifulSoup | ⚡ เร็ว (~120–200 หน้า/นาที แบบดิบ) | ❌ ไม่มี | ⚠️ ต่ำ | ง่าย | หน้า listing แบบ static, โปรเจกต์ขนาดเล็ก |
| Selenium / Headless Browser | 🐢 ช้า (~8–20 หน้า/นาที) | ✅ เต็มรูปแบบ | ⚠️ ปานกลาง | ปานกลาง | เนื้อหาไดนามิก, การคลิก “Read more”, cookie banner |
| Hidden JSON / GraphQL API | ⚡⚡ เร็วที่สุด (~200–600 หน้า/นาที แบบดิบ) | N/A | ✅ สูงกว่า | ยาก | ดึงรีวิว/ข้อมูลโรงแรมจำนวนมาก |
| แบบไม่ต้องเขียนโค้ด (Thunderbit) | ⚡ เร็ว | ✅ มีในตัว | ✅ มีในตัว | ง่ายที่สุด | คนไม่เขียนโค้ด, export งานครั้งคราวแบบเร็วๆ |
มีข้อควรรู้เล็กน้อย ความเร็วแบบดิบด้านบนเป็นตัวเลขเชิงทฤษฎี — rate limit ของ TripAdvisor (~10–15 requests ต่อนาทีต่อ IP) จะจำกัด throughput จริงไว้ประมาณ 10 หน้า/นาทีต่อ IP ไม่ว่าคุณจะใช้วิธีไหนก็ตาม วิธี hidden JSON จะให้ข้อมูลต่อ request มากที่สุด แปลว่าต้องส่ง request น้อยลงและเสี่ยงโดน rate limit น้อยลง Selenium ช้ากว่าวิธี request-based ประมาณ 5 เท่าในงานจริง แต่เป็นตัวเลือกเดียวเมื่อคุณต้องกดปุ่มหรือ render JavaScript
ส่วนที่เหลือของคู่มือนี้จะพาไปทีละวิธีด้วยโค้ดครบถ้วน เลือกวิธีที่เข้ากับสถานการณ์ของคุณ หรือจะผสมกันก็ได้ (ผมมักใช้ requests+BS4 สำหรับหน้า listing และใช้ hidden JSON สำหรับหน้ารายละเอียด)
เตรียม Python Environment
ก่อนเริ่ม มาจัด environment ให้พร้อม คุณต้องใช้ Python 3.10+ (ผมแนะนำ 3.12 หรือ 3.13 — แพ็กเกจหลักรองรับหมดและไม่มีปัญหาที่รู้กัน)
ติดตั้งทุกอย่างในครั้งเดียว:
pip install requests beautifulsoup4 selenium httpx parsel pandas curl-cffi
หมายเหตุแพ็กเกจ:
requests(2.33.1) — สำหรับ HTTP requests, ต้องใช้ Python 3.10+beautifulsoup4(4.14.3) — สำหรับ parse HTMLselenium(4.43.0) — สำหรับ browser automation, ต้องใช้ Python 3.10+httpx(0.28.1) — HTTP client แบบ asyncparsel(1.11.0) — ตัวเลือก CSS/XPath (เบากว่า BS4)pandas(3.0.2) — สำหรับ export ข้อมูล, ต้องใช้ Python 3.11+curl-cffi(0.15.0) — สำหรับเลียนแบบ TLS fingerprint (สำคัญมากในการหลบ Cloudflare)
ChromeDriver: ถ้าคุณใช้ Selenium ข่าวดีคือ ตั้งแต่ Selenium 4.6 เป็นต้นมา Selenium Manager จะดาวน์โหลดและแคช ChromeDriver ที่ถูกต้องให้อัตโนมัติ ไม่ต้องติดตั้งเอง และมันจับคู่เวอร์ชันให้แบบไดนามิก จึงไม่ต้องกังวลเรื่อง Chrome version ไม่ตรงกัน
Virtual environment (แนะนำ):
python -m venv tripadvisor-scraper
source tripadvisor-scraper/bin/activate # macOS/Linux
tripadvisor-scraper\Scripts\activate # Windows
วิธีที่ 1: ใช้ Requests และ BeautifulSoup ดึงข้อมูล TripAdvisor
นี่คือวิธีที่ง่ายที่สุด เหมาะมากกับการสแครปหน้า listing (ผลการค้นหาโรงแรม, รายการร้านอาหาร) ที่ข้อมูลอยู่ใน HTML แบบ static อยู่แล้ว ไม่ต้องเปิดเบราว์เซอร์ ไม่ต้อง render JavaScript ใช้ทรัพยากรน้อย
เข้าใจรูปแบบ URL ของ TripAdvisor
URL ของ TripAdvisor มีรูปแบบที่เดาได้ตามหมวดหมู่:
- 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 โดยแต่ละหน้าจะแสดง 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
ถ้าต้องการรีวิวทุกภาษา ให้เติม ?filterLang=ALL ต่อท้าย URL
ส่ง Request ด้วย Header ที่สมจริง
TripAdvisor ตรวจ header ค่อนข้างเข้ม ถ้าใช้ header ค่าเริ่มต้นของ 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 ของคุณสอดคล้องกันหรือไม่ ถ้าคุณอ้างว่าเป็น Chrome 135 ใน User-Agent แต่ Sec-CH-UA บอกว่า Chrome 120 คุณจะถูกจับได้ ควรหมุนทั้งชุด header ไปพร้อมกัน ไม่ใช่หมุนทีละตัว
Parse รายการด้วย BeautifulSoup
เมื่อได้ response ที่สำเร็จแล้ว ก็แยกข้อมูลด้วย BeautifulSoup ได้เลย TripAdvisor ใช้แอตทริบิวต์ data-automation และ data-test-attribute ซึ่งเสถียรกว่าชื่อ class CSS มาก (ที่เปลี่ยนบ่อย):
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)
ข้อสังเกตเรื่อง selector: TripAdvisor ใช้ CSS class แบบ obfuscated เช่น FGwzt, yyzcQ ซึ่งจะเปลี่ยนทุกครั้งที่เว็บอัปเดต ดังนั้นควรยึด data-automation และ data-test-target เป็นหลักเสมอ เพราะเสถียรกว่า
จัดการ Pagination
ถ้าจะสแครปหลายหน้า ให้ลูปผ่าน offset และเว้นเวลาระหว่าง request อย่างสุภาพ:
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}: ได้ status {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)) # หน่วงแบบสุ่มเพื่อลดโอกาสโดน rate limit
df = pd.DataFrame(all_hotels)
print(f"\nรวมโรงแรมที่สแครปได้: {len(df)}")
time.sleep(random.uniform(3, 7)) สำคัญมาก TripAdvisor มี rate limit อยู่ราว 10–15 requests ต่อนาทีต่อ IP ถ้าเร็วเกินไปจะเจอ CAPTCHA หรือ 429 errors
ข้อจำกัดของวิธีนี้
แล้ววิธีนี้จะพังเมื่อไร? requests+BS4 ใช้ไม่ได้เมื่อ:
- TripAdvisor ส่งเนื้อหาที่ render ด้วย JavaScript (บางหน้าผลการค้นหาต้องใช้ JS)
- ข้อความรีวิวถูกตัดไว้หลังปุ่ม "Read more"
- ระบบ anti-bot ยกระดับเป็น JavaScript challenge หรือ CAPTCHA
- คุณต้องการข้อมูลที่มีเฉพาะหลัง client-side rendering เช่น ราคา หรือ availability
ถ้าเจอสถานการณ์แบบนี้ คุณต้องใช้ Selenium (วิธีที่ 2) หรือ hidden JSON method (วิธีที่ 3)
วิธีที่ 2: ใช้ Selenium ดึงข้อมูล TripAdvisor แบบ Headless Browser
Selenium เปิดเบราว์เซอร์จริง จึง render JavaScript ได้, กดปุ่มได้, รับมือ cookie consent banner ได้ และโต้ตอบกับเนื้อหาไดนามิกได้ ข้อแลกเปลี่ยนคือมันช้ากว่าประมาณ 5 เท่า และใช้ RAM ราว 300–500MB ต่อ browser instance
ตั้งค่า Selenium ให้หลบการตรวจจับ
ถ้าใช้แบบแกะกล่อง Selenium จะถูกตรวจจับได้ง่ายมาก ระบบ fingerprinting ของ 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 mode แบบใหม่ (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)
# ลบ webdriver property ออกจาก navigator
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
})
แค่นี้พอสำหรับ TripAdvisor ไหม? สำหรับงานสแครปขนาดเล็ก (ไม่เกิน 50 หน้า) ชุดนี้บวก residential proxies มักจะใช้ได้ แต่ถ้าปริมาณมากกว่านั้น คุณอาจต้องใช้ undetected-chromedriver หรือ nodriver — เพราะการป้องกันของ DataDome บน TripAdvisor วิเคราะห์สัญญาณกว่า 1,000 จุดต่อ request รวมถึง TLS fingerprint ที่ 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 consent popup (ถ้ามี)
try:
cookie_btn = driver.find_element(By.ID, "onetrust-accept-btn-handler")
cookie_btn.click()
time.sleep(1)
except:
pass # ไม่มี popup
# ดึงข้อมูลโรงแรม
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 วินาที — เทียบกับน้อยกว่า 1 วินาทีด้วย requests+BS4 ช่องว่าง 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")
# rating ถูก encode อยู่ใน 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)} รายการ")
เพิ่ม Proxy Rotation ให้ Selenium
ถ้าจะสแครปต่อเนื่องยาวๆ คุณต้องมีการหมุน proxy ด้วย ตอนนี้ selenium-wire เลิกดูแลแล้วตั้งแต่เดือนมกราคม 2024 ให้ใช้ proxy support ที่มากับ Chrome แทน:
# กรณี proxy ไม่มี authentication
proxy = "http://your-proxy-address:port"
options.add_argument(f"--proxy-server={proxy}")
# สำหรับ proxy ที่ต้อง auth ให้ใช้ Chrome extension หรือ Selenium 4 BiDi protocol
ถ้าจะหมุน proxy แบบโปรแกรมมิ่ง ให้สร้าง driver instance ใหม่พร้อม proxy ใหม่ทุก batch ของ request ถึงจะไม่เนี้ยบ แต่เชื่อถือได้
วิธีที่ 3: ใช้ Hidden JSON Method (ไม่ต้อง parse HTML เลย)
คู่มือส่วนใหญ่ไม่พูดถึงวิธีนี้ ซึ่งน่าเสียดาย เพราะนี่คือวิธีที่เร็วและสะอาดที่สุดในทั้งสามแบบ TripAdvisor ฝังข้อมูลโครงสร้างเป็น JSON ไว้ใน HTML โดยตรง — อยู่ใน <script> tag เป็น JavaScript variables อย่าง pageManifest และ urqlCache การดึง JSON ตรงนี้จะได้ข้อมูลที่สะอาดกว่า (เช่น เรตติ้งเป็นตัวเลข, วันที่เป็น ISO format) ใช้ request น้อยลง และไม่ต้อง render JavaScript เลย
หา JSON ที่ฝังอยู่ใน source ของหน้า
หัวใจของวิธีนี้คือ: ใช้ requests.get() ดึงหน้า แล้วค่อยสกัด JSON จาก HTML ดิบ โดยไม่ต้อง render 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)
# ดึง JSON blob ของ pageManifest
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]}")
จะหาชื่อ variable เองยังไง: เปิดหน้าโรงแรม TripAdvisor ใน Chrome คลิกขวา → View Page Source แล้วกด Ctrl+F หา pageManifest หรือ urqlCache หรือ aggregateRating ข้อมูลพวกนี้อยู่ในนั้นรอให้ parse อยู่แล้ว
Parse JSON และดึงข้อมูลแบบมีโครงสร้าง
TripAdvisor ยังฝัง schema.org data แบบ application/ld+json ซึ่งดึงง่ายยิ่งกว่า:
from parsel import Selector
sel = Selector(text=response.text)
# ดึง structured data แบบ 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 แบบ static และไม่ต้อง render JavaScript เลย มันให้ชื่อสถานที่, aggregate rating, จำนวนรีวิว, ที่อยู่, พิกัด, ช่วงราคา และ URL รูปภาพ — ทั้งหมดโดยไม่ต้อง parse HTML tag สักตัว
ถ้าต้องการข้อมูลที่ลึกขึ้น เช่น รีวิวแต่ละรายการ, การแจกแจงคะแนน, หรือรายการสิ่งอำนวยความสะดวก คุณจะต้องใช้ object 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 เพื่อหา review data
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"พบ cache entry ของรีวิว: {key[:50]}...")
break
เส้นทาง JSON ที่แน่นอนอาจเปลี่ยนบ้างเมื่อ TripAdvisor ปรับ frontend แต่โครงสร้างหลัก — JSON-LD สำหรับข้อมูลสรุป และ urqlCache สำหรับข้อมูลละเอียด — มีความเสถียรมาหลายปีแล้ว
Reverse-engineer TripAdvisor GraphQL API (ขั้นสูง)
ถ้าจะดึงข้อมูลในปริมาณมาก TripAdvisor GraphQL endpoint จะส่ง structured data กลับมาโดยตรง วิธีนี้เร็วที่สุดแต่ต้องดูแลมากที่สุด
import httpx
import random
import string
def generate_request_id():
"""สร้างค่าของ header 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 reviews: {total}")
for r in reviews[:3]:
print(f" [{r['rating']}/5] {r['title']} - {r['createdDate']}")
ข้อควรระวังสำคัญ: ค่า preRegisteredQueryId (เช่น 84b17ed122fbdbd4 สำหรับ search และ ef1a9f94012220d3 สำหรับรีวิว) อาจใช้ไม่ได้เมื่อ TripAdvisor deploy ใหม่ ถ้าเกิดขึ้น request ของคุณจะล้มเหลวแบบเงียบๆ คุณต้องค้นหา query ID ใหม่โดยดู network request ใน DevTools ของเบราว์เซอร์
ทำไมวิธีนี้ช่วยลดความจำเป็นในการใช้ proxy
คำนวณง่ายๆ ถ้าใช้ requests+BS4 การสแครป hotel detail 100 หน้า ต้องใช้ 100 requests แต่ถ้าใช้ hidden JSON method แต่ละ request จะได้ข้อมูลที่ต้องการจากหน้าเดียวครบถ้วน — ไม่ต้องส่ง request เพิ่มเพื่อขยายรีวิวหรือโหลดเนื้อหาแบบไดนามิก และถ้าใช้ GraphQL เรียก API ครั้งเดียวก็ได้รีวิว 20 รายการ Fewer requests = โอกาสโดน rate limiting น้อยลง = ต้องพึ่ง proxy rotation น้อยลง สำหรับโปรเจกต์ขนาดเล็กถึงกลาง (ไม่เกิน 1,000 หน้า) บางทีคุณอาจไม่ต้องใช้ proxy เลย ถ้าใส่ delay ที่เหมาะสม
สแครปโรงแรม ร้านอาหาร และสถานที่ท่องเที่ยวด้วยสคริปต์เดียว
คู่มือที่แข่งกันอยู่สี่ในห้าชิ้นจะพูดแค่โรงแรม แต่ TripAdvisor มีคอนเทนต์หลัก 3 หมวด และรูปแบบ URL กับ field ที่ต้องดึงก็ไม่เหมือนกัน นี่คือวิธีสร้างฟังก์ชันเดียวที่รองรับครบทั้งสาม
ฟิลด์ข้อมูลที่มีในแต่ละหมวด
| ฟิลด์ | โรงแรม | ร้านอาหาร | สถานที่ท่องเที่ยว |
|---|---|---|---|
| ชื่อ | ✅ | ✅ | ✅ |
| เรตติ้ง | ✅ | ✅ | ✅ |
| จำนวนรีวิว | ✅ | ✅ | ✅ |
| ราคา/ช่วงราคา | ✅ | ✅ | บางครั้ง |
| ที่อยู่ | ✅ | ✅ | ✅ |
| ประเภทอาหาร | ❌ | ✅ | ❌ |
| ระยะเวลา/ประเภททัวร์ | ❌ | ❌ | ✅ |
| สิ่งอำนวยความสะดวก | ✅ | ❌ | ❌ |
| พิกัด | ✅ | ✅ | ✅ |
สร้างฟังก์ชัน 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):
"""
สแครปข้อมูล listing ของ TripAdvisor สำหรับโรงแรม ร้านอาหาร หรือสถานที่ท่องเที่ยว
Args:
category: "hotels", "restaurants", หรือ "attractions"
location_id: TripAdvisor geo ID (เช่น "187147" สำหรับ Paris)
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}: ได้ status {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())
ฟังก์ชันเดียว ครอบคลุม 3 หมวด ไม่มีโค้ดซ้ำ ถ้า TripAdvisor เปลี่ยน selector คุณแก้แค่จุดเดียว
ถ้า TripAdvisor บล็อก ต้องทำอย่างไร? (Troubleshooting แบบ Anti-Bot)
นี่คือส่วนที่ผมต้องการมากที่สุดตอนเริ่มสแครป TripAdvisor และเป็นส่วนที่คู่มืออื่นไม่ค่อยจัดระบบให้ TripAdvisor ใช้ DataDome (วิเคราะห์ 5+ ล้านล้าน data points ต่อวัน) ร่วมกับ Cloudflare WAF นี่คือตารางวิเคราะห์ปัญหาที่พบบ่อย:
| อาการ | สาเหตุที่เป็นไปได้ | วิธีแก้ |
|---|---|---|
| ได้ HTTP 403 | header หายไปหรือดูน่าสงสัย; Cloudflare JS challenge | ตั้ง User-Agent, Accept-Language, Referer และ Sec-CH-UA ให้สมจริง และให้สอดคล้องกัน |
| เจอหน้า CAPTCHA แทนข้อมูล | โดน rate limit หรือ browser fingerprinting | หมุน residential proxies, ใส่หน่วงแบบสุ่ม (2–7 วินาทีระหว่าง request) |
| HTML ว่างหรือเนื้อหาหน้าเปล่า | requests ไม่ได้ render JavaScript | เปลี่ยนไปใช้ Selenium หรือดึงจาก hidden JSON ใน source |
| รีวิวถูกตัด / กด “Read more” ไม่ได้ผล | เนื้อหาโหลดผ่าน click event | ใช้ .click() ใน Selenium หรือดึงจาก JSON blob ที่ฝังอยู่ |
| รีวิวมีแค่ภาษาเดียว | ไม่มีพารามิเตอร์ภาษา | เติม ?filterLang=ALL ต่อท้าย URL รีวิว |
| ดึงไปได้แค่ N หน้าแล้วหยุด | โดนจำกัดแบบ session-based | หมุน session และลบคุกกี้ระหว่าง batch |
| HTTP 1020 Access Denied | IP/ASN ถูก Cloudflare บล็อก | เปลี่ยนจาก datacenter proxy ไปใช้ residential proxy |
| วน challenge ไม่จบ (CAPTCHA วนลูป) | cookie persistence มีปัญหา | วอร์มอัปเซสชันด้วยการเข้า homepage ก่อน และเก็บ cookie jar ไว้ |
Retry Logic แบบ Exponential Backoff
บทความอื่นแทบไม่เคยโชว์โค้ดส่วนนี้ นี่คือฟังก์ชัน retry ที่นำกลับไปใช้ได้:
import time
import random
import requests
def fetch_with_retry(session, url, max_retries=4, base_delay=2, max_delay=60):
"""
ดึง URL ด้วย exponential backoff และ jitter
หมุน User-Agent ทุกครั้งที่ retry
"""
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 ตอน retry
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}. retry {attempt + 1}/{max_retries} ใน {wait:.1f}s...")
time.sleep(wait)
continue
# status อื่นๆ — ไม่ retry
print(f" status ผิดปกติ {response.status_code} สำหรับ {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} ใน {wait:.1f}s...")
time.sleep(wait)
print(f" retry ครบ {max_retries} ครั้งแล้วสำหรับ {url}")
return None
หมุน Headers, Proxies และ Sessions
สำหรับการสแครปต่อเนื่อง ควรเก็บชุด header หลายแบบไว้แล้วหมุนพร้อมกัน:
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",
# เพิ่ม residential proxies อื่นๆ
]
def get_rotated_session():
"""สร้าง session ใหม่พร้อม header และ proxy ที่หมุนแล้ว"""
session = requests.Session()
# เลือก header set แบบสุ่ม
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)
# เลือก proxy แบบสุ่ม
if PROXY_LIST:
proxy = random.choice(PROXY_LIST)
session.proxies = {"http": proxy, "https": proxy}
return session
ชนิดของ proxy สำคัญมาก Datacenter proxy มักถูก TripAdvisor บล็อกแทบจะทันที (HTTP 1020 Access Denied) การสแครประยะยาวจำเป็นต้องใช้ residential proxy เพราะมันวิ่งผ่าน ISP ของผู้ใช้จริงและแยกจากผู้ใช้ทั่วไปแทบไม่ออก ควรคาดงบประมาณราว $2.50–$8.40/GB แล้วแต่ผู้ให้บริการ
ส่งออกและจัดเก็บข้อมูล TripAdvisor ที่สแครปได้
เมื่อได้ข้อมูลแล้ว การเอาไปไว้ในรูปแบบที่ใช้งานง่ายก็ไม่ยาก
Export เป็น 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
Export เป็น 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 ไฟล์ CSV:
hotels.csv— 1 แถวต่อ 1 ที่พัก (แบบ flat)reviews.csv— 1 แถวต่อ 1 รีวิว พร้อมproperty_idเป็น foreign key
วิธีนี้ทำให้ join ใน pandas, โหลดเข้าฐานข้อมูล หรือ import เข้า BI tools ได้ง่าย
ถ้าคุณไม่อยากยุ่งกับ logic การ export พวกนี้ Thunderbit ให้คุณ ส่งออกข้อมูลที่สแครปได้โดยตรง ไปยัง Excel, Google Sheets, Airtable หรือ Notion — ฟรีทั้งหมดและไม่ต้องเขียนโค้ด เหมาะมากเวลาต้องแชร์ผลกับทีมที่ไม่ใช่สายเทคนิค
เคล็ดลับสแครป TripAdvisor อย่างรับผิดชอบและมีประสิทธิภาพ
สแครปอย่างรับผิดชอบใน 6 ข้อ:
- เช็ก
robots.txt:robots.txtของ TripAdvisor บล็อกบอทสำหรับ training AI (GPTBot, ClaudeBot ฯลฯ) แบบเต็มๆ ส่วน crawler ทั่วไปจะเจอข้อจำกัดเฉพาะบาง path ดูได้ที่tripadvisor.com/robots.txt - ใส่ delay: เว้น 3–7 วินาทีระหว่าง request เป็นช่วงที่ค่อนข้างปลอดภัย ถ้าเร็วกว่า 10–15 requests ต่อนาทีต่อ IP จะเสี่ยง rate limit
- สแครปเฉพาะข้อมูลสาธารณะ อย่า login เพื่อเข้าถึงข้อมูลที่ปิดไว้
- เก็บข้อมูลอย่างปลอดภัย และปฏิบัติตาม GDPR/CCPA ถ้าจัดการข้อมูลส่วนบุคคล (เช่น ชื่อผู้รีวิว)
- พิจารณา TripAdvisor official API ถ้าคุณต้องการข้อมูลระดับเชิงพาณิชย์ Developer Portal ให้ข้อมูลธุรกิจ พร้อมรีวิวได้สูงสุด 5 รายการและรูป 5 รูปต่อสถานที่ — จำกัด แต่เสถียรและถูกต้องตามกฎหมาย
- เข้าใจบริบททางกฎหมาย: คำตัดสินของศาล EU คดี Ryanair (ธันวาคม 2025) ทำให้ข้อจำกัดการสแครปที่อิง ToS เข้มขึ้นทั่วสหภาพยุโรป TripAdvisor ระบุใน Terms of Service ว่าห้ามสแครปโดยตรง ควรสแครปอย่างมีความรับผิดชอบและรับความเสี่ยงด้วยตัวเอง
สรุปท้ายบท
ภาพรวมทั้งหมดเป็นแบบนี้
- Requests + BeautifulSoup คือทางที่ง่ายที่สุด เหมาะกับหน้า listing แบบ static, ตั้งค่าน้อย, และเร็ว เริ่มจากวิธีนี้ถ้าคุณสแครปน้อยกว่า 100 หน้าและไม่ต้องการคอนเทนต์ที่ render ด้วย JavaScript
- Selenium จัดการทุกอย่างที่ requests ทำไม่ได้: เนื้อหาไดนามิก, ปุ่ม “Read more”, cookie banner มันช้ากว่า 5 เท่าและกินทรัพยากร แต่เป็นตัวเลือกเดียวเมื่อคุณต้องโต้ตอบกับหน้าเว็บ
- Hidden JSON / GraphQL คือวิธีที่สะอาดและเร็วที่สุด ให้ข้อมูลที่มีโครงสร้างโดยไม่ต้อง parse HTML ลดจำนวน request และความจำเป็นในการใช้ proxy ลงได้มาก ข้อเสียคือเริ่มต้นต้อง reverse-engineer เยอะกว่า และอาจต้องดูแลเป็นครั้งคราวเมื่อ TripAdvisor เปลี่ยนโครงสร้างข้อมูล
ฟังก์ชัน scrape_tripadvisor() ที่ทำให้ใช้ซ้ำได้ ครอบคลุมทั้งโรงแรม ร้านอาหาร และสถานที่ท่องเที่ยว คุณไม่ควรต้องเปิดอ่าน tutorial อันที่สองอีก
และถ้าระหว่างอ่านไปคุณเริ่มรู้สึกว่าเขียนโค้ดไม่ใช่ทางของคุณ — หรือแค่ต้องการโรงแรม 50 แห่งลงสเปรดชีตภายในวันนี้ — Thunderbit Chrome extension ทำได้ในสองคลิก ด้วยการตรวจจับ field ด้วย AI, pagination อัตโนมัติ และ export ฟรีไป Excel หรือ Google Sheets ไม่ต้องใช้ Python เลย
ถ้าอยากลงลึกกว่านี้ เรายังมีคู่มือสแครปเพิ่มเติมบน Thunderbit blog และ ช่อง YouTube
คำถามที่พบบ่อย
1. การสแครป TripAdvisor ถูกกฎหมายไหม?
TripAdvisor ระบุใน Terms of Service ว่าห้ามสแครปโดยตรง อย่างไรก็ตาม ศาลส่วนใหญ่มองว่าการสแครปข้อมูลที่เปิดให้สาธารณะเข้าถึงได้ (ไม่ต้องล็อกอิน) ไม่ถือว่าละเมิด Computer Fraud and Abuse Act ในสหรัฐฯ ถึงอย่างนั้น คำตัดสินของศาล EU คดี Ryanair ปี 2025 ได้เพิ่มความเข้มของข้อจำกัดที่อิง ToS ในยุโรป ควรสแครปเฉพาะข้อมูลสาธารณะ เคารพ robots.txt ไม่เผยแพร่เนื้อหาที่มีลิขสิทธิ์ซ้ำ และปรึกษาที่ปรึกษากฎหมายหากนำข้อมูลไปใช้เชิงพาณิชย์
2. ฉันสแครป TripAdvisor โดยไม่ใช้ Python ได้ไหม?
ได้ เครื่องมือแบบไม่ต้องเขียนโค้ดอย่าง Thunderbit สามารถสแครป TripAdvisor ได้จากเบราว์เซอร์ของคุณโดยตรง ด้วย AI ที่ช่วยตรวจจับ field และจัดการ pagination อัตโนมัติ คุณยังใช้ browser extension, Google Sheets add-on หรือ commercial scraping API ได้ด้วย Python ให้การควบคุมและยืดหยุ่นที่สุด แต่ไม่ใช่ตัวเลือกเดียว
3. จะหลบการโดนบล็อกตอนสแครป TripAdvisor ยังไง?
กลยุทธ์หลักคือ ใช้ header ที่สมจริงและสอดคล้องกัน (โดยเฉพาะ User-Agent และ Sec-CH-UA), หมุน residential proxy (datacenter IP มักโดนบล็อกทันที), ใส่ delay แบบสุ่ม 3–7 วินาทีระหว่าง request, ใช้วิธี hidden JSON เพื่อลดจำนวน request, ทำ retry logic ด้วย exponential backoff, และวอร์มอัปเซสชันด้วยการเข้า homepage ก่อนเริ่มสแครปหน้าลึกๆ
4. ฉันดึงข้อมูลอะไรจาก TripAdvisor ได้บ้าง?
โรงแรม ร้านอาหาร และสถานที่ท่องเที่ยว — รวมถึงชื่อ, เรตติ้ง, จำนวนรีวิว, ช่วงราคา, ที่อยู่, พิกัด, สิ่งอำนวยความสะดวก (โรงแรม), ประเภทอาหาร (ร้านอาหาร), ระยะเวลาทัวร์ (สถานที่ท่องเที่ยว), และข้อความรีวิวเต็มพร้อมคะแนนรายรีวิวและวันที่ วิธี hidden JSON และ GraphQL จะให้ข้อมูลที่ครบและลึกที่สุดต่อ request
5. หนึ่งวันฉันสแครป TripAdvisor ได้กี่หน้า?
ถ้าใช้ IP เดียวและเว้นเวลาพอสมควร: ประมาณ 600–1,000 หน้า/วัน ถ้าใช้ residential proxies 20 ตัวแบบหมุน: ประมาณ 200,000–300,000 หน้า/วันด้วยวิธี request-based ส่วน Selenium ช้ากว่า — คาดได้ราว 8,000–12,000 หน้า/วันต่อ proxy วิธี hidden JSON/GraphQL จะให้ข้อมูลต่อ request มากที่สุด ดังนั้นคุณอาจต้องดึงหน้าโดยรวมน้อยลงเพื่อให้ได้ข้อมูลเท่ากัน
เรียนรู้เพิ่มเติม


