Google ปิด API สำหรับ Flights ไปตั้งแต่ปี 2018 แต่ราคาตั๋วเครื่องบินยังคงเปลี่ยนตลอดเวลา — สำหรับเส้นทางบินภายในประเทศหนึ่งเส้นทาง ราคาอาจแกว่งได้ สูงสุด 17 ครั้งใน 48 ชั่วโมง ถ้าคุณต้องการเข้าถึงข้อมูลนี้แบบอัตโนมัติ การสแครปแทบจะเป็นทางเลือกเดียวที่ใช้งานได้จริง
ผมใช้เวลาทดสอบหลายวิธีเพื่อดึงข้อมูลเที่ยวบินจาก Google มาไม่น้อย และสถานการณ์ก็เปลี่ยนไปอย่างชัดเจน โดยเฉพาะหลังจาก Google เปิดใช้ SearchGuard ในเดือนมกราคม 2025 คู่มือนี้จะพาคุณสร้าง Python scraper สำหรับ Google Flights ด้วย Playwright อธิบายวิธีรับมือระบบป้องกันบอทที่ทำให้คนส่วนใหญ่ไปไม่รอด และต่อยอดเป็นตัวติดตามราคาที่แจ้งเตือนอัตโนมัติ หากคุณไม่อยากเขียนโค้ดเอง ผมจะสาธิตทางลัดแบบไม่ต้องเขียนโค้ดด้วย Thunderbit ซึ่งให้ผลลัพธ์แบบเดียวกันในเวลาราว 2 นาที
ทำไมต้องสแครป Google Flights ด้วย Python?
Google Flights ครองตลาดการค้นหาเที่ยวบินอย่างแท้จริง ความสามารถในการมองเห็นบนมือถือในสหรัฐฯ พุ่งขึ้นไปถึง 47.6% แซงหน้า OTA รายใหญ่ทุกเจ้า ส่วนตลาด metasearch ด้านท่องเที่ยวที่อยู่เบื้องหลังมีมูลค่า $8.33 พันล้านในปี 2024 และเติบโตด้วยอัตรา CAGR 30.2% แต่ตั้งแต่ QPX Express API ถูก ปิดถาวรเมื่อ 10 เมษายน 2018 ก็ไม่มีช่องทางอย่างเป็นทางการให้เข้าถึงข้อมูลนี้แบบโปรแกรมได้อีก
ในขณะเดียวกัน ราคาตั๋วเครื่องบินสำหรับเส้นทางเดียวกันอาจผันผวนได้ สูงถึง 50% โดยราคาต่ำสุดกับสูงสุดต่างกันเฉลี่ยราว 20 ดอลลาร์ สายการบินอย่าง Delta ใช้ถึง 77 fare buckets เพื่อกำหนดราคาแบบไดนามิก ค่าเฉลี่ยตั๋วไป-กลับในสหรัฐฯ ช่วงต้นปี 2026 อยู่ที่ 408 ดอลลาร์ และค่าโดยสารยัง สูงขึ้น 14.9% เมื่อเทียบกับปีก่อน
แพลตฟอร์มใหญ่ ไม่มี API ราคาเหวี่ยงแรง นี่จึงเป็นเหตุผลที่การสแครป Google Flights ด้วย Python กลายเป็นหนึ่งในโปรเจ็กต์ยอดนิยมทั้งบน GitHub และในฟอรั่มสายท่องเที่ยว
ใครบ้างได้ประโยชน์ และได้อย่างไร:
| ประเภทผู้ใช้ | กรณีใช้งาน | ประโยชน์หลัก |
|---|---|---|
| นักเดินทางทั่วไป | ติดตามราคาสำหรับเส้นทางที่ต้องการตามช่วงเวลา | ประหยัดได้เฉลี่ย $50 ต่อเที่ยวบิน |
| เอเจนซี่ท่องเที่ยว | วิเคราะห์ราคาคู่แข่ง | ติดตามความเท่าเทียมของค่าโดยสารแบบเรียลไทม์ |
| ทีมท่องเที่ยวองค์กร | เพิ่มประสิทธิภาพด้านต้นทุนในหลายเส้นทาง | ประหยัดงบการเดินทางองค์กรได้ 10–30% |
| นักพัฒนา | สร้างแอปเปรียบเทียบค่าโดยสาร | เข้าถึงข้อมูลราคาด้วยโปรแกรม |
| นักวิจัย | วิเคราะห์ความผันผวนของราคาสายการบิน | ใช้ทำวิจัยเชิงวิชาการและตลาด |
ผู้ใช้ในฟอรั่มพูดตรง ๆ ว่าทำไมถึงหันมาสแครป: "Google Flights API was discontinued and I should use web scraping instead" เป็นความเห็นที่โผล่มาซ้ำ ๆ และผลลัพธ์ก็คุ้มจริง — Hopper ระบุว่าทำนายความแม่นยำได้ 95% จากการวิเคราะห์ใบเสนอราคามากกว่า 5 พันล้านรายการต่อวัน ขณะที่ข้อมูลของ Expedia ในปี 2026 ชี้ว่าจองล่วงหน้า 8–15 วันช่วยประหยัดได้ราว $25 สำหรับเที่ยวบินภายในประเทศ
ข้อมูลอะไรบ้างที่ดึงจาก Google Flights ได้?
หน้าผลลัพธ์ของ Google Flights มีข้อมูลที่ละเอียดกว่าที่หลายคนคิด โดยทั่วไปจะมีข้อมูลเหล่านี้:
- ชื่อสายการบิน (พร้อมโลโก้)
- เวลาออกเดินทาง และรหัสสนามบิน
- เวลาถึง และรหัสสนามบิน
- ระยะเวลาเดินทางรวม
- จำนวนจุดแวะพัก และรายละเอียดการต่อเครื่อง (สนามบิน ระยะเวลา และมีค้างคืนหรือไม่)
- ราคาตั๋ว (ตามสกุลเงิน)
- การปล่อย CO2 (kg CO2e พร้อมเปอร์เซ็นต์เทียบกับเที่ยวบินทั่วไป)
- ชั้นโดยสาร หมายเลขเที่ยวบิน รุ่นเครื่องบิน
- ข้อมูลพื้นที่วางขา
- สิ่งอำนวยความสะดวก (Wi-Fi, ปลั๊กไฟ, สตรีมมิงสื่อ)
- ตัวบ่งชี้ระดับราคา (ต่ำ/ทั่วไป/สูง)
- คำเตือนเรื่องดีเลย์ ("มักล่าช้าเกิน 30 นาที")
ข้อมูลที่มีให้จะแตกต่างกันไปตามเส้นทาง วันที่ และประเภทตั๋ว (เที่ยวเดียวหรือไป-กลับ) ตัวอย่างข้อมูลเที่ยวบินที่สแครปได้หนึ่งรายการในรูป JSON:
{
"search_date": "2026-04-16",
"route": "SFO-JFK",
"departure_date": "2026-05-15",
"flights": [
{
"airline": "United Airlines",
"flight_number": "UA123",
"departure_time": "08:00",
"departure_airport": "SFO",
"arrival_time": "16:35",
"arrival_airport": "JFK",
"duration_minutes": 335,
"stops": 0,
"price_usd": 287,
"price_level": "low",
"co2_kg": 156,
"co2_vs_typical": "-12%",
"travel_class": "Economy"
}
]
}
การเตรียมสภาพแวดล้อม Python
ก่อนเริ่มเขียนโค้ดสแครป คุณต้องเตรียมของไม่กี่อย่างให้พร้อมก่อน
สิ่งที่ต้องมี:
- ระดับความยาก: ปานกลาง
- เวลาที่ใช้: ประมาณ 1–2 ชั่วโมงสำหรับบทความเต็ม
- สิ่งที่ต้องมี: Python 3.7+, พื้นฐาน Python, เบราว์เซอร์ที่ใช้ Chromium ได้
ติดตั้งไลบรารีที่ต้องใช้
เราจะใช้ Playwright สำหรับควบคุมเบราว์เซอร์ (Google Flights เรนเดอร์ด้วย JavaScript ล้วน ๆ — ยิง HTTP แบบธรรมดาจะไม่ได้อะไรที่ใช้ได้), พร้อมตัวช่วยอีกเล็กน้อย:
pip install playwright playwright-stealth pandas
playwright install chromium
- Playwright — ระบบอัตโนมัติสำหรับเบราว์เซอร์แบบ headless รองรับการเรนเดอร์ JavaScript และมีระบบรอโหลดในตัว
- playwright-stealth — ช่วยอำพรางสัญญาณที่บ่งบอกว่าเป็นบอท
- pandas — ใช้สำหรับวิเคราะห์ข้อมูลและส่งออกเป็น CSV ในภายหลัง
ทำไมเลือก Playwright แทน Selenium หรือ Requests
Google Flights ใช้ requests + BeautifulSoup อย่างเดียวไม่ได้ เพราะเนื้อหาของหน้าเว็บถูกเรนเดอร์ด้วย JavaScript ทั้งหมด คุณจึงต้องใช้เบราว์เซอร์จริง
| ฟีเจอร์ | Playwright | Selenium | requests + BS4 |
|---|---|---|---|
| การเรนเดอร์ JS | รองรับเต็มรูปแบบ | รองรับเต็มรูปแบบ | ไม่มี |
| ความเร็ว | เร็วกว่าโดยรวม 42% | ฐานมาตรฐาน | ไม่เหมาะกับงานนี้ |
| รองรับ Async | มีในตัว | ทำงานแบบลำดับเป็นหลัก | ไม่เกี่ยว |
| การใช้หน่วยความจำ | น้อยกว่า 30% | สูงกว่า | น้อยที่สุด |
| หลบการตรวจจับบอท | ดี (เมื่อใช้ stealth) | ถูกตรวจจับง่ายกว่า | ไม่เกี่ยว |
Playwright เร็วกว่า ทันสมัยกว่า และรองรับ async ได้ดีกว่า สำหรับ Google Flights นี่คือคำตอบที่ชัดเจน
ทีละขั้น: วิธีสแครป Google Flights ด้วย Python
นี่คือแกนหลักของบทความ เราจะสร้าง scraper ไปทีละส่วน

ขั้นที่ 1: กำหนด Data Class ของคุณ
เริ่มจากจัดโครงสร้างพารามิเตอร์การค้นหาและข้อมูลเที่ยวบินด้วย Python dataclasses จะช่วยให้โค้ดสะอาดและต่อยอดง่ายภายหลัง
from dataclasses import dataclass, field
from typing import Optional, List
@dataclass
class SearchParams:
origin: str # เช่น "SFO"
destination: str # เช่น "JFK"
departure_date: str # เช่น "2026-05-15"
return_date: Optional[str] = None
trip_type: str = "one-way" # "one-way" หรือ "round-trip"
travel_class: str = "economy"
@dataclass
class FlightData:
airline: str = ""
departure_time: str = ""
arrival_time: str = ""
duration: str = ""
stops: str = ""
price: str = ""
co2_emissions: str = ""
แต่ละฟิลด์ถูกแมปตรงกับข้อมูลที่เราจะดึงจากหน้าเว็บ การมีโครงสร้างนี้ตั้งแต่แรกจะช่วยไม่ให้ต้องแบก dictionary ยุ่ง ๆ ไปมาในภายหลัง
ขั้นที่ 2: ทำความเข้าใจโครงสร้าง URL ของ Google Flights
Google Flights เข้ารหัสพารามิเตอร์การค้นหาไว้ในพารามิเตอร์ URL ชื่อ tfs โดยใช้ Base64-encoded Protobuf คุณจะ reverse-engineer การเข้ารหัสนี้ก็ได้ หรือเลือกวิธีที่ง่ายกว่า: สร้าง URL แบบข้อความธรรมชาติ
วิธีที่ง่ายที่สุดคือใช้รูปแบบ query search:
https://www.google.com/travel/flights?q=flights+from+SFO+to+JFK+on+2026-05-15&curr=USD
ถ้าต้องการควบคุมมากขึ้น สามารถสร้าง URL ด้วยโค้ดได้:
def build_flights_url(origin: str, destination: str, date: str) -> str:
base = "https://www.google.com/travel/flights"
query = f"flights from {origin} to {destination} on {date}"
return f"{base}?q={query.replace(' ', '+')}&curr=USD"
อีกทางหนึ่งคือ reverse-engineer การเข้ารหัส Protobuf ซึ่งให้ความแม่นยำมากกว่า แต่พังได้เมื่อ Google เปลี่ยนรูปแบบภายใน ไลบรารีอย่าง fast-flights บน GitHub ใช้การ decode Protobuf เพื่อเลี่ยงการ parse HTML ไปเลย แต่แนวทางนั้นถือว่าเป็นขั้นสูงกว่า
ขั้นที่ 3: เปิดเบราว์เซอร์และเข้า Google Flights
ต่อไปคือการตั้งค่า Playwright เราใช้ playwright-stealth เพื่อลดความเสี่ยงการถูกตรวจจับตั้งแต่ต้น
import asyncio
from playwright.async_api import async_playwright
from playwright_stealth import Stealth
async def scrape_flights(params: SearchParams) -> List[FlightData]:
async with Stealth().use_async(async_playwright()) as pw:
browser = await pw.chromium.launch(
headless=True,
args=[
"--disable-blink-features=AutomationControlled",
"--disable-dev-shm-usage",
"--no-first-run",
]
)
context = await browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent=(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/125.0.0.0 Safari/537.36"
),
locale="en-US",
timezone_id="America/New_York",
)
# ตั้งคุกกี้ consent ล่วงหน้าเพื่อข้าม popup
await context.add_cookies([{
"name": "SOCS",
"value": "CAESHwgBEhJnd3NfMjAyNTAyMjctMF9SQzIaBXpoLUNOIAEaBgiAy6O-Bg",
"domain": ".google.com",
"path": "/"
}])
page = await context.new_page()
เรารันแบบ headless สำหรับ production (ถ้าจะดีบักให้เปลี่ยนเป็น headless=False), ตั้ง viewport และ user agent ให้สมจริง และตั้งคุกกี้ SOCS ไว้ล่วงหน้าเพื่อข้ามหน้าต่างขอความยินยอม — จะอธิบายในส่วน anti-bot อีกที
ขั้นที่ 4: ไปยังหน้าผลการค้นหา
โหลด URL ที่สร้างไว้ แล้วรอให้ผลลัพธ์เที่ยวบินแสดงขึ้นมา:
url = build_flights_url(
params.origin, params.destination, params.departure_date
)
await page.goto(url, wait_until="networkidle")
# รอให้ผลลัพธ์เที่ยวบินโหลด
await page.wait_for_selector(
"li.pIav2d", timeout=15000
)
ถ้าเจอ timeout ตรงนี้ ส่วนใหญ่แปลว่ามี popup consent บังหน้าเว็บ (ดูวิธีแก้ด้วย cookie ในขั้นที่ 3) หรือ Google กำลังส่ง CAPTCHA มาให้ เราจะพูดถึงทั้งสองกรณีในส่วน anti-bot
ขั้นที่ 5: โหลดผลลัพธ์เที่ยวบินทั้งหมด
Google Flights ซ่อนผลลัพธ์เพิ่มเติมไว้หลังปุ่ม "Show more flights" คุณต้องกดปุ่มนี้ซ้ำไปเรื่อย ๆ จนกว่าจะเห็นทุกเที่ยวบิน:
# กด "Show more flights" จนกว่าผลลัพธ์ทั้งหมดจะโหลดครบ
while True:
try:
more_button = page.locator(
'button:has-text("Show more flights")'
)
if await more_button.is_visible(timeout=3000):
await more_button.click()
await page.wait_for_timeout(2000)
else:
break
except Exception:
break
ลูปนี้จะคลิกปุ่ม รอ 2 วินาทีให้ผลลัพธ์ใหม่เรนเดอร์ แล้วหยุดเมื่อปุ่มไม่แสดงแล้ว จากการทดสอบของผม เส้นทางส่วนใหญ่มีผลลัพธ์ 1–3 หน้า
ขั้นที่ 6: ดึงข้อมูลเที่ยวบินด้วย CSS Selector
ตอนนี้เราจะ parse ข้อมูลเที่ยวบินจริงจากหน้าที่โหลดแล้ว นี่คือ selector ที่ตรวจสอบแล้ว ณ เดือนเมษายน 2026 — ดูหัวข้อการดูแลรักษาด้านล่างว่าทำไมวันที่นี้ถึงสำคัญ:
flights = []
cards = await page.query_selector_all("li.pIav2d")
for card in cards:
flight = FlightData()
# ชื่อสายการบิน
airline_el = await card.query_selector(
"div.sSHqwe span:not([class])"
)
if airline_el:
flight.airline = (await airline_el.inner_text()).strip()
# เวลาออกเดินทาง
dep_el = await card.query_selector(
'span[aria-label*="Departure time"]'
)
if dep_el:
flight.departure_time = (await dep_el.inner_text()).strip()
# เวลาถึง
arr_el = await card.query_selector(
'span[aria-label*="Arrival time"]'
)
if arr_el:
flight.arrival_time = (await arr_el.inner_text()).strip()
# ระยะเวลาเดินทาง
dur_el = await card.query_selector("div.gvkrdb")
if dur_el:
flight.duration = (await dur_el.inner_text()).strip()
# จำนวนจุดแวะพัก
stops_el = await card.query_selector("div.EfT7Ae span")
if stops_el:
flight.stops = (await stops_el.inner_text()).strip()
# ราคา
price_el = await card.query_selector(
"div.FpEdX span"
)
if price_el:
flight.price = (await price_el.inner_text()).strip()
# การปล่อย CO2
co2_el = await card.query_selector("div.O7CXue")
if co2_el:
flight.co2_emissions = (
await co2_el.get_attribute("aria-label") or ""
).strip()
flights.append(flight)
await browser.close()
return flights
ข้อควรระวัง: class อย่าง pIav2d, sSHqwe และ FpEdX ถูกสร้างโดย Closure Compiler ของ Google และสามารถเปลี่ยนได้ทุก build ส่วน selector ที่ใช้ aria-label จะเสถียรกว่า ผมจะอธิบายกลยุทธ์ดูแลระยะยาวในภายหลัง
ขั้นที่ 7: บันทึกผลลัพธ์เป็น JSON หรือ CSV
สุดท้าย ให้บันทึกข้อมูลที่สแครปได้พร้อมเวลาไว้ด้วย ซึ่งสำคัญมากสำหรับการติดตามราคาในอนาคต:
import json
from datetime import datetime
from dataclasses import asdict
async def main():
params = SearchParams(
origin="SFO",
destination="JFK",
departure_date="2026-05-15"
)
flights = await scrape_flights(params)
output = {
"search_date": datetime.now().isoformat(),
"params": asdict(params),
"flights": [asdict(f) for f in flights],
}
with open("flights.json", "w") as f:
json.dump(output, f, indent=2)
# บันทึกเป็น CSV ด้วย
import pandas as pd
df = pd.DataFrame([asdict(f) for f in flights])
df["search_date"] = datetime.now().isoformat()
df["route"] = f"{params.origin}-{params.destination}"
df.to_csv("flights.csv", index=False)
print(f"Scraped {len(flights)} flights")
asyncio.run(main())
รันแล้วคุณจะได้ไฟล์ flights.json และ flights.csv ที่มีข้อมูลผลลัพธ์ จากการทดสอบของผม การค้นหา SFO-JFK มักคืนตัวเลือกเที่ยวบินประมาณ 30–80 รายการ และใช้เวลาราว 15–20 วินาที
คู่มือเอาตัวรอดจากระบบป้องกันบอทของ Google Flights
หลายบทความหยุดแค่นี้ แต่ scraper ส่วนใหญ่พังตรงนี้ Google เปิดใช้ SearchGuard ในเดือนมกราคม 2025 ซึ่งทำให้ SERP scraper แทบทั้งหมดใช้ไม่ได้ในชั่วข้ามคืน Google อธิบายว่ามันเป็น "ผลลัพธ์ของการลงทุนทั้งเวลาและงบประมาณมหาศาล" Google Flights ถูกประเมินว่ามีความยากในการสแครประดับ 4/5
ไม่มีบทความคู่แข่งลงลึกเรื่องนี้ ทั้งที่นี่คือสาเหตุอันดับหนึ่งที่ scraper ของคนส่วนใหญ่หยุดทำงาน มาดูกันว่าคุณกำลังเจอกับอะไร และรับมืออย่างไร

ใส่ดีเลย์สุ่มระหว่างการร้องขอ
เป็นแนวป้องกัน rate limit ที่ง่ายที่สุด โค้ดแค่สองบรรทัด แต่ได้ผลระดับกลาง:
import time
import random
time.sleep(random.uniform(3, 7))
ให้แทรกไว้ระหว่างการเปลี่ยนหน้า ถ้าใช้ช่วงเวลาคงที่ เช่น ทุก 5 วินาทีเป๊ะ ๆ จะดูผิดปกติ ควรสุ่มค่าแทน
หมุน User-Agent
การส่ง user-agent เดิมซ้ำทุกครั้งเป็นสัญญาณที่จับได้ง่าย ควรสุ่มจากรายการ:
import random
USER_AGENTS = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/125.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 Safari/605.1.15",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0",
]
user_agent = random.choice(USER_AGENTS)
หลบการตรวจจับโหมด Headless
Google ตรวจ navigator.webdriver และสัญญาณ automation อื่น ๆ ไลบรารี playwright-stealth ช่วยจัดการส่วนใหญ่ได้ แต่คุณควรตั้ง launch arguments ตามที่แสดงในขั้นที่ 3 ด้วย ธงสำคัญคือ:
args=[
"--disable-blink-features=AutomationControlled",
"--disable-dev-shm-usage",
"--no-first-run",
]
วิธีนี้ช่วยผ่านการตรวจจับพื้นฐานได้ SearchGuard ตรวจลึกกว่านั้น — ดูพฤติกรรมการขยับเมาส์ จังหวะพิมพ์ และรูปแบบการเลื่อนหน้า — แต่ถ้าปริมาณการสแครปไม่สูงมาก stealth mode ร่วมกับดีเลย์ที่สมจริงมักเพียงพอ
หมุนพร็อกซี: Datacenter vs Residential
ถ้าจะค้นหาเกินไม่กี่ครั้ง คุณจะต้องใช้พร็อกซี ความต่างตรงนี้สำคัญมาก:
| ฟีเจอร์ | Datacenter Proxy | Residential Proxy |
|---|---|---|
| ความเร็ว | 100–1,000 Mbps | 10–100 Mbps |
| อัตราสำเร็จ (Google) | 20–40% | 85–95% |
| ค่าใช้จ่าย | $0.10–$0.50/IP/เดือน | $5–$15/GB |
| ความเสี่ยงถูกตรวจจับ | สูง | ต่ำมาก |
เมื่อสแครปเว็บที่มีการป้องกัน Residential proxy มีต้นทุนต่อคำขอที่สำเร็จต่ำกว่ามาก ถึงราว 180 เท่า ผู้ให้บริการในปี 2026 เช่น Smartproxy เริ่มต้นที่ $7/GB, Bright Data $8.40/GB และ Oxylabs $8/GB
เพิ่มพร็อกซีให้ Playwright แบบนี้:
browser = await pw.chromium.launch(
proxy={"server": "http://proxy-host:port",
"username": "user", "password": "pass"}
)
วิธีรับมือ popup ขอความยินยอมคุกกี้
ผู้ใช้มักบอกตรงกันว่า popup "I agree to terms" เป็นตัวขัดขวาง: "first google will show you the 'I agree to terms and conditions' popup." วิธีที่สะอาดที่สุดคือเซ็ตคุกกี้ SOCS ไว้ล่วงหน้า (ตามขั้นที่ 3) ถ้ายังไม่ได้ผล ให้คลิกผ่านมันไป:
try:
accept_btn = page.locator('button:has-text("Accept all")')
if await accept_btn.is_visible(timeout=3000):
await accept_btn.click()
await page.wait_for_timeout(1000)
except Exception:
pass # ไม่มี popup
หมายเหตุ: ข้อความบนปุ่มอาจเปลี่ยนตามภาษา เช่น "Alle akzeptieren" ในเยอรมัน หรือ "Tout accepter" ในฝรั่งเศส
สรุปด่วนเรื่อง anti-bot
| เทคนิค | ความยาก | ประสิทธิภาพ | ต้องเขียนโค้ดไหม? |
|---|---|---|---|
| ดีเลย์สุ่ม (2–7 วินาที) | ต่ำ | ปานกลาง | 2 บรรทัด |
| หมุน user-agent | ต่ำ | ปานกลาง | 5 บรรทัด |
| หลบการตรวจจับ headless | ปานกลาง | สูง | Playwright launch args |
| ปลั๊กอิน playwright-stealth | ปานกลาง | 60–80% บนเว็บพื้นฐาน | pip install |
| หมุน proxy (datacenter) | ปานกลาง | ปานกลาง | ตั้งค่า |
| หมุน proxy (residential) | ปานกลาง | สำเร็จ 85–95% | ตั้งค่า |
| ตั้งคุกกี้ consent ล่วงหน้า (SOCS) | ต่ำ | จำเป็น | 1 บรรทัด |
สำหรับอัตราการใช้งานที่ปลอดภัย: เว้นช่วง 10–20 วินาทีระหว่างคำขอพร้อมหมุน IP Google มีเกณฑ์คร่าว ๆ ว่าประมาณ 100 requests/minute ต่อ IP ก่อนจะเจอ 429 และถ้าใช้งานต่อเนื่องเกิน 1,000 requests/day ต่อ IP อาจโดนแบนชั่วคราว
ทำไม selector ของ Google Flights ถึงพังบ่อย และแก้อย่างไร
นี่คือจุดปวดหัวอันดับหนึ่งแบบทิ้งห่าง ฟอรั่มเต็มไปด้วยข้อความประมาณ "สุดท้ายก็ได้กลับมาแค่ list ว่าง 14 อัน" ทุกบทความชอบยื่น selector ให้ แต่ไม่มีใครบอกว่าทำไมมันถึงพัง
ทำไม selector ของ Google Flights ถึงเปลี่ยน
มีอยู่ 3 เหตุผลหลัก:
-
การทำให้ชื่อคลาสอ่านไม่ออกด้วย Closure Compiler Google ใช้ Closure Stylesheets เพื่อสร้าง class name อย่าง
BVAVmfและYMlIzผ่านgoog.setCssNameMapping()ซึ่งเปลี่ยนทุกครั้งที่ build — บางทีก็ทุกสัปดาห์ -
A/B testing ผู้ใช้แต่ละกลุ่มอาจเห็นโครงสร้าง HTML ต่างกันในเวลาเดียวกัน scraper ของคุณอาจใช้ได้บนเครื่องคุณ แต่ล้มเหลวเมื่อรันจากภูมิภาคอื่น
-
ความต่างของ locale ผู้ใช้ในยุโรปอาจเห็นคำศัพท์ รูปแบบหน้า และแม้แต่ฟิลด์ข้อมูลต่างจากผู้ใช้ในสหรัฐฯ
เขียน selector ให้ทนทานขึ้น
เลือก selector ที่ยึดกับความหมาย มากกว่าหน้าตา:
# เปราะบาง — พังทุกครั้งที่มี build ใหม่
price_el = await card.query_selector("div.BVAVmf > div.YMlIz")
# ทนทานกว่า — ผูกกับ accessibility label
Dep_el = await card.query_selector('span[aria-label*="Departure time"]')
# ทนทานเช่นกัน — จับตามข้อความ
more_btn = page.locator('button:has-text("Show more flights")')
ลำดับความเสถียรของ selector (จากเสถียรที่สุดไปน้อยที่สุด):
- แอตทริบิวต์
aria-label— ผูกกับ accessibility มักไม่เปลี่ยนง่าย - แอตทริบิวต์
data-*— ถูกเพิ่มมาเพื่อฟังก์ชันโดยตรง - แอตทริบิวต์
role— ARIA roles เป็นเชิงความหมาย - selector แบบจับข้อความ — ตรงกับเนื้อหาที่มองเห็น
- การจับชื่อคลาสบางส่วน — เช่น
[class*="price"] - ชื่อคลาสที่ obfuscate เต็มรูปแบบ — ควรหลีกเลี่ยงถ้าเป็นไปได้
เพิ่มฟังก์ชันตรวจสอบความถูกต้อง
อย่าปล่อยให้ selector ที่พังเงียบ ๆ ทำให้ได้ข้อมูลว่างโดยไม่รู้ตัว ควรจับให้ได้เร็ว:
import logging
logger = logging.getLogger(__name__)
def validate_flight(data: FlightData) -> bool:
required = ["airline", "price", "departure_time",
"arrival_time", "duration"]
valid = True
for field_name in required:
if not getattr(data, field_name, ""):
logger.warning(
f"Missing '{field_name}' — selectors may need updating"
)
valid = False
return valid
ให้รันฟังก์ชันนี้กับทุก flight ที่สแครปได้ ถ้าเริ่มเห็น warning ก็ถึงเวลาตรวจหน้าเว็บและอัปเดต selector แล้ว
กลยุทธ์ดูแล selector ในระยะยาว
- ตรวจ selector ทุกเดือน หรือทันทีเมื่อคุณภาพของผลลัพธ์ลดลง
- เก็บ selector ไว้ใน dictionary แยกต่างหากเพื่อแก้ไขง่าย
- Selector ในบทความนี้ตรวจล่าสุด: เมษายน 2026
- พิจารณาใช้ fast-flights library เป็นทางเลือก — มัน decode Protobuf แทนการใช้ CSS selector จึงเลี่ยงปัญหานี้ได้ทั้งหมด (แต่ก็ยังมีความเปราะบางของตัวเองเมื่อ Google เปลี่ยนรูปแบบข้อมูลภายใน)
จากการสแครปครั้งเดียว สู่ Google Flights Price Tracker แบบอัตโนมัติ
บทความส่วนใหญ่จบที่ "บันทึกเป็น JSON" แต่หัวข้อของบทความนี้คือ "Price Alerts" ดังนั้นถึงเวลาส่งมอบของจริง
![]()
ตั้งเวลาให้ scraper ทำงานอัตโนมัติ
ตัวเลือก 1: ไลบรารี schedule ของ Python (ง่ายที่สุด ใช้ได้ข้ามแพลตฟอร์ม):
import schedule
import time
def run_scraper():
asyncio.run(main())
schedule.every().day.at("06:00").do(run_scraper)
schedule.every().day.at("18:00").do(run_scraper)
while True:
schedule.run_pending()
time.sleep(60)
ตัวเลือก 2: cron job (Linux/Mac):
# รันทุกวันเวลา 6 โมงเช้าและ 6 โมงเย็น
0 6,18 * * * cd /path/to/scraper && python scraper.py
ตัวเลือก 3: Windows Task Scheduler — สร้าง task พื้นฐานให้รัน python scraper.py ตามเวลาที่ต้องการ
ข้อแลกเปลี่ยนคือทุกวิธีต้องมีเครื่องที่เปิดอยู่ตลอด ถ้ารันบนแล็ปท็อปที่หลับเอง คุณจะพลาดรอบสแครป
เก็บข้อมูลราคาย้อนหลัง
เปลี่ยนจากการเขียนทับไฟล์ JSON มาเป็นการเพิ่มข้อมูลลงใน SQLite database:
import sqlite3
from datetime import datetime
def init_db():
conn = sqlite3.connect("flights.db")
conn.execute("""
CREATE TABLE IF NOT EXISTS flights (
id INTEGER PRIMARY KEY AUTOINCREMENT,
scrape_date TEXT NOT NULL,
route TEXT NOT NULL,
airline TEXT,
departure_time TEXT,
arrival_time TEXT,
duration TEXT,
stops TEXT,
price_usd REAL,
co2_emissions TEXT
)
""")
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_route_date "
"ON flights(route, scrape_date)"
)
conn.commit()
return conn
def save_flight(conn, route: str, flight: FlightData):
price_num = float(
flight.price.replace("$", "").replace(",", "")
) if flight.price else None
conn.execute(
"INSERT INTO flights "
"(scrape_date, route, airline, departure_time, "
"arrival_time, duration, stops, price_usd, co2_emissions) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
(datetime.now().isoformat(), route, flight.airline,
flight.departure_time, flight.arrival_time,
flight.duration, flight.stops, price_num,
flight.co2_emissions)
)
conn.commit()
หลังจากสแครปวันละสองครั้งไปหนึ่งสัปดาห์ คุณก็จะมีข้อมูลพอสำหรับเริ่มมองเห็นแนวโน้มแล้ว
วิเคราะห์แนวโน้มราคาและตั้งการแจ้งเตือน
หาตัวเลือกที่ถูกที่สุดจากข้อมูลย้อนหลังของคุณ:
import pandas as pd
import sqlite3
conn = sqlite3.connect("flights.db")
df = pd.read_sql_query(
"SELECT * FROM flights WHERE route = 'SFO-JFK'", conn
)
summary = df.groupby("scrape_date")["price_usd"].agg(
["min", "max", "mean"]
)
cheapest = df.loc[df["price_usd"].idxmin()]
print(
f"Cheapest: ${cheapest['price_usd']:.0f} on "
f"{cheapest['scrape_date']} ({cheapest['airline']})"
)
ส่งอีเมลแจ้งเตือนเมื่อราคาลดต่ำกว่าที่ตั้งไว้:
import smtplib
from email.mime.text import MIMEText
def send_price_alert(route, price, threshold, recipient):
msg = MIMEText(
f"Price drop alert! {route}: ${price:.0f} "
f"(below your ${threshold:.0f} threshold)"
)
msg["Subject"] = f"Flight Deal: {route} at ${price:.0f}"
msg["From"] = "alerts@example.com"
msg["To"] = recipient
with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
server.login("alerts@example.com", "your_app_password")
server.send_message(msg)
# หลังจากสแครปแต่ละครั้ง ให้เช็กราคาดีล
min_price = df["price_usd"].min()
threshold = 250
if min_price < threshold:
send_price_alert("SFO-JFK", min_price, threshold,
"you@email.com")
สำหรับความถี่ที่แนะนำ: วันละสองครั้งก็เพียงพอสำหรับการติดตามราคาส่วนตัว และช่วยลดความเสี่ยงการถูกตรวจจับ ถ้าทำในระดับธุรกิจให้ทุก 4–6 ชั่วโมงพร้อมหมุนพร็อกซี ส่วนรายชั่วโมงควรใช้เฉพาะช่วงลดราคาสั้น ๆ และควรใช้แค่ชั่วคราว
ทางเลือกที่ง่ายกว่า: Scheduled Scraper ของ Thunderbit
ถ้าการต้องดูแล cron job, เซิร์ฟเวอร์ที่ต้องเปิดตลอด และการตั้งค่าพร็อกซีดูเป็นโครงสร้างที่มากเกินไปสำหรับคุณ Thunderbit มี Scheduled Scraper ที่ทำงานแทน use case เดียวกันโดยไม่ต้องแบกภาระเหล่านั้น คุณเพียงอธิบายช่วงเวลาที่ต้องการสแครปด้วยภาษาธรรมดา ใส่ URL ของ Google Flights แล้ว scraper จะทำงานอัตโนมัติบน cloud infrastructure ของ Thunderbit — พร้อมระบบจัดการ anti-bot ในตัว และส่งออกผลลัพธ์ไปยัง Google Sheets, Excel หรือ Airtable ได้โดยตรง มันไม่ใช่ตัวแทนของแนวทาง Python เต็มรูปแบบ (เพราะคุณจะเสียความยืดหยุ่นบางอย่างไป) แต่ถ้าเป้าหมายของคุณคือ "อยากมีสเปรดชีตราคาตั๋ว" นี่คือทางลัดที่เร็วที่สุด คุณสามารถลองได้ใน แพ็กเกจฟรี
ถ้า Python ใหญ่เกินความจำเป็น: วิธีไม่ต้องเขียนโค้ดเพื่อสแครป Google Flights
หลังจากสร้างทุกอย่างข้างบนแล้ว ผมพูดตรง ๆ ว่ามันมีหลายชิ้นส่วนพอสมควร ไม่ใช่ทุกคนที่ต้องการการควบคุมระดับนี้ selector พัง, proxy ต้องหมุน, cron job ต้องเฝ้า ถ้าเป้าหมายของคุณคือ "เอาราคาตั๋วเข้า spreadsheet เป็นประจำ" ยังมีทางเลือกที่เร็วกว่า
เปรียบเทียบ: Python ทำเอง vs API Services vs Thunderbit
| แนวทาง | เวลาในการตั้งค่า | ต้องเขียนโค้ดไหม | จัดการ Anti-Bot | การตั้งเวลา | ค่าใช้จ่าย |
|---|---|---|---|---|---|
| DIY Playwright (บทความนี้) | 1–2 ชั่วโมง | Python (ระดับกลาง) | ต้องตั้งค่าเอง | ต้องตั้งเอง (cron) | ฟรี + ค่า proxy |
| SerpApi Google Flights endpoint | 15 นาที | แค่เรียก API | จัดการให้แล้ว | ผ่าน API | ประมาณ $50+/เดือน |
| Thunderbit Chrome Extension | 2 นาที | ไม่ต้อง | Cloud scraping | มี scheduler ในตัว | มีแพ็กเกจฟรี |
หมายเหตุเรื่อง SerpApi: Google ยื่นฟ้อง DMCA ต่อ SerpApi ในเดือนธันวาคม 2025 โดยอ้างว่าปริมาณคำขอของพวกเขาเพิ่มขึ้น 25,000% ภายในสองปี ความไม่แน่นอนทางกฎหมายแบบนี้ควรพิจารณาอย่างจริงจังถ้าคุณกำลังประเมินผู้ให้บริการ API
Thunderbit สแครป Google Flights อย่างไร
เปิดหน้าผลลัพธ์ Google Flights ใน Chrome แล้วกดปุ่ม Thunderbit "AI Suggest Fields" — AI จะอ่านหน้าจอและแนะนำคอลัมน์ เช่น airline, price, departure time, stops จากนั้นตรวจทานฟิลด์ที่แนะนำ แล้วกด "Scrape" ผลลัพธ์จะปรากฏในตารางที่คุณส่งออกไปยัง Excel, Google Sheets, Airtable หรือ Notion ได้ — ทั้งหมดนี้ใช้ได้ใน แพ็กเกจฟรี
สำหรับ use case การติดตามราคาโดยเฉพาะ Thunderbit's Scheduled Scraper และ Cloud Scraping (ที่รองรับการทำงานพร้อมกันถึง 50 หน้า) เข้ามาแทนทั้งชุด cron + proxy + server ได้เลย
Python ให้การควบคุมเต็มรูปแบบและปรับแต่งได้ไม่จำกัด Thunderbit ให้ความเร็วและไม่ต้องดูแลระบบ เลือกตามเป้าหมายจริงของคุณ หากอยากเรียนรู้วิธีสแครปแบบไม่ต้องเขียนโค้ดเพิ่มเติม ลองอ่านคู่มือ web scraper แบบ no-code ที่ดีที่สุด

การสแครป Google Flights ถูกกฎหมายไหม? สิ่งที่คุณควรรู้
ผู้ใช้ในฟอรั่มมักถามเรื่องนี้บ่อย: "scraping Google Flights directly violates the TOS that Google has." เป็นข้อกังวลที่สมเหตุสมผล โดยเฉพาะเมื่อไม่มี API ทางการให้ใช้แทน
การละเมิด ToS กับความรับผิดทางกฎหมาย
ข้อกำหนดการใช้งานของ Google (อัปเดต 22 พฤษภาคม 2024) ระบุว่าผู้ใช้ต้องไม่ "เข้าถึงหรือใช้บริการหรือเนื้อหาใด ๆ ผ่านวิธีการอัตโนมัติ (เช่น robots, spiders หรือ scrapers)" การละเมิด ToS คือการผิดสัญญา (คดีแพ่ง) — ไม่ใช่เรื่องเดียวกับการทำผิดกฎหมาย
บรรทัดฐานทางกฎหมายสำคัญคือ hiQ v. LinkedIn (ศาลอุทธรณ์ Ninth Circuit, 2022) ซึ่งวางหลักว่าการสแครปข้อมูลที่เปิดเผยต่อสาธารณะไม่ถือว่าละเมิด Computer Fraud and Abuse Act (CFAA) อย่างไรก็ตาม คดีนั้นจบด้วยการยอมความ และคดีที่ Google ฟ้อง SerpApi ในเดือนธันวาคม 2025 ใช้ทฤษฎีกฎหมายอีกแบบ — DMCA Section 1201 (การหลบเลี่ยงมาตรการป้องกันทางเทคโนโลยี) — ซึ่งอาจร้ายแรงกว่า
แนวทางปฏิบัติที่ดีสำหรับการสแครปอย่างรับผิดชอบ
- จำกัดอัตราการร้องขอ — เว้น 10–20 วินาที พร้อมหมุน IP
- อย่าสแครปข้อมูลส่วนบุคคล — ราคาตั๋วเป็นข้อมูลสาธารณะเชิงสรุป
- อย่าหลบ CAPTCHA แบบอัตโนมัติ (นี่คือจุดเสี่ยงด้าน DMCA)
- ใช้ข้อมูลเพื่อการวิจัยส่วนตัว ไม่ใช่สร้างสินค้าการค้าแข่งโดยไม่มีไลเซนส์ที่เหมาะสม
- พิจารณา API ทางการ เมื่อมีให้ใช้
แหล่งข้อมูลทางเลือก
ถ้าคิดว่าการสแครปเสี่ยงเกินไปสำหรับ use case ของคุณ ยังมีตัวเลือก API ที่ถูกต้องตามกฎหมาย:
| ผู้ให้บริการ | ค่าใช้จ่าย | Free Tier | หมายเหตุ |
|---|---|---|---|
| SerpApi | $75–$3,750+/เดือน | 250 searches/เดือน | JSON ของ Google Flights โดยตรง (อยู่ระหว่างการพิจารณาด้านกฎหมาย) |
| Kiwi Tequila | ฟรี (โมเดล affiliate) | ไม่จำกัด | เหมาะกับสตาร์ทอัพและการทดสอบ |
| Amadeus | จ่ายตามการใช้งาน | 2,000 requests/เดือน | สายการบินมากกว่า 400 ราย และรองรับการจอง |
| Skyscanner | กำหนดเอง | ต้องขออนุมัติ | 52 ตลาด 30 ภาษา |
เรามีบทความเจาะลึกเรื่อง ผลกระทบทางกฎหมายของ web scraping หากคุณอยากดูภาพรวมแบบครบถ้วน
บทสรุปและข้อคิดสำคัญ
ข้อมูลเยอะพอสมควร สรุปสิ่งที่สำคัญจริง ๆ:
- Python + Playwright เป็นแนวทางที่ยืดหยุ่นที่สุดสำหรับการสแครป Google Flights แต่ต้องดูแลรักษาต่อเนื่อง
- มาตรการ anti-bot เช่น ดีเลย์ การหมุน user-agent และ residential proxy ไม่ใช่ทางเลือกเสริม แต่เป็นสิ่งจำเป็นต่อความเสถียร โดยเฉพาะหลัง SearchGuard
- Selector พังบ่อย — ใช้
aria-labelและ selector แบบจับข้อความให้มากที่สุด ตรวจสอบผลลัพธ์ และตั้งรอบบำรุงรักษา - ทำอัตโนมัติด้วย
scheduleหรือ cron เพื่อเปลี่ยนการสแครปครั้งเดียวให้กลายเป็นตัวติดตามราคาจริง พร้อมข้อมูลย้อนหลังและอีเมลแจ้งเตือน - Thunderbit มีทางเลือกไม่ต้องเขียนโค้ด พร้อมการตั้งเวลาในตัว cloud scraping และการจัดการ anti-bot — เหมาะมากถ้าเป้าหมายของคุณคือสเปรดชีตราคาตั๋ว ไม่ใช่โปรเจ็กต์เขียนโค้ด
- เคารพขอบเขตทางกฎหมาย — จำกัดอัตราการร้องขอ สแครปเฉพาะข้อมูลสาธารณะ และพิจารณาทางเลือก API หากใช้งานเชิงพาณิชย์
หยิบโค้ดจากบทความนี้ไปใช้ หรือจะติดตั้ง Thunderbit Chrome extension เพื่อไปทางลัดก็ได้ ไม่ว่าจะเลือกแบบไหน คุณก็จะเริ่มติดตามราคาตั๋วได้แทนที่จะต้องรีเฟรช Google Flights เองซ้ำ ๆ
ถ้าต้องการเทคนิคสแครปด้วย Python เพิ่มเติม ดูคู่มือของเราเกี่ยวกับ วิธี web scrape ด้วย Python และ เครื่องมือ web scraping ที่ดีที่สุดสำหรับ Python
คำถามที่พบบ่อย
1. ฉันสแครป Google Flights ได้โดยไม่ใช้ Python ไหม?
ได้ บริการ API อย่าง SerpApi และ Kiwi Tequila ให้ข้อมูลเที่ยวบินแบบมีโครงสร้างผ่าน API calls โดยไม่ต้องควบคุมเบราว์เซอร์ สำหรับแนวทางที่ไม่ต้องเขียนโค้ดเลย Thunderbit Chrome extension สามารถสแครปผลลัพธ์ Google Flights จากเบราว์เซอร์ของคุณได้โดยตรง พร้อมฟิลด์ที่ AI แนะนำและส่งออกได้ด้วยคลิกเดียว
2. Google บล็อกการสแครปเที่ยวบินไหม?
Google ใช้การตรวจจับบอท (SearchGuard), CAPTCHA และ rate limiting ถ้าตั้งค่าป้องกันบอทอย่างเหมาะสม — ดีเลย์สุ่ม, หมุน user-agent, residential proxy และตั้งค่า stealth browser — คุณยังสแครปได้อย่างน่าเชื่อถือในระดับปริมาณปานกลาง ดูส่วน anti-bot ด้านบนสำหรับเทคนิคและเกณฑ์โดยละเอียด
3. ควรสแครป Google Flights บ่อยแค่ไหนสำหรับติดตามราคา?
วันละสองครั้งแบบสุ่มเวลาดีเพียงพอสำหรับการติดตามราคาส่วนตัว และช่วยลดความเสี่ยงการถูกตรวจจับ ถ้าทำเพื่อธุรกิจให้ทุก 4–6 ชั่วโมงพร้อมหมุน proxy หลีกเลี่ยงการสแครปรายชั่วโมง ยกเว้นช่วงโปรโมชันสั้น ๆ เพราะจะเพิ่มโอกาสโดนบล็อกอย่างมาก
4. มี Google Flights API ฟรีไหม?
Google QPX Express API แบบทางการถูก ยกเลิกในเดือนเมษายน 2018 และไม่มีเวอร์ชันทดแทนที่เป็นทางการแบบฟรี ตัวเลือกฟรีที่ใกล้เคียงที่สุดคือ Kiwi Tequila API (โมเดล affiliate, ค้นหาได้ไม่จำกัด) ส่วน SerpApi ให้ 250 searches ฟรีต่อเดือน สำหรับผู้ใช้ส่วนใหญ่ การสแครปหรือใช้เครื่องมือ no-code อย่าง Thunderbit คือทางเลือกที่ใช้งานได้จริง
5. ทำไม CSS selector ของ Google Flights ถึงได้ข้อมูลว่างตลอด?
Google ใช้ Closure Compiler สร้าง class name แบบ obfuscate ที่เปลี่ยนทุกครั้งที่ build รวมถึงมี A/B testing และความต่างของ locale ที่ทำให้โครงสร้าง HTML แตกต่างกันระหว่างผู้ใช้ วิธีแก้คือใช้ aria-label และ selector แบบจับข้อความแทนชื่อคลาส เพิ่มฟังก์ชันตรวจสอบความถูกต้องเพื่อจับปัญหาเร็ว และตรวจ selector ทุกเดือน ดูหัวข้อการดูแล selector สำหรับกลยุทธ์แบบละเอียด
เรียนรู้เพิ่มเติม


